Skip to content

Commit bf36c4e

Browse files
authored
Merge pull request #447 from joreilly/claude/multiplatform-ui-tests-011CUSYuGiuNEE2C6VpwnhTb
Create Cross-Platform Compose UI Tests
2 parents 356f08b + edec1f2 commit bf36c4e

File tree

9 files changed

+308
-42
lines changed

9 files changed

+308
-42
lines changed
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
name: Compose UI Tests
2+
3+
# This workflow runs the Compose Multiplatform UI tests in commonTest
4+
# These tests can run on multiple platforms and use the multiplatform UI testing framework
5+
6+
on:
7+
pull_request:
8+
push:
9+
branches: [ main, master ]
10+
11+
# Cancel any current or previous job from the same PR
12+
concurrency:
13+
group: compose-ui-tests-${{ github.head_ref || github.ref }}
14+
cancel-in-progress: true
15+
16+
jobs:
17+
compose-ui-tests:
18+
name: Compose UI Tests (JVM)
19+
runs-on: ubuntu-24.04
20+
21+
steps:
22+
- name: Checkout
23+
uses: actions/checkout@v5
24+
25+
- name: Set up JDK 17
26+
uses: actions/setup-java@v5
27+
with:
28+
distribution: 'zulu'
29+
java-version: 17
30+
cache: 'gradle'
31+
32+
- name: Setup Gradle
33+
uses: gradle/actions/setup-gradle@v4
34+
with:
35+
cache-read-only: ${{ github.ref != 'refs/heads/main' && github.ref != 'refs/heads/master' }}
36+
37+
- name: Grant execute permission for gradlew
38+
run: chmod +x gradlew
39+
40+
- name: Run Compose UI Tests
41+
run: ./gradlew :common:jvmTest --tests "*Ui*" --no-daemon --stacktrace
42+
43+
- name: Upload Test Results
44+
if: always()
45+
uses: actions/upload-artifact@v4
46+
with:
47+
name: compose-ui-test-results
48+
path: |
49+
common/build/reports/tests/jvmTest/
50+
common/build/test-results/jvmTest/*.xml
51+
retention-days: 7
52+
53+
- name: Publish Test Report
54+
if: always()
55+
uses: mikepenz/action-junit-report@v4
56+
with:
57+
report_paths: 'common/build/test-results/jvmTest/*.xml'
58+
check_name: 'Compose UI Test Results'
59+
detailed_summary: true
60+
include_passed: true
61+
fail_on_failure: true

app/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ dependencies {
7272
implementation(libs.androidx.compose.ui)
7373
implementation(libs.androidx.compose.ui.tooling)
7474
implementation(libs.androidx.navigation.compose)
75+
implementation(libs.androidx.compose.material.icons.extended)
7576
implementation(libs.androidx.compose.material3)
7677
implementation(libs.androidx.compose.material3.adaptive)
7778
implementation(libs.androidx.compose.material3.adaptive.layout)

common/build.gradle.kts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@ kotlin {
7979
implementation(libs.koin.test)
8080
implementation(libs.kotlinx.coroutines.test)
8181
implementation(kotlin("test"))
82+
@OptIn(org.jetbrains.compose.ExperimentalComposeLibrary::class)
83+
implementation(compose.uiTest)
8284
}
8385

8486
androidMain.dependencies {
@@ -96,6 +98,10 @@ kotlin {
9698
implementation(libs.kotlinx.coroutines.swing)
9799
}
98100

101+
jvmTest.dependencies {
102+
implementation(compose.desktop.currentOs)
103+
}
104+
99105
appleMain.dependencies {
100106
implementation(libs.ktor.client.darwin)
101107
implementation(libs.sqldelight.native.driver)
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package dev.johnoreilly.peopleinspace
2+
3+
import dev.johnoreilly.common.remote.Assignment
4+
import dev.johnoreilly.common.remote.IssPosition
5+
import dev.johnoreilly.common.repository.PeopleInSpaceRepositoryInterface
6+
import kotlinx.coroutines.flow.Flow
7+
import kotlinx.coroutines.flow.flowOf
8+
9+
class PeopleInSpaceRepositoryFake: PeopleInSpaceRepositoryInterface {
10+
val peopleList = listOf(
11+
Assignment("ISS", "Chris Cassidy", "https://example.com/cassidy.jpg", "American astronaut", "USA"),
12+
Assignment("ISS", "Anatoly Ivanishin", "https://example.com/ivanishin.jpg", "Russian cosmonaut", "Russia"),
13+
Assignment("ISS", "Ivan Vagner", "https://example.com/vagner.jpg", "Russian cosmonaut", "Russia")
14+
)
15+
16+
val issPosition = IssPosition(53.2743394, -9.0514163)
17+
18+
override fun fetchPeopleAsFlow(): Flow<List<Assignment>> {
19+
return flowOf(peopleList)
20+
}
21+
22+
override fun pollISSPosition(): Flow<IssPosition> {
23+
return flowOf(issPosition)
24+
}
25+
26+
override suspend fun fetchAndStorePeople() {
27+
// No-op for fake
28+
}
29+
}

common/src/commonTest/kotlin/com/surrus/peopleinspace/PeopleInSpaceTest.kt

Lines changed: 0 additions & 41 deletions
This file was deleted.
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package dev.johnoreilly.peopleinspace.ui
2+
3+
import androidx.compose.material3.MaterialTheme
4+
import androidx.compose.ui.test.*
5+
import dev.johnoreilly.common.ui.CoordinateDisplay
6+
import kotlin.test.Test
7+
8+
/**
9+
* Compose Multiplatform UI Tests
10+
*
11+
* These tests use the multiplatform Compose UI testing framework and can run on
12+
* Android, iOS, Desktop, and other platforms supported by Compose Multiplatform.
13+
*
14+
* Key differences from platform-specific tests:
15+
* - Uses `runComposeUiTest` instead of `createComposeRule()`
16+
* - Works with kotlin.test instead of JUnit
17+
* - Can be executed on multiple platforms
18+
*/
19+
@OptIn(ExperimentalTestApi::class)
20+
class ComposeMultiplatformUiTests {
21+
22+
@Test
23+
fun testCoordinateDisplay_showsLabelAndValue() = runComposeUiTest {
24+
// Given
25+
val label = "Latitude"
26+
val value = "53.2743394"
27+
28+
// When
29+
setContent {
30+
MaterialTheme {
31+
CoordinateDisplay(
32+
label = label,
33+
value = value
34+
)
35+
}
36+
}
37+
38+
// Then
39+
onNodeWithText(label).assertIsDisplayed()
40+
onNodeWithText(value).assertIsDisplayed()
41+
}
42+
43+
@Test
44+
fun testCoordinateDisplay_longitudeDisplay() = runComposeUiTest {
45+
// Given
46+
val label = "Longitude"
47+
val value = "-9.0514163"
48+
49+
// When
50+
setContent {
51+
MaterialTheme {
52+
CoordinateDisplay(
53+
label = label,
54+
value = value
55+
)
56+
}
57+
}
58+
59+
// Then
60+
onNodeWithText(label).assertExists()
61+
onNodeWithText(value).assertExists()
62+
}
63+
64+
@Test
65+
fun testCoordinateDisplay_withDifferentValues() = runComposeUiTest {
66+
// Given
67+
val testCases = listOf(
68+
"Latitude" to "0.0",
69+
"Longitude" to "180.0",
70+
"Latitude" to "-90.0"
71+
)
72+
73+
testCases.forEach { (label, value) ->
74+
// When
75+
setContent {
76+
MaterialTheme {
77+
CoordinateDisplay(
78+
label = label,
79+
value = value
80+
)
81+
}
82+
}
83+
84+
// Then
85+
onNodeWithText(label).assertIsDisplayed()
86+
onNodeWithText(value).assertIsDisplayed()
87+
}
88+
}
89+
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
package dev.johnoreilly.peopleinspace.ui
2+
3+
import androidx.compose.ui.test.*
4+
import dev.johnoreilly.common.remote.IssPosition
5+
import dev.johnoreilly.common.ui.CoordinateDisplay
6+
import dev.johnoreilly.peopleinspace.PeopleInSpaceRepositoryFake
7+
import kotlin.test.Test
8+
9+
/**
10+
* UI Tests for ISS Position components
11+
*
12+
* These tests demonstrate testing Compose Multiplatform UI components
13+
* with realistic data from a fake repository.
14+
*/
15+
@OptIn(ExperimentalTestApi::class)
16+
class ISSPositionUiTests {
17+
18+
private val repository = PeopleInSpaceRepositoryFake()
19+
20+
@Test
21+
fun testCoordinateDisplay_withISSPosition_displaysLatitude() = runComposeUiTest {
22+
// Given
23+
val position = repository.issPosition
24+
25+
// When
26+
setContent {
27+
CoordinateDisplay(
28+
label = "Latitude",
29+
value = position.latitude.toString()
30+
)
31+
}
32+
33+
// Then
34+
onNodeWithText("Latitude").assertIsDisplayed()
35+
onNodeWithText(position.latitude.toString()).assertIsDisplayed()
36+
}
37+
38+
@Test
39+
fun testCoordinateDisplay_withISSPosition_displaysLongitude() = runComposeUiTest {
40+
// Given
41+
val position = repository.issPosition
42+
43+
// When
44+
setContent {
45+
CoordinateDisplay(
46+
label = "Longitude",
47+
value = position.longitude.toString()
48+
)
49+
}
50+
51+
// Then
52+
onNodeWithText("Longitude").assertIsDisplayed()
53+
onNodeWithText(position.longitude.toString()).assertIsDisplayed()
54+
}
55+
56+
@Test
57+
fun testCoordinateDisplay_withZeroCoordinates() = runComposeUiTest {
58+
// Given
59+
val position = IssPosition(0.0, 0.0)
60+
61+
// When
62+
setContent {
63+
CoordinateDisplay(
64+
label = "Latitude",
65+
value = position.latitude.toString()
66+
)
67+
}
68+
69+
// Then
70+
onNodeWithText("0.0").assertExists()
71+
}
72+
73+
@Test
74+
fun testCoordinateDisplay_withNegativeCoordinates() = runComposeUiTest {
75+
// Given
76+
val position = IssPosition(-45.5, -120.8)
77+
78+
// When - Display Latitude
79+
setContent {
80+
CoordinateDisplay(
81+
label = "Latitude",
82+
value = position.latitude.toString()
83+
)
84+
}
85+
86+
// Then
87+
onNodeWithText("-45.5").assertIsDisplayed()
88+
}
89+
90+
@Test
91+
fun testCoordinateDisplay_withExtremeCoordinates() = runComposeUiTest {
92+
// Given - Test North Pole
93+
val northPole = IssPosition(90.0, 0.0)
94+
95+
// When
96+
setContent {
97+
CoordinateDisplay(
98+
label = "Latitude",
99+
value = northPole.latitude.toString()
100+
)
101+
}
102+
103+
// Then
104+
onNodeWithText("90.0").assertIsDisplayed()
105+
106+
// Given - Test South Pole
107+
val southPole = IssPosition(-90.0, 0.0)
108+
109+
// When
110+
setContent {
111+
CoordinateDisplay(
112+
label = "Latitude",
113+
value = southPole.latitude.toString()
114+
)
115+
}
116+
117+
// Then
118+
onNodeWithText("-90.0").assertIsDisplayed()
119+
}
120+
}

gradle/libs.versions.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ androidx-compose-runtime = { group = "androidx.compose.runtime", name = "runtime
6767
androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" }
6868
androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
6969
androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" }
70+
androidx-compose-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended" }
7071
androidx-compose-material3-adaptive = { module = "androidx.compose.material3.adaptive:adaptive", version.ref = "material3-adaptive" }
7172
androidx-compose-material3-adaptive-layout = { module = "androidx.compose.material3.adaptive:adaptive-layout", version.ref = "material3-adaptive" }
7273
androidx-compose-material3-adaptive-navigation = { module = "androidx.compose.material3.adaptive:adaptive-navigation", version.ref = "material3-adaptive" }

wearApp/src/main/java/com/surrus/peopleinspace/di/AppModule.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import dev.johnoreilly.peopleinspace.list.PersonListViewModel
88
import dev.johnoreilly.peopleinspace.map.MapViewModel
99
import dev.johnoreilly.peopleinspace.person.PersonDetailsViewModel
1010
import org.koin.android.ext.koin.androidContext
11-
import org.koin.androidx.viewmodel.dsl.viewModel
11+
import org.koin.core.module.dsl.viewModel
1212
import org.koin.dsl.module
1313

1414
val wearAppModule = module {

0 commit comments

Comments
 (0)