Skip to content

Commit

Permalink
Run selected instrumented tests (#5214)
Browse files Browse the repository at this point in the history
Add ability to run selected instrumented tests.
Fix iOS version check.
Unmute ignored tests.
  • Loading branch information
ASalavei authored Jan 24, 2025
1 parent b463a44 commit cedd48f
Show file tree
Hide file tree
Showing 8 changed files with 74 additions and 35 deletions.
4 changes: 2 additions & 2 deletions instrumented-test/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"
```
5 changes: 1 addition & 4 deletions instrumented-test/gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -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" }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -97,7 +96,7 @@ class ComponentsAccessibilitySemanticTest {
}

var oldValue = sliderValue
val sliderNode = findNode("Slider")
val sliderNode = findNodeWithTag("Slider")
sliderNode.element?.accessibilityIncrement()
assertTrue(oldValue < sliderValue)

Expand Down Expand Up @@ -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)
}

Expand Down Expand Up @@ -227,7 +226,7 @@ class ComponentsAccessibilitySemanticTest {
}
}

findNode("RadioButton").element?.accessibilityActivate()
findNodeWithTag("RadioButton").element?.accessibilityActivate()
assertAccessibilityTree {
node {
isAccessibilityElement = true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Original file line number Diff line number Diff line change
Expand Up @@ -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 -> {
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -25,14 +29,22 @@ 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 <reified Class>setupXCTestSuite(vararg tests: KFunction1<Class, *>): 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
* with K/N tests.
* 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<KClass<*>>? = null, testCases: Map<String?, Set<KFunction<*>>>? = null): XCTestSuite {
val nativeTestSuite = XCTestSuite.testSuiteWithName(TOP_LEVEL_SUITE)

// Initialize settings with the given args
Expand All @@ -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<XCTest>).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<XCTest>).size) {
"The amount of generated XCTest suites should be equal to Kotlin test suites"
}
}
}

Expand Down Expand Up @@ -104,10 +131,13 @@ private fun testArguments(key: String): Array<String> {

internal val TestCase.fullName get() = "${suite.name}.$name"

private fun Collection<TestSuite>.generate(): List<XCTestSuite> {
return this.map { suite ->
private fun Collection<TestSuite>.generate(
addTestSuite: (TestSuite) -> Boolean,
addTestCase: (TestCase) -> Boolean
): List<XCTestSuite> {
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
Expand Down

0 comments on commit cedd48f

Please sign in to comment.