Skip to content

Instantly share code, notes, and snippets.

@alexstyl
Created October 27, 2025 02:44
Show Gist options
  • Select an option

  • Save alexstyl/fa9bfcf406ae5015ce2155567c7c0e0a to your computer and use it in GitHub Desktop.

Select an option

Save alexstyl/fa9bfcf406ae5015ce2155567c7c0e0a to your computer and use it in GitHub Desktop.
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