Skip to content

Commit 5e5d4da

Browse files
committed
Began adding a UI Smoke Test
1 parent 861a1b5 commit 5e5d4da

File tree

6 files changed

+179
-20
lines changed

6 files changed

+179
-20
lines changed

.github/workflows/build.yml

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,11 @@ jobs:
3434
- name: Run Unit Tests
3535
run: ./gradlew test
3636

37-
# - name: Run Instrumentation Tests
38-
# uses: reactivecircus/android-emulator-runner@v2
39-
# with:
40-
# api-level: 35
41-
# target: default
42-
# arch: x86_64
43-
# profile: Nexus 6
44-
# script: ./gradlew connectedCheck
37+
- name: Run Instrumentation Tests
38+
uses: reactivecircus/android-emulator-runner@v2
39+
with:
40+
api-level: 35
41+
target: default
42+
arch: x86_64
43+
profile: Nexus 6
44+
script: ./gradlew connectedCheck

app/build.gradle.kts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,11 +126,13 @@ dependencies {
126126
testImplementation(libs.kotlinx.coroutines.test)
127127
testImplementation(kotlin("test"))
128128
androidTestImplementation(libs.androidx.junit)
129-
androidTestImplementation(libs.androidx.espresso.core)
129+
androidTestImplementation(libs.androidx.rules)
130130
androidTestImplementation(platform(libs.androidx.compose.bom))
131131
androidTestImplementation(libs.androidx.ui.test.junit4)
132132
androidTestImplementation(libs.mockk.android)
133133
androidTestImplementation(libs.mockk.agent)
134+
androidTestImplementation(libs.ui.test.junit4)
135+
debugImplementation(libs.ui.test.manifest)
134136
debugImplementation(libs.androidx.ui.tooling)
135137
debugImplementation(libs.androidx.ui.test.manifest)
136138
}
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
package com.darkrockstudios.app.securecamera
2+
3+
import android.app.Application
4+
import android.content.res.Resources
5+
import androidx.annotation.StringRes
6+
import androidx.compose.ui.semantics.Role
7+
import androidx.compose.ui.semantics.SemanticsProperties
8+
import androidx.compose.ui.test.SemanticsMatcher
9+
import androidx.compose.ui.test.assertIsDisplayed
10+
import androidx.compose.ui.test.hasContentDescription
11+
import androidx.compose.ui.test.hasSetTextAction
12+
import androidx.compose.ui.test.hasTextExactly
13+
import androidx.compose.ui.test.junit4.createAndroidComposeRule
14+
import androidx.compose.ui.test.onNodeWithText
15+
import androidx.compose.ui.test.performClick
16+
import androidx.compose.ui.test.performTextClearance
17+
import androidx.compose.ui.test.performTextInput
18+
import androidx.test.core.app.ApplicationProvider
19+
import androidx.test.rule.GrantPermissionRule
20+
import kotlinx.coroutines.test.runTest
21+
import org.junit.Rule
22+
import org.junit.Test
23+
import kotlin.time.Duration.Companion.seconds
24+
25+
26+
class SmokeTestUiTest {
27+
28+
@get:Rule
29+
val permissionsRule = GrantPermissionRule.grant(
30+
android.Manifest.permission.POST_NOTIFICATIONS,
31+
android.Manifest.permission.ACCESS_FINE_LOCATION,
32+
android.Manifest.permission.CAMERA
33+
)
34+
35+
@get:Rule
36+
val composeTestRule = createAndroidComposeRule<MainActivity>()
37+
38+
@Test
39+
fun smokeTest() = runTest {
40+
composeTestRule.apply {
41+
onNodeWithText(str(R.string.intro_next)).performClick()
42+
onNodeWithText(str(R.string.intro_slide1_title)).assertIsDisplayed()
43+
44+
onNodeWithText(str(R.string.intro_next)).performClick()
45+
onNodeWithText(str(R.string.intro_slide2_title)).assertIsDisplayed()
46+
47+
onNodeWithText(str(R.string.intro_skip)).performClick()
48+
onNodeWithText(str(R.string.security_intro_supported_security_label)).assertIsDisplayed()
49+
50+
onNodeWithText(str(R.string.intro_next)).performClick()
51+
onNodeWithText(str(R.string.pin_creation_title)).assertIsDisplayed()
52+
53+
onNode(
54+
hasSetTextAction() and hasTextExactly(
55+
str(R.string.pin_creation_hint),
56+
includeEditableText = false
57+
)
58+
).performTextInput("3133734")
59+
60+
onNode(
61+
hasSetTextAction() and hasTextExactly(
62+
str(R.string.pin_creation_confirm_hint),
63+
includeEditableText = false
64+
)
65+
).performTextInput("313373")
66+
67+
onNodeWithText(str(R.string.pin_creation_button)).performClick()
68+
69+
onNodeWithText(str(R.string.pin_creation_error)).assertIsDisplayed()
70+
71+
onNode(
72+
hasSetTextAction() and hasTextExactly(
73+
str(R.string.pin_creation_hint),
74+
includeEditableText = false
75+
)
76+
).apply {
77+
performTextClearance()
78+
performTextInput("123456")
79+
}
80+
81+
onNode(
82+
hasSetTextAction() and hasTextExactly(
83+
str(R.string.pin_creation_confirm_hint),
84+
includeEditableText = false
85+
)
86+
).apply {
87+
performTextClearance()
88+
performTextInput("123456")
89+
}
90+
91+
onNodeWithText(str(R.string.pin_creation_button)).performClick()
92+
93+
onNodeWithText(str(R.string.pin_creation_error_weak_pin)).assertIsDisplayed()
94+
95+
onNode(
96+
hasSetTextAction() and hasTextExactly(
97+
str(R.string.pin_creation_hint),
98+
includeEditableText = false
99+
)
100+
).apply {
101+
performTextClearance()
102+
performTextInput("313373")
103+
}
104+
105+
onNode(
106+
hasSetTextAction() and hasTextExactly(
107+
str(R.string.pin_creation_confirm_hint),
108+
includeEditableText = false
109+
)
110+
).apply {
111+
performTextClearance()
112+
performTextInput("313373")
113+
}
114+
115+
onNodeWithText(str(R.string.pin_creation_button)).performClick()
116+
117+
onNodeWithText(str(R.string.pin_creating_vault)).assertIsDisplayed()
118+
119+
composeTestRule.waitUntil(
120+
timeoutMillis = 10.seconds.inWholeMilliseconds
121+
) {
122+
composeTestRule
123+
.onAllNodes(hasRole(Role.Button) and hasContentDescription(str(R.string.camera_shutter_button_desc)))
124+
.fetchSemanticsNodes().isNotEmpty()
125+
}
126+
127+
onNode(
128+
hasRole(Role.Button) and hasContentDescription(str(R.string.camera_shutter_button_desc))
129+
).assertExists()
130+
}
131+
}
132+
133+
fun hasRole(role: Role): SemanticsMatcher =
134+
SemanticsMatcher.expectValue(SemanticsProperties.Role, role)
135+
136+
private fun str(@StringRes id: Int): String = r.getString(id)
137+
private val r: Resources
138+
get() {
139+
val application = ApplicationProvider.getApplicationContext<Application?>()
140+
return application.resources
141+
}
142+
}

