Created
October 27, 2025 02:44
-
-
Save alexstyl/fa9bfcf406ae5015ce2155567c7c0e0a to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package com.composeunstyled | |
| import androidx.compose.ui.test.ComposeUiTest | |
| import androidx.compose.ui.test.ExperimentalTestApi | |
| import androidx.compose.ui.test.runComposeUiTest | |
| import kotlinx.coroutines.async | |
| import kotlinx.coroutines.awaitAll | |
| import kotlinx.coroutines.runBlocking | |
| import kotlin.reflect.KClass | |
| import org.junit.Assume.assumeTrue | |
| internal data class TestResult( | |
| val name: String, | |
| val passed: Boolean, | |
| val error: Throwable? = null, | |
| val ignored: Boolean = false | |
| ) | |
| @OptIn(ExperimentalTestApi::class) | |
| internal fun testCase( | |
| name: String, | |
| expected: KClass<out Throwable>? = null, | |
| ignored: Boolean = false, | |
| assertions: suspend ComposeUiTest.() -> Unit | |
| ): TestResult { | |
| // This function is only used for standalone test cases outside of runTestSuite | |
| // For tests within runTestSuite, the testCase function in TestSuiteScope should be used | |
| if (ignored) { | |
| println("π Ignoring '$name'") | |
| assumeTrue(false) | |
| return TestResult(name, passed = true, ignored = true) | |
| } | |
| val result = runCatching { runComposeUiTest { assertions() } } | |
| return when { | |
| result.isSuccess && expected == null -> { | |
| println("β '$name' passed") | |
| TestResult(name, passed = true) | |
| } | |
| result.isSuccess && expected != null -> { | |
| val error = AssertionError("β '$name' failed.\nExpected ${expected.simpleName} but none thrown.") | |
| println("β '$name' failed.\nExpected ${expected.simpleName} but none thrown.") | |
| TestResult(name, passed = false, error = error) | |
| } | |
| result.isFailure && expected == null -> { | |
| val error = result.exceptionOrNull() ?: AssertionError("Unknown error") | |
| println("β '$name' failed.\nReason: $error") | |
| TestResult(name, passed = false, error = error) | |
| } | |
| result.isFailure && expected != null -> { | |
| val ex = result.exceptionOrNull()!! | |
| if (expected.isInstance(ex)) { | |
| println("β '$name' passed") | |
| TestResult(name, passed = true) | |
| } else { | |
| val error = AssertionError("β '$name' failed.\nExpected ${expected.simpleName}, got ${ex::class.simpleName}") | |
| println("β '$name' failed.\nExpected ${expected.simpleName}, got ${ex::class.simpleName}") | |
| TestResult(name, passed = false, error = error) | |
| } | |
| } | |
| else -> { | |
| val error = AssertionError("β '$name' failed.\nUnexpected test state.") | |
| println("β '$name' failed.\nUnexpected test state.") | |
| TestResult(name, passed = false, error = error) | |
| } | |
| } | |
| } | |
| @OptIn(ExperimentalTestApi::class) | |
| internal fun runTestSuite(block: TestSuiteScope.() -> Unit) { | |
| val scope = TestSuiteScope() | |
| scope.block() | |
| runBlocking { | |
| val results = scope.testCases.map { testCase -> | |
| async { | |
| if (testCase.ignored) { | |
| println("π Ignoring '${testCase.name}'") | |
| return@async TestResult(testCase.name, passed = true, ignored = true) | |
| } | |
| val result = runCatching { runComposeUiTest { testCase.assertions(this) } } | |
| when { | |
| result.isSuccess && testCase.expected == null -> { | |
| TestResult(testCase.name, passed = true) | |
| } | |
| result.isSuccess && testCase.expected != null -> { | |
| val error = AssertionError("Expected ${testCase.expected.simpleName} but none thrown.") | |
| TestResult(testCase.name, passed = false, error = error) | |
| } | |
| result.isFailure && testCase.expected == null -> { | |
| val error = result.exceptionOrNull() ?: AssertionError("Unknown error") | |
| TestResult(testCase.name, passed = false, error = error) | |
| } | |
| result.isFailure && testCase.expected != null -> { | |
| val ex = result.exceptionOrNull()!! | |
| if (testCase.expected.isInstance(ex)) { | |
| TestResult(testCase.name, passed = true) | |
| } else { | |
| val error = AssertionError("Expected ${testCase.expected.simpleName}, got ${ex::class.simpleName}") | |
| TestResult(testCase.name, passed = false, error = error) | |
| } | |
| } | |
| else -> { | |
| val error = AssertionError("Unexpected test state.") | |
| TestResult(testCase.name, passed = false, error = error) | |
| } | |
| } | |
| } | |
| }.awaitAll() | |
| // Report results and throw if any tests failed | |
| val failedTests = results.filter { !it.passed && !it.ignored } | |
| val passedTests = results.filter { it.passed && !it.ignored } | |
| val ignoredTests = results.filter { it.ignored } | |
| println("\nπ Test Summary:") | |
| println("β Passed: ${passedTests.size}") | |
| println("β Failed: ${failedTests.size}") | |
| println("π Ignored: ${ignoredTests.size}") | |
| if (failedTests.isNotEmpty()) { | |
| val errorMessage = buildString { | |
| append("Test suite failed with ${failedTests.size} failure(s):\n") | |
| failedTests.forEach { result -> | |
| append(" β ${result.name}: ${result.error?.message}\n") | |
| } | |
| append("---") | |
| } | |
| throw AssertionError(errorMessage) | |
| } | |
| } | |
| } | |
| @OptIn(ExperimentalTestApi::class) | |
| class TestSuiteScope { | |
| internal val testCases = mutableListOf<TestCase>() | |
| fun testCase( | |
| name: String, | |
| expected: KClass<out Throwable>? = null, | |
| ignored: Boolean = false, | |
| assertions: suspend ComposeUiTest.() -> Unit | |
| ) { | |
| // Just register the test case for later parallel execution | |
| testCases.add(TestCase(name, expected, ignored, assertions)) | |
| } | |
| } | |
| @OptIn(ExperimentalTestApi::class) | |
| internal data class TestCase( | |
| val name: String, | |
| val expected: KClass<out Throwable>? = null, | |
| val ignored: Boolean = false, | |
| val assertions: suspend ComposeUiTest.() -> Unit | |
| ) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment