Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions .github/workflows/compose-ui-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
name: Compose UI Tests

# This workflow runs the Compose Multiplatform UI tests in commonTest
# These tests can run on multiple platforms and use the multiplatform UI testing framework

on:
pull_request:
push:
branches: [ main, master ]

# Cancel any current or previous job from the same PR
concurrency:
group: compose-ui-tests-${{ github.head_ref || github.ref }}
cancel-in-progress: true

jobs:
compose-ui-tests:
name: Compose UI Tests (JVM)
runs-on: ubuntu-24.04

steps:
- name: Checkout
uses: actions/checkout@v5

- name: Set up JDK 17
uses: actions/setup-java@v5
with:
distribution: 'zulu'
java-version: 17
cache: 'gradle'

- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
with:
cache-read-only: ${{ github.ref != 'refs/heads/main' && github.ref != 'refs/heads/master' }}

- name: Grant execute permission for gradlew
run: chmod +x gradlew

- name: Run Compose UI Tests
run: ./gradlew :common:jvmTest --tests "*Ui*" --no-daemon --stacktrace

- name: Upload Test Results
if: always()
uses: actions/upload-artifact@v4
with:
name: compose-ui-test-results
path: |
common/build/reports/tests/jvmTest/
common/build/test-results/jvmTest/*.xml
retention-days: 7

- name: Publish Test Report
if: always()
uses: mikepenz/action-junit-report@v4
with:
report_paths: 'common/build/test-results/jvmTest/*.xml'
check_name: 'Compose UI Test Results'
detailed_summary: true
include_passed: true
fail_on_failure: true
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ dependencies {
implementation(libs.androidx.compose.ui)
implementation(libs.androidx.compose.ui.tooling)
implementation(libs.androidx.navigation.compose)
implementation(libs.androidx.compose.material.icons.extended)
implementation(libs.androidx.compose.material3)
implementation(libs.androidx.compose.material3.adaptive)
implementation(libs.androidx.compose.material3.adaptive.layout)
Expand Down
6 changes: 6 additions & 0 deletions common/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ kotlin {
implementation(libs.koin.test)
implementation(libs.kotlinx.coroutines.test)
implementation(kotlin("test"))
@OptIn(org.jetbrains.compose.ExperimentalComposeLibrary::class)
implementation(compose.uiTest)
}

androidMain.dependencies {
Expand All @@ -96,6 +98,10 @@ kotlin {
implementation(libs.kotlinx.coroutines.swing)
}

jvmTest.dependencies {
implementation(compose.desktop.currentOs)
}

appleMain.dependencies {
implementation(libs.ktor.client.darwin)
implementation(libs.sqldelight.native.driver)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package dev.johnoreilly.peopleinspace

import dev.johnoreilly.common.remote.Assignment
import dev.johnoreilly.common.remote.IssPosition
import dev.johnoreilly.common.repository.PeopleInSpaceRepositoryInterface
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf

class PeopleInSpaceRepositoryFake: PeopleInSpaceRepositoryInterface {
val peopleList = listOf(
Assignment("ISS", "Chris Cassidy", "https://example.com/cassidy.jpg", "American astronaut", "USA"),
Assignment("ISS", "Anatoly Ivanishin", "https://example.com/ivanishin.jpg", "Russian cosmonaut", "Russia"),
Assignment("ISS", "Ivan Vagner", "https://example.com/vagner.jpg", "Russian cosmonaut", "Russia")
)

val issPosition = IssPosition(53.2743394, -9.0514163)

override fun fetchPeopleAsFlow(): Flow<List<Assignment>> {
return flowOf(peopleList)
}

override fun pollISSPosition(): Flow<IssPosition> {
return flowOf(issPosition)
}

override suspend fun fetchAndStorePeople() {
// No-op for fake
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package dev.johnoreilly.peopleinspace.ui

import androidx.compose.material3.MaterialTheme
import androidx.compose.ui.test.*
import dev.johnoreilly.common.ui.CoordinateDisplay
import kotlin.test.Test

/**
* Compose Multiplatform UI Tests
*
* These tests use the multiplatform Compose UI testing framework and can run on
* Android, iOS, Desktop, and other platforms supported by Compose Multiplatform.
*
* Key differences from platform-specific tests:
* - Uses `runComposeUiTest` instead of `createComposeRule()`
* - Works with kotlin.test instead of JUnit
* - Can be executed on multiple platforms
*/
@OptIn(ExperimentalTestApi::class)
class ComposeMultiplatformUiTests {

@Test
fun testCoordinateDisplay_showsLabelAndValue() = runComposeUiTest {
// Given
val label = "Latitude"
val value = "53.2743394"

// When
setContent {
MaterialTheme {
CoordinateDisplay(
label = label,
value = value
)
}
}

// Then
onNodeWithText(label).assertIsDisplayed()
onNodeWithText(value).assertIsDisplayed()
}

@Test
fun testCoordinateDisplay_longitudeDisplay() = runComposeUiTest {
// Given
val label = "Longitude"
val value = "-9.0514163"

// When
setContent {
MaterialTheme {
CoordinateDisplay(
label = label,
value = value
)
}
}

// Then
onNodeWithText(label).assertExists()
onNodeWithText(value).assertExists()
}

@Test
fun testCoordinateDisplay_withDifferentValues() = runComposeUiTest {
// Given
val testCases = listOf(
"Latitude" to "0.0",
"Longitude" to "180.0",
"Latitude" to "-90.0"
)

testCases.forEach { (label, value) ->
// When
setContent {
MaterialTheme {
CoordinateDisplay(
label = label,
value = value
)
}
}

// Then
onNodeWithText(label).assertIsDisplayed()
onNodeWithText(value).assertIsDisplayed()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package dev.johnoreilly.peopleinspace.ui

import androidx.compose.ui.test.*
import dev.johnoreilly.common.remote.IssPosition
import dev.johnoreilly.common.ui.CoordinateDisplay
import dev.johnoreilly.peopleinspace.PeopleInSpaceRepositoryFake
import kotlin.test.Test

/**
* UI Tests for ISS Position components
*
* These tests demonstrate testing Compose Multiplatform UI components
* with realistic data from a fake repository.
*/
@OptIn(ExperimentalTestApi::class)
class ISSPositionUiTests {

private val repository = PeopleInSpaceRepositoryFake()

@Test
fun testCoordinateDisplay_withISSPosition_displaysLatitude() = runComposeUiTest {
// Given
val position = repository.issPosition

// When
setContent {
CoordinateDisplay(
label = "Latitude",
value = position.latitude.toString()
)
}

// Then
onNodeWithText("Latitude").assertIsDisplayed()
onNodeWithText(position.latitude.toString()).assertIsDisplayed()
}

@Test
fun testCoordinateDisplay_withISSPosition_displaysLongitude() = runComposeUiTest {
// Given
val position = repository.issPosition

// When
setContent {
CoordinateDisplay(
label = "Longitude",
value = position.longitude.toString()
)
}

// Then
onNodeWithText("Longitude").assertIsDisplayed()
onNodeWithText(position.longitude.toString()).assertIsDisplayed()
}

@Test
fun testCoordinateDisplay_withZeroCoordinates() = runComposeUiTest {
// Given
val position = IssPosition(0.0, 0.0)

// When
setContent {
CoordinateDisplay(
label = "Latitude",
value = position.latitude.toString()
)
}

// Then
onNodeWithText("0.0").assertExists()
}

@Test
fun testCoordinateDisplay_withNegativeCoordinates() = runComposeUiTest {
// Given
val position = IssPosition(-45.5, -120.8)

// When - Display Latitude
setContent {
CoordinateDisplay(
label = "Latitude",
value = position.latitude.toString()
)
}

// Then
onNodeWithText("-45.5").assertIsDisplayed()
}

@Test
fun testCoordinateDisplay_withExtremeCoordinates() = runComposeUiTest {
// Given - Test North Pole
val northPole = IssPosition(90.0, 0.0)

// When
setContent {
CoordinateDisplay(
label = "Latitude",
value = northPole.latitude.toString()
)
}

// Then
onNodeWithText("90.0").assertIsDisplayed()

// Given - Test South Pole
val southPole = IssPosition(-90.0, 0.0)

// When
setContent {
CoordinateDisplay(
label = "Latitude",
value = southPole.latitude.toString()
)
}

// Then
onNodeWithText("-90.0").assertIsDisplayed()
}
}
1 change: 1 addition & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ androidx-compose-runtime = { group = "androidx.compose.runtime", name = "runtime
androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" }
androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" }
androidx-compose-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended" }
androidx-compose-material3-adaptive = { module = "androidx.compose.material3.adaptive:adaptive", version.ref = "material3-adaptive" }
androidx-compose-material3-adaptive-layout = { module = "androidx.compose.material3.adaptive:adaptive-layout", version.ref = "material3-adaptive" }
androidx-compose-material3-adaptive-navigation = { module = "androidx.compose.material3.adaptive:adaptive-navigation", version.ref = "material3-adaptive" }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import dev.johnoreilly.peopleinspace.list.PersonListViewModel
import dev.johnoreilly.peopleinspace.map.MapViewModel
import dev.johnoreilly.peopleinspace.person.PersonDetailsViewModel
import org.koin.android.ext.koin.androidContext
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.core.module.dsl.viewModel
import org.koin.dsl.module

val wearAppModule = module {
Expand Down