app/src/main/kotlin/com/darkrockstudios/app/securecamera/camera/BottomCameraControls.kt

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,24 +9,33 @@ import androidx.compose.material.icons.Icons
99
import androidx.compose.material.icons.filled.Camera
1010
import androidx.compose.material.icons.filled.PhotoLibrary
1111
import androidx.compose.material.icons.filled.Settings
12-
import androidx.compose.material3.*
12+
import androidx.compose.material3.ButtonDefaults
13+
import androidx.compose.material3.ElevatedButton
14+
import androidx.compose.material3.FilledTonalButton
15+
import androidx.compose.material3.Icon
16+
import androidx.compose.material3.MaterialTheme
1317
import androidx.compose.runtime.Composable
1418
import androidx.compose.ui.Alignment
1519
import androidx.compose.ui.Modifier
1620
import androidx.compose.ui.draw.clip
21+
import androidx.compose.ui.platform.LocalContext
1722
import androidx.compose.ui.res.stringResource
23+
import androidx.compose.ui.semantics.contentDescription
24+
import androidx.compose.ui.semantics.semantics
1825
import androidx.compose.ui.unit.dp
1926
import androidx.navigation.NavHostController
2027
import com.darkrockstudios.app.securecamera.R
2128
import com.darkrockstudios.app.securecamera.navigation.AppDestinations
2229

