From cedd48f99e877f8b936ff574812d573baf231307 Mon Sep 17 00:00:00 2001 From: Andrei Salavei Date: Fri, 24 Jan 2025 15:31:09 +0100 Subject: [PATCH] Run selected instrumented tests (#5214) Add ability to run selected instrumented tests. Fix iOS version check. Unmute ignored tests. --- instrumented-test/README.md | 4 +- instrumented-test/gradle/libs.versions.toml | 5 +- .../ComponentsAccessibilitySemanticTest.kt | 17 +++---- .../androidx/compose/test/configuration.kt | 12 ++++- .../test/utils/AccessibilityTestNode.kt | 10 ++-- .../androidx/compose/test/utils/OsVersion.kt | 6 +-- .../test/utils/UIKitInstrumentedTest.kt | 5 ++ .../androidx/compose/xctest/configuration.kt | 50 +++++++++++++++---- 8 files changed, 74 insertions(+), 35 deletions(-) diff --git a/instrumented-test/README.md b/instrumented-test/README.md index a5996f47af..2ccb54819f 100644 --- a/instrumented-test/README.md +++ b/instrumented-test/README.md @@ -7,7 +7,7 @@ This project is a Compose Multiplatform module that implements instrumented UI t ## Requirements - Kotlin >= 2.1.0 -- Compose Multiplatform 1.8.0-alpha01 +- Compose Multiplatform 1.8.0-alpha02 - iOS 12+ ## Testing @@ -16,5 +16,5 @@ To execute XCTest cases on an iOS Simulator, use: ```shell cd launcher -xcrun xcodebuild test -scheme Launcher -destination "platform=iOS Simulator,name=iPhone 16 Pro" +xcodebuild test -scheme Launcher -destination "platform=iOS Simulator,name=iPhone 16 Pro" ``` diff --git a/instrumented-test/gradle/libs.versions.toml b/instrumented-test/gradle/libs.versions.toml index da839df2bf..9e4706da62 100644 --- a/instrumented-test/gradle/libs.versions.toml +++ b/instrumented-test/gradle/libs.versions.toml @@ -1,13 +1,10 @@ [versions] androidx-lifecycle = "2.8.4" -compose-multiplatform = "1.8.0-alpha01" +compose-multiplatform = "1.8.0-alpha02" junit = "4.13.2" kotlin = "2.1.0" [libraries] -kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } -kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" } -junit = { group = "junit", name = "junit", version.ref = "junit" } androidx-lifecycle-viewmodel = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-viewmodel", version.ref = "androidx-lifecycle" } androidx-lifecycle-runtime-compose = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "androidx-lifecycle" } diff --git a/instrumented-test/ui-instrumented-test/src/iosMain/kotlin/androidx/compose/test/accessibility/ComponentsAccessibilitySemanticTest.kt b/instrumented-test/ui-instrumented-test/src/iosMain/kotlin/androidx/compose/test/accessibility/ComponentsAccessibilitySemanticTest.kt index 902b4121bb..e6f7b9039b 100644 --- a/instrumented-test/ui-instrumented-test/src/iosMain/kotlin/androidx/compose/test/accessibility/ComponentsAccessibilitySemanticTest.kt +++ b/instrumented-test/ui-instrumented-test/src/iosMain/kotlin/androidx/compose/test/accessibility/ComponentsAccessibilitySemanticTest.kt @@ -17,7 +17,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.test.utils.assertAccessibilityTree import androidx.compose.test.utils.available -import androidx.compose.test.utils.findNode +import androidx.compose.test.utils.findNodeWithTag import androidx.compose.test.utils.runUIKitInstrumentedTest import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier @@ -35,7 +35,6 @@ import androidx.compose.ui.viewinterop.UIKitView import platform.UIKit.* import kotlin.test.* -@Ignore // TODO: Uncomment when switching to 1.8.0-alpha02 class ComponentsAccessibilitySemanticTest { @OptIn(ExperimentalMaterialApi::class) @Test @@ -97,7 +96,7 @@ class ComponentsAccessibilitySemanticTest { } var oldValue = sliderValue - val sliderNode = findNode("Slider") + val sliderNode = findNodeWithTag("Slider") sliderNode.element?.accessibilityIncrement() assertTrue(oldValue < sliderValue) @@ -177,19 +176,19 @@ class ComponentsAccessibilitySemanticTest { } } - findNode("Switch").element?.accessibilityActivate() + findNodeWithTag("Switch").element?.accessibilityActivate() assertTrue(switch) waitForIdle() - findNode("Switch").element?.accessibilityActivate() + findNodeWithTag("Switch").element?.accessibilityActivate() assertFalse(switch) - findNode("Checkbox").element?.accessibilityActivate() + findNodeWithTag("Checkbox").element?.accessibilityActivate() assertTrue(checkbox) waitForIdle() - findNode("Checkbox").element?.accessibilityActivate() + findNodeWithTag("Checkbox").element?.accessibilityActivate() assertFalse(checkbox) - findNode("TriStateCheckbox").element?.accessibilityActivate() + findNodeWithTag("TriStateCheckbox").element?.accessibilityActivate() assertEquals(ToggleableState.On, triStateCheckbox) } @@ -227,7 +226,7 @@ class ComponentsAccessibilitySemanticTest { } } - findNode("RadioButton").element?.accessibilityActivate() + findNodeWithTag("RadioButton").element?.accessibilityActivate() assertAccessibilityTree { node { isAccessibilityElement = true diff --git a/instrumented-test/ui-instrumented-test/src/iosMain/kotlin/androidx/compose/test/configuration.kt b/instrumented-test/ui-instrumented-test/src/iosMain/kotlin/androidx/compose/test/configuration.kt index 94732ede19..bec2e0ff66 100644 --- a/instrumented-test/ui-instrumented-test/src/iosMain/kotlin/androidx/compose/test/configuration.kt +++ b/instrumented-test/ui-instrumented-test/src/iosMain/kotlin/androidx/compose/test/configuration.kt @@ -9,6 +9,14 @@ import kotlinx.cinterop.ExperimentalForeignApi import androidx.compose.xctest.* import platform.XCTest.XCTestSuite -// TODO: create a configuration setup procedure with test selection and reporting +@Suppress("unused") @OptIn(ExperimentalForeignApi::class) -fun testSuite(): XCTestSuite = setupXCTestSuite() +fun testSuite(): XCTestSuite = setupXCTestSuite( + // Run all test cases from the tests + // BasicInteractionTest::class, + // LayersAccessibilityTest::class, + + // Run test cases from a test + // BasicInteractionTest::testButtonClick, + // LayersAccessibilityTest::testLayersAppearanceOrder +) diff --git a/instrumented-test/ui-instrumented-test/src/iosMain/kotlin/androidx/compose/test/utils/AccessibilityTestNode.kt b/instrumented-test/ui-instrumented-test/src/iosMain/kotlin/androidx/compose/test/utils/AccessibilityTestNode.kt index 82249a1f2e..8f77d16626 100644 --- a/instrumented-test/ui-instrumented-test/src/iosMain/kotlin/androidx/compose/test/utils/AccessibilityTestNode.kt +++ b/instrumented-test/ui-instrumented-test/src/iosMain/kotlin/androidx/compose/test/utils/AccessibilityTestNode.kt @@ -42,11 +42,11 @@ internal fun UIKitInstrumentedTest.getAccessibilityTree(): AccessibilityTestNode if (count == NSIntegerMax) { when (element) { is UITableView -> { - TODO("Unused in tests. Implement correct table view traversal.") + println("warning: UITableView is currently unsupported") } is UICollectionView -> { - TODO("Unused in tests. Implement correct collection view traversal.") + println("warning: UICollectionView is currently unsupported") } is UIView -> { @@ -244,9 +244,9 @@ internal fun UIKitInstrumentedTest.assertAccessibilityTree( assertAccessibilityTree(validator) } -internal fun UIKitInstrumentedTest.findNode(identifier: String) = findNodeOrNull { - it.identifier == identifier -} ?: fail("Unable to find node with identifier: $identifier") +internal fun UIKitInstrumentedTest.findNodeWithTag(tag: String) = findNodeOrNull { + it.identifier == tag +} ?: fail("Unable to find node with identifier: $tag") internal fun UIKitInstrumentedTest.findNodeWithLabel(label: String) = findNodeOrNull { it.label == label diff --git a/instrumented-test/ui-instrumented-test/src/iosMain/kotlin/androidx/compose/test/utils/OsVersion.kt b/instrumented-test/ui-instrumented-test/src/iosMain/kotlin/androidx/compose/test/utils/OsVersion.kt index 746d5b19bc..9b13c2d483 100644 --- a/instrumented-test/ui-instrumented-test/src/iosMain/kotlin/androidx/compose/test/utils/OsVersion.kt +++ b/instrumented-test/ui-instrumented-test/src/iosMain/kotlin/androidx/compose/test/utils/OsVersion.kt @@ -13,9 +13,9 @@ import platform.Foundation.NSProcessInfo internal fun available(iosMajorVersion: Int, iosMinorVersion: Int = 0): Boolean { return NSProcessInfo.processInfo.operatingSystemVersion.useContents { when { - majorVersion.toInt() > iosMajorVersion -> false - majorVersion.toInt() < iosMajorVersion -> true - minorVersion.toInt() > iosMinorVersion -> false + majorVersion.toInt() < iosMajorVersion -> false + majorVersion.toInt() > iosMajorVersion -> true + minorVersion.toInt() < iosMinorVersion -> false else -> true } } diff --git a/instrumented-test/ui-instrumented-test/src/iosMain/kotlin/androidx/compose/test/utils/UIKitInstrumentedTest.kt b/instrumented-test/ui-instrumented-test/src/iosMain/kotlin/androidx/compose/test/utils/UIKitInstrumentedTest.kt index a3aad86483..2aa3a7203f 100644 --- a/instrumented-test/ui-instrumented-test/src/iosMain/kotlin/androidx/compose/test/utils/UIKitInstrumentedTest.kt +++ b/instrumented-test/ui-instrumented-test/src/iosMain/kotlin/androidx/compose/test/utils/UIKitInstrumentedTest.kt @@ -104,6 +104,11 @@ internal class UIKitInstrumentedTest { } } + fun delay(timeoutMillis: Long) { + val runLoop = NSRunLoop.currentRunLoop() + runLoop.runUntilDate(NSDate.dateWithTimeIntervalSinceNow(timeoutMillis.toDouble() / 1000.0)) + } + fun waitUntil( conditionDescription: String? = null, timeoutMillis: Long = 5_000, diff --git a/instrumented-test/ui-xctest/src/iosMain/kotlin/androidx/compose/xctest/configuration.kt b/instrumented-test/ui-xctest/src/iosMain/kotlin/androidx/compose/xctest/configuration.kt index de7e5b7407..1a2c9827f4 100644 --- a/instrumented-test/ui-xctest/src/iosMain/kotlin/androidx/compose/xctest/configuration.kt +++ b/instrumented-test/ui-xctest/src/iosMain/kotlin/androidx/compose/xctest/configuration.kt @@ -12,6 +12,10 @@ import platform.XCTest.XCTest import platform.XCTest.XCTestObservationCenter import platform.XCTest.XCTestSuite import kotlin.native.internal.test.* +import kotlin.reflect.KClass +import kotlin.reflect.KFunction +import kotlin.reflect.KFunction1 + // Top level suite name used to hold all Native tests internal const val TOP_LEVEL_SUITE = "Kotlin/Native test suite" @@ -25,6 +29,14 @@ private const val TEST_ARGUMENTS_KEY = "KotlinNativeTestArgs" */ private lateinit var testSettings: TestSettings +@Suppress("unused") +fun setupXCTestSuite(vararg tests: KClass<*>): XCTestSuite = + setupXCTestSuite(tests = tests.toSet(), testCases = null) + +@Suppress("unused") +inline fun setupXCTestSuite(vararg tests: KFunction1): XCTestSuite = + setupXCTestSuite(tests = null, testCases = mapOf(Class::class.qualifiedName to tests.toSet())) + /** * This is an entry-point of XCTestSuites and XCTestCases generation. * Function returns the XCTest's top level TestSuite that holds all the test cases @@ -32,7 +44,7 @@ private lateinit var testSettings: TestSettings * This test suite can be run by either native launcher compiled to bundle or * by the other test suite (e.g. compiled as a framework). */ -fun setupXCTestSuite(): XCTestSuite { +fun setupXCTestSuite(tests: Set>? = null, testCases: Map>>? = null): XCTestSuite { val nativeTestSuite = XCTestSuite.testSuiteWithName(TOP_LEVEL_SUITE) // Initialize settings with the given args @@ -52,16 +64,31 @@ fun setupXCTestSuite(): XCTestSuite { ) if (testSettings.runTests) { - // Generate and add tests to the main suite - testSettings.testSuites.generate().forEach { + val includeAllTests = tests == null && testCases == null + val testSuiteNames = tests?.map { it.qualifiedName }.orEmpty() + testCases?.keys.orEmpty() + fun includeTestSuite(testSuite: TestSuite) = + includeAllTests || testSuiteNames.contains(testSuite.name) + // Generate and add tests to the main suite + testSettings.testSuites.generate( + addTestSuite = { testSuite -> + includeTestSuite(testSuite) + }, + addTestCase = { testCase -> + includeTestSuite(testCase.suite) && + (testCases == null || + testCases[testCase.suite.name]?.firstOrNull { it.name == testCase.name } != null) + } + ).forEach { nativeTestSuite.addTest(it) } - // Tests created (self-check) - @Suppress("UNCHECKED_CAST") - check(testSettings.testSuites.size == (nativeTestSuite.tests as List).size) { - "The amount of generated XCTest suites should be equal to Kotlin test suites" + if (includeAllTests) { + // Tests created (self-check) + @Suppress("UNCHECKED_CAST") + check(testSettings.testSuites.size == (nativeTestSuite.tests as List).size) { + "The amount of generated XCTest suites should be equal to Kotlin test suites" + } } } @@ -104,10 +131,13 @@ private fun testArguments(key: String): Array { internal val TestCase.fullName get() = "${suite.name}.$name" -private fun Collection.generate(): List { - return this.map { suite -> +private fun Collection.generate( + addTestSuite: (TestSuite) -> Boolean, + addTestCase: (TestCase) -> Boolean +): List { + return this.filter(addTestSuite).map { suite -> val xcSuite = XCTestSuiteWrapper(suite) - suite.testCases.values.map { testCase -> + suite.testCases.values.filter(addTestCase).map { testCase -> XCTestCaseWrapper(testCase) }.forEach { // add test to its test suite wrapper