Skip to content

Commit 2f8acf6

Browse files
authored
Create workflow to build Android LeapChat (#26)
Create the E2E test flow for Android LeapChat example app. Changes are 1. A new button in the UI to control whether to use tools in the chat. 2. An E2E test case with 2 turns of chat user interactions. 3. A new workflow to run the test on Google Firebase Test Lab.
1 parent 0fd252f commit 2f8acf6

File tree

6 files changed

+161
-33
lines changed

6 files changed

+161
-33
lines changed
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
name: Android LeapChat Build
2+
on:
3+
push:
4+
branches: [ main ]
5+
paths:
6+
- 'Android/LeapChat/**'
7+
- '.github/workflows/android-leap-chat-test.yml'
8+
pull_request:
9+
branches: [ main ]
10+
paths:
11+
- 'Android/LeapChat/**'
12+
- '.github/workflows/android-leap-chat-test.yml'
13+
workflow_dispatch:
14+
15+
jobs:
16+
build-and-e2e-test:
17+
runs-on: ubuntu-latest
18+
steps:
19+
- uses: actions/checkout@v4
20+
- name: Set up JDK 21
21+
uses: actions/setup-java@v4
22+
with:
23+
java-version: '21'
24+
distribution: 'temurin'
25+
cache: 'gradle'
26+
- name: Build LeapChat
27+
run: cd Android/LeapChat && ./gradlew :app:assemble
28+
- name: Build E2E test
29+
run: cd Android/LeapChat && ./gradlew :app:assembleAndroidTest
30+
- name: Run E2E test on Firebase Test Lab
31+
run: |
32+
echo "$SERVICE_ACCOUNT" > /tmp/service_account.json
33+
gcloud auth activate-service-account --key-file=/tmp/service_account.json
34+
gcloud firebase test android run --type instrumentation \
35+
--app Android/LeapChat/app/build/outputs/apk/debug/app-debug.apk \
36+
--test Android/LeapChat/app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk \
37+
--device model=MediumPhone.arm,version=36,locale=en,orientation=portrait \
38+
--project liquid-leap
39+
env:
40+
SERVICE_ACCOUNT: ${{ secrets.FIREBASE_SERVICE_ACCOUNT }}
41+
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package ai.liquid.leapchat
2+
3+
import androidx.compose.ui.test.ExperimentalTestApi
4+
import androidx.compose.ui.test.assertIsDisplayed
5+
import androidx.compose.ui.test.assertTextContains
6+
import androidx.compose.ui.test.hasTestTag
7+
import androidx.compose.ui.test.hasText
8+
import androidx.compose.ui.test.isDisplayed
9+
import androidx.compose.ui.test.junit4.createAndroidComposeRule
10+
import androidx.compose.ui.test.onAllNodesWithTag
11+
import androidx.compose.ui.test.performClick
12+
import androidx.compose.ui.test.performTextInput
13+
import androidx.test.ext.junit.runners.AndroidJUnit4
14+
import org.junit.Rule
15+
import org.junit.Test
16+
import org.junit.runner.RunWith
17+
18+
@RunWith(AndroidJUnit4::class)
19+
class MainActivityE2EAssetTests {
20+
21+
@get:Rule
22+
val composeTestRule = createAndroidComposeRule<MainActivity>()
23+
24+
25+
@OptIn(ExperimentalTestApi::class)
26+
@Test
27+
fun testEndToEndChat() {
28+
val modelLoadingIndicatorMatcher = hasTestTag("ModelLoadingIndicator")
29+
val inputBoxMatcher = hasTestTag("InputBox")
30+
val sendButtonMatcher = hasText("Send")
31+
32+
// Wait for the model to be downloaded and loaded
33+
composeTestRule.onNode(modelLoadingIndicatorMatcher).assertIsDisplayed()
34+
composeTestRule.waitUntilDoesNotExist(
35+
modelLoadingIndicatorMatcher,
36+
timeoutMillis = MODEL_LOADING_TIMEOUT
37+
)
38+
composeTestRule.waitUntilAtLeastOneExists(sendButtonMatcher, timeoutMillis = 5000L)
39+
40+
// Send an input to the model
41+
composeTestRule.onNode(inputBoxMatcher)
42+
.performTextInput("How many 'r' are there in the word 'strawberry'?")
43+
composeTestRule.onNode(sendButtonMatcher).performClick()
44+
composeTestRule.waitUntilAtLeastOneExists(
45+
hasTestTag("AssistantMessageView"),
46+
timeoutMillis = 5000L
47+
)
48+
composeTestRule.waitUntil(timeoutMillis = 5000L) {
49+
composeTestRule.onNode(hasTestTag("AssistantMessageViewText").and(hasText("strawberry", substring = true)))
50+
.isDisplayed()
51+
}
52+
53+
54+
// Continue the chat with a second prompt
55+
composeTestRule.onNode(inputBoxMatcher).performTextInput("What about letter 'a'?")
56+
composeTestRule.onNode(sendButtonMatcher).performClick()
57+
composeTestRule.waitUntil(timeoutMillis = 5000L) {
58+
composeTestRule.onAllNodesWithTag("AssistantMessageView")
59+
.fetchSemanticsNodes().size == 2
60+
}
61+
}
62+
63+
companion object {
64+
const val MODEL_LOADING_TIMEOUT = 5L * 60L * 1000L
65+
}
66+
}

Android/LeapChat/app/src/main/java/ai/liquid/leapchat/MainActivity.kt

Lines changed: 48 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import ai.liquid.leap.message.ChatMessageContent
1717
import ai.liquid.leap.message.MessageResponse
1818
import ai.liquid.leapchat.models.ChatMessageDisplayItem
1919
import ai.liquid.leapchat.views.ChatHistory
20-
import android.annotation.SuppressLint
2120
import android.os.Bundle
2221
import android.util.Log
2322
import androidx.activity.ComponentActivity
@@ -53,6 +52,7 @@ import androidx.compose.ui.Modifier
5352
import androidx.compose.ui.focus.FocusRequester
5453
import androidx.compose.ui.focus.focusRequester
5554
import androidx.compose.ui.platform.LocalContext
55+
import androidx.compose.ui.platform.testTag
5656
import androidx.compose.ui.unit.dp
5757
import androidx.lifecycle.MutableLiveData
5858
import androidx.lifecycle.lifecycleScope
@@ -93,6 +93,10 @@ class MainActivity : ComponentActivity() {
9393
MutableLiveData<Boolean>(false)
9494
}
9595

96+
private val isToolEnabled: MutableLiveData<Boolean> by lazy {
97+
MutableLiveData<Boolean>(false)
98+
}
99+
96100
private val gson = GsonBuilder().registerLeapAdapters().create()
97101

98102
override fun onCreate(savedInstanceState: Bundle?) {
@@ -149,23 +153,22 @@ class MainActivity : ComponentActivity() {
149153
onValueChange = { userInputFieldText = it },
150154
modifier = Modifier
151155
.padding(4.dp)
152-
.fillMaxWidth(1.0f),
156+
.fillMaxWidth(1.0f).testTag("InputBox"),
153157
enabled = !isInGeneration.value
154158
)
155159
Row(
156160
horizontalArrangement = Arrangement.End,
157161
modifier = Modifier.fillMaxWidth(1.0f)
158162
) {
159-
Button(
160-
onClick = {
161-
this@MainActivity.isInGeneration.value = true
162-
sendText(userInputFieldText)
163-
userInputFieldText = ""
164-
chatHistoryFocusRequester.requestFocus()
165-
},
166-
enabled = !isInGeneration.value
167-
) {
168-
Text(getString(R.string.send_message_button_label))
163+
val isToolEnabledState by isToolEnabled.observeAsState(false)
164+
Button(onClick = {
165+
isToolEnabled.value = !isToolEnabledState
166+
}) {
167+
if (isToolEnabledState) {
168+
Text(getString(R.string.tool_on_button_label))
169+
} else {
170+
Text(getString(R.string.tool_off_button_label))
171+
}
169172
}
170173
Button(
171174
onClick = {
@@ -184,6 +187,17 @@ class MainActivity : ComponentActivity() {
184187
) {
185188
Text(getString(R.string.clean_history_button_label))
186189
}
190+
Button(
191+
onClick = {
192+
this@MainActivity.isInGeneration.value = true
193+
sendText(userInputFieldText)
194+
userInputFieldText = ""
195+
chatHistoryFocusRequester.requestFocus()
196+
},
197+
enabled = !isInGeneration.value
198+
) {
199+
Text(getString(R.string.send_message_button_label))
200+
}
187201
}
188202
}
189203
}
@@ -359,20 +373,21 @@ class MainActivity : ComponentActivity() {
359373
conversationInstance
360374
}
361375

362-
363-
conversation.registerFunction(
364-
LeapFunction(
365-
"compute_sum", "Compute sum of a series of numbers", listOf(
366-
LeapFunctionParameter(
367-
name = "values",
368-
type = LeapFunctionParameterType.Array(
369-
itemType = LeapFunctionParameterType.String()
370-
),
371-
description = "Numbers to compute sum. Values should be represented in string."
376+
if (isToolEnabled.value == true) {
377+
conversation.registerFunction(
378+
LeapFunction(
379+
"compute_sum", "Compute sum of a series of numbers", listOf(
380+
LeapFunctionParameter(
381+
name = "values",
382+
type = LeapFunctionParameterType.Array(
383+
itemType = LeapFunctionParameterType.String()
384+
),
385+
description = "Numbers to compute sum. Values should be represented in string."
386+
)
372387
)
373388
)
374389
)
375-
)
390+
}
376391

377392
return conversation
378393
}
@@ -455,8 +470,8 @@ class MainActivity : ComponentActivity() {
455470
}
456471

457472
companion object {
458-
const val MODEL_SLUG = "lfm2-1.2b"
459-
const val QUANTIZATION_SLUG = "lfm2-1.2b-20250710-8da4w"
473+
const val MODEL_SLUG = "lfm2-350m"
474+
const val QUANTIZATION_SLUG = "lfm2-350m-20250710-8da4w"
460475
}
461476
}
462477

@@ -480,9 +495,13 @@ fun ModelLoadingIndicator(
480495
})
481496
}
482497
}
483-
Box(Modifier
484-
.padding(4.dp)
485-
.fillMaxSize(1.0f), contentAlignment = Alignment.Center) {
498+
Box(
499+
Modifier
500+
.padding(4.dp)
501+
.fillMaxSize(1.0f)
502+
.testTag("ModelLoadingIndicator"),
503+
contentAlignment = Alignment.Center
504+
) {
486505
Text(modelLoadingStatusText, style = MaterialTheme.typography.titleSmall)
487506
}
488-
}
507+
}