2330
@Composable
2431
fun BottomCameraControls(
25-
modifier: Modifier = Modifier.Companion,
32+
modifier: Modifier = Modifier,
2633
onCapture: (() -> Unit)?,
2734
isLoading: Boolean,
2835
navController: NavHostController,
2936
) {
37+
val context = LocalContext.current
38+
3039
Box(
3140
modifier = modifier
3241
.fillMaxWidth()
@@ -35,22 +44,25 @@ fun BottomCameraControls(
3544
ElevatedButton(
3645
onClick = { navController.navigate(AppDestinations.SETTINGS_ROUTE) },
3746
enabled = isLoading.not(),
38-
modifier = Modifier.Companion.align(Alignment.Companion.BottomStart),
47+
modifier = Modifier.align(Alignment.BottomStart),
3948
) {
4049
Icon(
4150
imageVector = Icons.Filled.Settings,
4251
contentDescription = stringResource(R.string.camera_settings_button),
43-
modifier = Modifier.Companion.size(32.dp),
52+
modifier = Modifier.size(32.dp),
4453
)
4554
}
4655

4756
if (onCapture != null) {
4857
FilledTonalButton(
4958
onClick = onCapture,
50-
modifier = Modifier.Companion
59+
modifier = Modifier
5160
.size(80.dp)
5261
.clip(CircleShape)
53-
.align(Alignment.Companion.BottomCenter),
62+
.align(Alignment.BottomCenter)
63+
.semantics {
64+
contentDescription = context.getString(R.string.camera_shutter_button_desc)
65+
},
5466
colors = ButtonDefaults.filledTonalButtonColors(
5567
containerColor = MaterialTheme.colorScheme.primary,
5668
),
@@ -59,20 +71,20 @@ fun BottomCameraControls(
5971
imageVector = Icons.Filled.Camera,
6072
contentDescription = stringResource(id = R.string.camera_capture_content_description),
6173
tint = MaterialTheme.colorScheme.onPrimary,
62-
modifier = Modifier.Companion.size(32.dp),
74+
modifier = Modifier.size(32.dp),
6375
)
6476
}
6577
}
6678

6779
ElevatedButton(
6880
onClick = { navController.navigate(AppDestinations.GALLERY_ROUTE) },
6981
enabled = isLoading.not(),
70-
modifier = Modifier.Companion.align(Alignment.Companion.BottomEnd),
82+
modifier = Modifier.align(Alignment.BottomEnd),
7183
) {
7284
Icon(
7385
imageVector = Icons.Filled.PhotoLibrary,
7486
contentDescription = stringResource(id = R.string.camera_gallery_content_description),
75-
modifier = Modifier.Companion.size(32.dp),
87+
modifier = Modifier.size(32.dp),
7688
)
7789
}
7890
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@
117117
<string name="camera_settings_content_description">Settings</string>
118118
<string name="camera_permissions_required">Camera permissions are required.</string>
119119
<string name="camera_open_settings">Open Settings</string>
120+
<string name="camera_shutter_button_desc">Take Photo</string>
120121

121122
<!-- Settings Screen -->
122123
<string name="settings_title">Settings</string>

gradle/libs.versions.toml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ argon2kt = "1.6.0"
33
bcrypt = "0.10.2"
44
faceDetection = "16.1.7"
55
kotlinxCoroutinesTest = "1.10.2"
6+
rules = "1.6.1"
67
versionCode = "15"
78
versionName = "3.1.0"
89
workManager = "2.10.1"
@@ -26,7 +27,6 @@ runtimeLivedata = "1.8.1"
2627
coreKtx = "1.16.0"
2728
junit = "4.13.2"
2829
junitVersion = "1.2.1"
29-
espressoCore = "3.6.1"
3030
activityCompose = "1.10.1"
3131
composeBom = "2025.05.00"
3232
navigationCompose = "2.9.0"
@@ -52,6 +52,7 @@ androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-
5252
androidx-lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycleViewModel" }
5353
androidx-material-icons-extended = { module = "androidx.compose.material:material-icons-extended", version.ref = "materialIconsExtended" }
5454
androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationCompose" }
55+
androidx-rules = { module = "androidx.test:rules", version.ref = "rules" }
5556
androidx-runtime-livedata = { module = "androidx.compose.runtime:runtime-livedata", version.ref = "runtimeLivedata" }
5657
argon2kt = { module = "com.lambdapioneer.argon2kt:argon2kt", version.ref = "argon2kt" }
5758
bcrypt = { module = "at.favre.lib:bcrypt", version.ref = "bcrypt" }
@@ -63,7 +64,6 @@ cryptography-provider-jdk = { module = "dev.whyoleg.cryptography:cryptography-pr
6364
cryptography-core = { module = "dev.whyoleg.cryptography:cryptography-core" }
6465
junit = { group = "junit", name = "junit", version.ref = "junit" }
6566
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
66-
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
6767
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeCompose" }
6868
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
6969
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
@@ -89,6 +89,8 @@ mockk-android = { module = "io.mockk:mockk-android", version.ref = "mockk" }
8989
androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferences" }
9090
androidx-datastore-preferences-core = { module = "androidx.datastore:datastore-preferences-core", version.ref = "datastorePreferences" }
9191
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" }
92+
ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4" }
93+
ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest" }
9294
zoomable = { module = "net.engawapg.lib:zoomable", version.ref = "zoomable" }
9395
androidx-work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version.ref = "workManager" }
9496

0 commit comments

Comments
 (0)