Skip to content

Commit 718037f

Browse files
Add Compose UI tests for Demo Compose App
1 parent 651f735 commit 718037f

File tree

9 files changed

+226
-69
lines changed

9 files changed

+226
-69
lines changed

.github/workflows/demo-compose-app.yml

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,3 +63,44 @@ jobs:
6363
run: ./gradlew ${{ matrix.gradle-tasks }} --stacktrace
6464
env:
6565
JAVA_OPTS: "-Xmx8g -Dfile.encoding=UTF-8 -Djava.awt.headless=true -Dkotlin.daemon.jvm.options=-Xmx6g"
66+
67+
68+
Tests:
69+
runs-on: ${{ matrix.os }}
70+
strategy:
71+
fail-fast: false
72+
matrix:
73+
gradle-tasks: [
74+
"jvmTest"
75+
# "wasmJsBrowserTest"
76+
]
77+
os: [ ubuntu-latest ]
78+
include:
79+
- gradle-tasks: "iosSimulatorArm64Test"
80+
os: macos-latest-xlarge
81+
steps:
82+
- name: Configure Git
83+
run: |
84+
git config --global core.autocrlf input
85+
- uses: actions/checkout@v5
86+
- name: Set up JDK ${{ env.JAVA_VERSION }}
87+
uses: actions/setup-java@v5
88+
with:
89+
java-version: ${{ env.JAVA_VERSION }}
90+
distribution: ${{ env.JAVA_DISTRIBUTION }}
91+
cache: gradle
92+
93+
# Configure Gradle for optimal use in GitHub Actions, including caching of downloaded dependencies.
94+
# See: https://github.com/gradle/actions/blob/main/setup-gradle/README.md
95+
- name: Setup Gradle
96+
uses: gradle/actions/setup-gradle@v4
97+
with:
98+
# Cache downloaded JDKs/konan in addition to the default directories.
99+
gradle-home-cache-includes: |
100+
caches
101+
notifications
102+
jdks
103+
~/.konan/**
104+
- name: Run UI tests
105+
working-directory: ./examples/demo-compose-app
106+
run: ./gradlew ${{ matrix.gradle-tasks }} --stacktrace

examples/demo-compose-app/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,13 @@ To build the application bundle:
3232
### Desktop
3333
Run the desktop application: `./gradlew :desktopApp:run`
3434
Run the desktop **hot reload** application: `./gradlew :desktopApp:hotRun`
35+
Run desktop UI tests: `./gradlew jvmTest`
3536

3637
### iOS
3738
To run the application on iPhone device/simulator:
3839
- Open `iosApp/iosApp.xcproject` in Xcode and run standard configuration
3940
- Or use [Kotlin Multiplatform Mobile plugin](https://plugins.jetbrains.com/plugin/14936-kotlin-multiplatform-mobile) for Android Studio
41+
- Run iOS simulator UI tests: `./gradlew iosSimulatorArm64Test`
4042

4143
### Web Distribution
4244
Build web distribution: `./gradlew :webApp:composeCompatibilityBrowserDistribution`
@@ -47,3 +49,4 @@ Run the browser application: `./gradlew :webApp:jsBrowserDevelopmentRun --contin
4749

4850
### Wasm Browser
4951
Run the browser application: `./gradlew :webApp:wasmJsBrowserDevelopmentRun --continue`
52+
Run browser UI tests: `./gradlew clean wasmJsBrowserTest`

examples/demo-compose-app/commonApp/build.gradle.kts

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
1-
@file:OptIn(ExperimentalWasmDsl::class)
2-
31
import org.jetbrains.compose.reload.gradle.ComposeHotRun
4-
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
52
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
63

74
plugins {
@@ -15,11 +12,10 @@ plugins {
1512
kotlin {
1613
jvmToolchain(libs.versions.javaVersion.get().toInt())
1714

18-
android {
15+
androidLibrary {
1916
namespace = "com.jetbrains.example.koog.share.ui"
2017
compileSdk = 36
2118
minSdk = 23
22-
androidResources.enable = true
2319
}
2420

2521
jvm()
@@ -32,18 +28,18 @@ kotlin {
3228

3329
sourceSets {
3430
commonMain.dependencies {
35-
implementation(compose.animation)
36-
implementation(compose.animationGraphics)
37-
implementation(compose.components.resources)
38-
implementation(compose.components.uiToolingPreview)
39-
implementation(compose.foundation)
40-
implementation(compose.material3)
41-
implementation(compose.materialIconsExtended)
42-
implementation(compose.runtime)
43-
implementation(compose.ui)
44-
implementation(compose.uiUtil)
45-
implementation(libs.jetbrains.lifecycle.viewmodel.compose)
46-
implementation(libs.jetbrains.navigation.compose)
31+
implementation(libs.jetbrains.compose.animation)
32+
implementation(libs.jetbrains.compose.animation.graphics)
33+
implementation(libs.jetbrains.compose.components.resources)
34+
implementation(libs.jetbrains.compose.foundation)
35+
implementation(libs.jetbrains.compose.material.icons.extended)
36+
implementation(libs.jetbrains.compose.material3)
37+
implementation(libs.jetbrains.compose.runtime)
38+
implementation(libs.jetbrains.compose.ui)
39+
implementation(libs.jetbrains.compose.ui.tooling.preview)
40+
implementation(libs.jetbrains.compose.ui.util)
41+
implementation(libs.jetbrains.lifecycle.viewmodel.navigation3)
42+
implementation(libs.jetbrains.navigation3.ui)
4743
implementation(libs.koin.compose)
4844
implementation(libs.koog.agents.core)
4945
implementation(libs.koog.prompt.executor.llms.all)
@@ -55,9 +51,14 @@ kotlin {
5551
implementation(project.dependencies.platform(libs.ktor.bom))
5652
}
5753

54+
commonTest.dependencies {
55+
implementation(kotlin("test"))
56+
implementation(libs.jetbrains.compose.ui.test)
57+
}
58+
5859
androidMain.dependencies {
59-
implementation(compose.uiTooling)
6060
implementation(libs.androidx.datastore.preferences)
61+
implementation(libs.jetbrains.compose.ui.tooling)
6162
implementation(libs.kotlinx.coroutines.android)
6263
implementation(libs.ktor.client.okhttp)
6364
}

examples/demo-compose-app/commonApp/src/commonMain/kotlin/com/jetbrains/example/koog/compose/ComposeApp.kt

Lines changed: 37 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@ import androidx.compose.foundation.layout.fillMaxSize
44
import androidx.compose.material3.MaterialTheme
55
import androidx.compose.material3.Surface
66
import androidx.compose.runtime.Composable
7+
import androidx.compose.runtime.mutableStateListOf
78
import androidx.compose.ui.Modifier
8-
import androidx.navigation.compose.NavHost
9-
import androidx.navigation.compose.composable
10-
import androidx.navigation.compose.rememberNavController
9+
import androidx.navigation3.runtime.NavKey
10+
import androidx.navigation3.runtime.entryProvider
11+
import androidx.navigation3.ui.NavDisplay
1112
import com.jetbrains.example.koog.compose.screens.agentdemo.AgentDemoScreen
1213
import com.jetbrains.example.koog.compose.screens.settings.SettingsScreen
1314
import com.jetbrains.example.koog.compose.screens.start.StartScreen
@@ -27,57 +28,50 @@ fun ComposeApp() = AppTheme {
2728
color = MaterialTheme.colorScheme.background
2829
) {
2930
val koin = getKoin()
30-
val navController = rememberNavController()
31-
NavHost(
32-
navController = navController,
33-
startDestination = NavRoute.StartScreen,
34-
) {
35-
composable<NavRoute.StartScreen> {
36-
StartScreen(
37-
onNavigateToSettings = {
38-
navController.navigate(NavRoute.SettingsScreen)
39-
},
40-
onNavigateToAgentDemo = { demoRoute ->
41-
navController.navigate(demoRoute)
42-
},
43-
viewModel = koin.get()
44-
)
45-
}
31+
val backStack = mutableStateListOf<NavKey>(NavRoute.StartScreen)
32+
NavDisplay(
33+
backStack = backStack,
34+
onBack = { backStack.removeLastOrNull() },
35+
entryProvider = entryProvider {
36+
entry<NavRoute.StartScreen> {
37+
StartScreen(
38+
onNavigateToSettings = { backStack.add(NavRoute.SettingsScreen) },
39+
onNavigateToAgentDemo = { demoRoute -> backStack.add(demoRoute) },
40+
viewModel = koin.get()
41+
)
42+
}
4643

47-
composable<NavRoute.SettingsScreen> {
48-
SettingsScreen(
49-
onNavigateBack = {
50-
navController.popBackStack()
51-
},
52-
onSaveSettings = {
53-
navController.popBackStack()
54-
},
55-
viewModel = koin.get()
56-
)
57-
}
44+
entry<NavRoute.SettingsScreen> {
45+
SettingsScreen(
46+
onNavigateBack = { backStack.removeLastOrNull() },
47+
onSaveSettings = { backStack.removeLastOrNull() },
48+
viewModel = koin.get()
49+
)
50+
}
5851

59-
composable<NavRoute.AgentDemoRoute.CalculatorScreen> {
60-
AgentDemoScreen(
61-
onNavigateBack = { navController.popBackStack() },
62-
viewModel = koin.get { parametersOf("calculator") }
63-
)
64-
}
52+
entry<NavRoute.AgentDemoRoute.CalculatorScreen> {
53+
AgentDemoScreen(
54+
onNavigateBack = { backStack.removeLastOrNull() },
55+
viewModel = koin.get { parametersOf("calculator") }
56+
)
57+
}
6558

66-
composable<NavRoute.AgentDemoRoute.WeatherScreen> {
67-
AgentDemoScreen(
68-
onNavigateBack = { navController.popBackStack() },
69-
viewModel = koin.get { parametersOf("weather") }
70-
)
59+
entry<NavRoute.AgentDemoRoute.WeatherScreen> {
60+
AgentDemoScreen(
61+
onNavigateBack = { backStack.removeLastOrNull() },
62+
viewModel = koin.get { parametersOf("weather") }
63+
)
64+
}
7165
}
72-
}
66+
)
7367
}
7468
}
7569

7670
/**
7771
* Navigation routes for the app
7872
*/
7973
@Serializable
80-
sealed interface NavRoute {
74+
sealed interface NavRoute : NavKey {
8175
@Serializable
8276
data object StartScreen : NavRoute
8377

examples/demo-compose-app/commonApp/src/commonMain/kotlin/com/jetbrains/example/koog/compose/screens/agentdemo/AgentDemoScreen.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,10 @@ import androidx.compose.ui.focus.FocusRequester
4848
import androidx.compose.ui.focus.focusRequester
4949
import androidx.compose.ui.platform.LocalFocusManager
5050
import androidx.compose.ui.text.input.ImeAction
51+
import androidx.compose.ui.tooling.preview.Preview
5152
import androidx.compose.ui.unit.dp
5253
import com.jetbrains.example.koog.compose.theme.AppDimension
5354
import com.jetbrains.example.koog.compose.theme.AppTheme
54-
import org.jetbrains.compose.ui.tooling.preview.Preview
5555

5656
@Composable
5757
fun AgentDemoScreen(

examples/demo-compose-app/commonApp/src/commonMain/kotlin/com/jetbrains/example/koog/compose/screens/settings/SettingsScreen.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,9 @@ import androidx.compose.runtime.Composable
2323
import androidx.compose.runtime.collectAsState
2424
import androidx.compose.runtime.getValue
2525
import androidx.compose.ui.Modifier
26+
import androidx.compose.ui.tooling.preview.Preview
2627
import com.jetbrains.example.koog.compose.theme.AppDimension
2728
import com.jetbrains.example.koog.compose.theme.AppTheme
28-
import org.jetbrains.compose.ui.tooling.preview.Preview
2929

3030
@Composable
3131
fun SettingsScreen(

examples/demo-compose-app/commonApp/src/commonMain/kotlin/com/jetbrains/example/koog/compose/screens/start/StartScreen.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,10 @@ import androidx.compose.ui.Alignment
2525
import androidx.compose.ui.Modifier
2626
import androidx.compose.ui.draw.clip
2727
import androidx.compose.ui.text.style.TextAlign
28+
import androidx.compose.ui.tooling.preview.Preview
2829
import com.jetbrains.example.koog.compose.NavRoute
2930
import com.jetbrains.example.koog.compose.theme.AppDimension
3031
import com.jetbrains.example.koog.compose.theme.AppTheme
31-
import org.jetbrains.compose.ui.tooling.preview.Preview
3232

3333
@Composable
3434
fun StartScreen(
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
package com.jetbrains.example.koog.compose
2+
3+
import androidx.compose.ui.test.ExperimentalTestApi
4+
import androidx.compose.ui.test.onNodeWithContentDescription
5+
import androidx.compose.ui.test.onNodeWithText
6+
import androidx.compose.ui.test.onRoot
7+
import androidx.compose.ui.test.performClick
8+
import androidx.compose.ui.test.performTextInput
9+
import androidx.compose.ui.test.printToLog
10+
import androidx.compose.ui.test.runComposeUiTest
11+
import kotlin.test.Test
12+
13+
@OptIn(ExperimentalTestApi::class)
14+
class KoinAppTest {
15+
16+
@Test
17+
fun simpleCheck() = runComposeUiTest {
18+
setContent {
19+
KoinApp()
20+
}
21+
22+
onRoot().printToLog("0_StartScreenTag")
23+
24+
// Open Settings screen
25+
onNodeWithContentDescription("Settings").performClick()
26+
27+
// Enter empty value to OpenAI token field
28+
onNodeWithText("OpenAI Token").performTextInput("")
29+
30+
// Enter empty value to Anthropic token field
31+
onNodeWithText("Anthropic Token").performTextInput("")
32+
33+
// Press save settings button
34+
onNodeWithContentDescription("Save").performClick()
35+
36+
// Open Calculator screen
37+
onNodeWithText("Calculator").performClick()
38+
39+
// Type a test message
40+
onNodeWithText("Type a message...").performTextInput("What is 2+2?")
41+
42+
// Press send button
43+
onNodeWithContentDescription("Send").performClick()
44+
45+
onRoot().printToLog("1_CalculatorScreenEmptyTokenTag")
46+
47+
// Press back button
48+
onNodeWithContentDescription("Back").performClick()
49+
50+
// Open Weather Forecast screen
51+
onNodeWithText("Weather Forecast").performClick()
52+
53+
// Type a test message
54+
onNodeWithText("Type a message...").performTextInput("What's the weather in New York?")
55+
56+
// Press send button
57+
onNodeWithContentDescription("Send").performClick()
58+
59+
onRoot().printToLog("2_WeatherForecastScreenEmptyTokenTag")
60+
61+
// Press back button
62+
onNodeWithContentDescription("Back").performClick()
63+
64+
// Open Settings screen
65+
onNodeWithContentDescription("Settings").performClick()
66+
67+
// Enter "IncorrectApiKey" value to OpenAI token field
68+
onNodeWithText("OpenAI Token").performTextInput("IncorrectApiKey")
69+
70+
// Enter empty value to Anthropic token field
71+
onNodeWithText("Anthropic Token").performTextInput("")
72+
73+
// Press save settings button
74+
onNodeWithContentDescription("Save").performClick()
75+
76+
// Open Calculator screen
77+
onNodeWithText("Calculator").performClick()
78+
79+
// Type a test message
80+
onNodeWithText("Type a message...").performTextInput("What is 2+2?")
81+
82+
// Press send button
83+
onNodeWithContentDescription("Send").performClick()
84+
85+
onRoot().printToLog("3_CalculatorScreenIncorrectKeyTag")
86+
87+
// Press back button
88+
onNodeWithContentDescription("Back").performClick()
89+
90+
// Open Weather Forecast screen
91+
onNodeWithText("Weather Forecast").performClick()
92+
93+
// Type a test message
94+
onNodeWithText("Type a message...").performTextInput("What's the weather in New York?")
95+
96+
// Press send button
97+
onNodeWithContentDescription("Send").performClick()
98+
99+
onRoot().printToLog("4_WeatherForecastScreenIncorrectKeyTag")
100+
101+
// Press back button
102+
onNodeWithContentDescription("Back").performClick()
103+
}
104+
}

0 commit comments

Comments
 (0)