Android/LeapChat/app/src/main/java/ai/liquid/leapchat/views/AssistantMessage.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import androidx.compose.material3.Text
1717
import androidx.compose.runtime.Composable
1818
import androidx.compose.ui.Alignment
1919
import androidx.compose.ui.Modifier
20+
import androidx.compose.ui.platform.testTag
2021
import androidx.compose.ui.res.painterResource
2122
import androidx.compose.ui.tooling.preview.Preview
2223
import androidx.compose.ui.unit.dp
@@ -27,7 +28,7 @@ fun AssistantMessage(
2728
reasoningText: String?,
2829
) {
2930
val reasoningText = reasoningText?.trim()
30-
Row(modifier = Modifier.padding(all = 8.dp).fillMaxWidth(1.0f), horizontalArrangement = Arrangement.Absolute.Left) {
31+
Row(modifier = Modifier.padding(all = 8.dp).fillMaxWidth(1.0f).testTag("AssistantMessageView"), horizontalArrangement = Arrangement.Absolute.Left) {
3132
Image(
3233
painter = painterResource(R.drawable.smart_toy_outline),
3334
contentDescription = "Assistant icon",
@@ -43,7 +44,7 @@ fun AssistantMessage(
4344
Text(text = reasoningText, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.secondary)
4445
}
4546
Spacer(modifier = Modifier.height(4.dp))
46-
Text(text = text, style = MaterialTheme.typography.bodyMedium)
47+
Text(text = text, style = MaterialTheme.typography.bodyMedium, modifier = Modifier.testTag("AssistantMessageViewText"))
4748
}
4849
}
4950
}

Android/LeapChat/app/src/main/res/values/strings.xml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,6 @@
66
<string name="send_message_button_label">Send</string>
77
<string name="clean_history_button_label">Clean</string>
88
<string name="stop_generation_button_label">Stop</string>
9-
9+
<string name="tool_on_button_label">Tool ON</string>
10+
<string name="tool_off_button_label">Tool OFF</string>
1011
</resources>

Android/LeapChat/gradle/libs.versions.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ coreKtx = "1.16.0"
55
junit = "4.13.2"
66
junitVersion = "1.2.1"
77
espressoCore = "3.6.1"
8-
leapSdk = "0.4.0"
8+
leapSdk = "0.5.0"
99
lifecycleRuntimeKtx = "2.9.1"
1010
activityCompose = "1.10.1"
1111
composeBom = "2025.06.01"

0 commit comments

Comments
 (0)