GlanceNodeAssertionCollection.kt

/*
 * Copyright 2023 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.glance.testing

import androidx.annotation.RestrictTo

/**
 * Represents a collection of Glance nodes from the tree that can be asserted on.
 *
 * An instance of [GlanceNodeAssertionCollection] can be obtained from
 * [GlanceNodeAssertionsProvider.onAllNodes] and equivalent methods.
 */
// Equivalent to SemanticsNodeInteractionCollection in compose.
class GlanceNodeAssertionCollection<R, T : GlanceNode<R>>
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) constructor(
    private val testContext: TestContext<R, T>,
    private val selector: GlanceNodeSelector<R>
) {
    /**
     * Asserts that this collection of nodes is equal to the given [expectedCount].
     *
     * @throws AssertionError if the size is not equal to [expectedCount]
     */
    fun assertCountEquals(
        expectedCount: Int
    ): GlanceNodeAssertionCollection<R, T> {
        val errorMessageOnFail = "Failed to assert count of nodes"

        val actualCount = testContext.findMatchingNodes(selector, errorMessageOnFail).size
        if (actualCount != expectedCount) {
            throw AssertionError(
                buildErrorMessageWithReason(
                    errorMessageOnFail = errorMessageOnFail,
                    reason = buildErrorReasonForCountMismatch(
                        matcherDescription = selector.description,
                        expectedCount = expectedCount,
                        actualCount = actualCount
                    )
                )
            )
        }
        return this
    }

    /**
     * Asserts that all the nodes in this collection satisfy the given [matcher].
     *
     * Doesn't throw error if the collection is empty. Use [assertCountEquals] to assert on expected
     * size of the collection.
     *
     * @param matcher Matcher that has to be satisfied by all the nodes in the collection.
     * @throws AssertionError if the collection contains at least one element that does not satisfy
     * the given matcher.
     */
    fun assertAll(
        matcher: GlanceNodeMatcher<R>,
    ): GlanceNodeAssertionCollection<R, T> {
        val errorMessageOnFail = "Failed to assertAll(${matcher.description})"

        val filteredNodes = testContext.findMatchingNodes(selector, errorMessageOnFail)
        val violations = filteredNodes.filter {
            !matcher.matches(it)
        }
        if (violations.isNotEmpty()) {
            throw AssertionError(buildGeneralErrorMessage(errorMessageOnFail, violations))
        }
        return this
    }

    /**
     * Asserts that this collection contains at least one element that satisfies the given
     * [matcher].
     *
     * @param matcher Matcher that has to be satisfied by at least one of the nodes in the
     * collection.
     * @throws AssertionError if not at least one matching node was found.
     */
    fun assertAny(
        matcher: GlanceNodeMatcher<R>,
    ): GlanceNodeAssertionCollection<R, T> {
        val errorMessageOnFail = "Failed to assertAny(${matcher.description})"
        val filteredNodes = testContext.findMatchingNodes(selector, errorMessageOnFail)

        if (filteredNodes.isEmpty()) {
            throw AssertionError(
                buildErrorMessageWithReason(
                    errorMessageOnFail = errorMessageOnFail,
                    reason = buildErrorReasonForAtLeastOneNodeExpected(selector.description)
                )
            )
        }

        if (!matcher.matchesAny(filteredNodes)) {
            throw AssertionError(buildGeneralErrorMessage(errorMessageOnFail, filteredNodes))
        }
        return this
    }

    /**
     * Returns a [GlanceNodeAssertion] that can assert on the node at the given index of this
     * collection.
     *
     * Any subsequent assertion on its result will throw error if index is out of bounds of the
     * matching nodes found from previous operations.
     */
    operator fun get(index: Int): GlanceNodeAssertion<R, T> {
        return GlanceNodeAssertion(
            testContext = testContext,
            selector = selector.addIndexedSelector(index)
        )
    }

    /**
     * Returns a new collection of nodes by filtering the given nodes using the provided [matcher].
     */
    fun filter(matcher: GlanceNodeMatcher<R>): GlanceNodeAssertionCollection<R, T> {
        return GlanceNodeAssertionCollection(
            testContext,
            selector.addMatcherSelector(
                selectorName = "filter",
                matcher = matcher
            )
        )
    }
}