Skip to content

Commit c9d237d

Browse files
authored
ADFA-4109: Harden e2e automation and add Gradle offline-mode toggle (#1526)
* ADFA-4109: Harden e2e automation and add Gradle offline-mode toggle * ADFA-4109: Clean up androidTest inspection warnings * ADFA-4109: Apply Spotless formatting to androidTest sources
1 parent eeffc0e commit c9d237d

9 files changed

Lines changed: 1202 additions & 1061 deletions

‎app/src/androidTest/kotlin/com/itsaky/androidide/AutomationEndToEndTest.kt‎

Lines changed: 175 additions & 199 deletions
Large diffs are not rendered by default.

‎app/src/androidTest/kotlin/com/itsaky/androidide/EndToEndTest.kt‎

Lines changed: 140 additions & 167 deletions
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,24 @@ import androidx.test.core.app.ActivityScenario
44
import androidx.test.espresso.matcher.ViewMatchers.isNotEnabled
55
import androidx.test.ext.junit.runners.AndroidJUnit4
66
import androidx.test.platform.app.InstrumentationRegistry
7-
import androidx.test.uiautomator.UiSelector
87
import com.itsaky.androidide.activities.SplashActivity
98
import com.itsaky.androidide.helper.advancePastWelcomeScreen
109
import com.itsaky.androidide.helper.clickFirstAccessibilityNodeByText
10+
import com.itsaky.androidide.helper.configureAutomationBuildPreferences
1111
import com.itsaky.androidide.helper.ensureOnHomeScreenBeforeCreateProject
1212
import com.itsaky.androidide.helper.grantAllRequiredPermissionsThroughOnboardingUi
13+
import com.itsaky.androidide.helper.handlePrivacyDisclosure
1314
import com.itsaky.androidide.helper.initializeProjectAndCancelBuild
1415
import com.itsaky.androidide.helper.selectProjectTemplate
1516
import com.itsaky.androidide.helper.waitForMainHomeOrEditorUi
16-
import com.itsaky.androidide.resources.R as ResourcesR
1717
import com.itsaky.androidide.screens.HomeScreen.clickCreateProjectHomeScreen
1818
import com.itsaky.androidide.screens.OnboardingScreen
19+
import com.itsaky.androidide.screens.PermissionScreen
1920
import com.itsaky.androidide.screens.ProjectSettingsScreen.clickCreateProjectProjectSettings
2021
import com.itsaky.androidide.screens.ProjectSettingsScreen.setProjectName
21-
import com.itsaky.androidide.screens.PermissionScreen
2222
import com.itsaky.androidide.utils.PermissionsHelper
2323
import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
2424
import org.junit.Assert.assertEquals
25-
import org.junit.Assert.assertFalse
2625
import org.junit.Assert.assertTrue
2726
import org.junit.Test
2827
import org.junit.runner.RunWith
@@ -36,167 +35,141 @@ import org.junit.runner.RunWith
3635
*/
3736
@RunWith(AndroidJUnit4::class)
3837
class EndToEndTest : TestCase() {
39-
40-
private val targetContext
41-
get() = InstrumentationRegistry.getInstrumentation().targetContext
42-
43-
private val acceptText: String
44-
get() = targetContext.getString(ResourcesR.string.privacy_disclosure_accept)
45-
46-
private val learnMoreText: String
47-
get() = targetContext.getString(ResourcesR.string.privacy_disclosure_learn_more)
48-
49-
private val dialogTitle: String
50-
get() = targetContext.getString(ResourcesR.string.privacy_disclosure_title)
51-
52-
@Test
53-
fun test_endToEnd() = run {
54-
55-
// ── Launch ──
56-
57-
step("Launch app") {
58-
ActivityScenario.launch(SplashActivity::class.java)
59-
Thread.sleep(1000)
60-
}
61-
62-
// ── Welcome Screen ──
63-
64-
step("Verify welcome screen") {
65-
OnboardingScreen {
66-
greetingTitle.isVisible()
67-
greetingSubtitle.isVisible()
68-
nextButton {
69-
isVisible()
70-
isClickable()
71-
}
72-
}
73-
}
74-
75-
advancePastWelcomeScreen()
76-
77-
// ── Permissions Screen (with privacy disclosure dialog overlay) ──
78-
79-
step("Verify privacy disclosure dialog") {
80-
val d = device.uiDevice
81-
val title = d.findObject(UiSelector().text(dialogTitle))
82-
assertTrue("Dialog title missing", title.waitForExists(2_000))
83-
assertTrue("Accept button missing", d.findObject(UiSelector().text(acceptText)).exists())
84-
assertTrue("Learn more button missing", d.findObject(UiSelector().text(learnMoreText)).exists())
85-
}
86-
87-
step("Accept privacy disclosure") {
88-
clickFirstAccessibilityNodeByText(acceptText)
89-
device.uiDevice.waitForIdle()
90-
}
91-
92-
step("Verify privacy dialog does not reappear") {
93-
assertFalse(
94-
"Dialog should not reappear",
95-
device.uiDevice.findObject(UiSelector().text(dialogTitle)).exists(),
96-
)
97-
}
98-
99-
val required = PermissionsHelper.getRequiredPermissions(targetContext)
100-
101-
step("Verify all permission items") {
102-
flakySafely(timeoutMs = 3_000) {
103-
PermissionScreen {
104-
title { isVisible() }
105-
subTitle { isVisible() }
106-
rvPermissions {
107-
isVisible()
108-
isDisplayed()
109-
}
110-
assertEquals(required.size, rvPermissions.getSize())
111-
112-
rvPermissions {
113-
required.forEachIndexed { index, item ->
114-
childAt<PermissionScreen.PermissionItem>(index) {
115-
title {
116-
isVisible()
117-
hasText(item.title)
118-
}
119-
description {
120-
isVisible()
121-
hasText(item.description)
122-
}
123-
grantButton {
124-
isVisible()
125-
isClickable()
126-
hasText(R.string.title_grant)
127-
}
128-
}
129-
}
130-
}
131-
}
132-
}
133-
}
134-
135-
grantAllRequiredPermissionsThroughOnboardingUi()
136-
137-
step("Confirm all permissions granted") {
138-
flakySafely(timeoutMs = 3_000) {
139-
assertTrue(PermissionsHelper.areAllPermissionsGranted(targetContext))
140-
}
141-
}
142-
143-
step("Confirm all grant buttons disabled") {
144-
device.uiDevice.waitForIdle()
145-
PermissionScreen {
146-
rvPermissions {
147-
required.indices.forEach { index ->
148-
childAt<PermissionScreen.PermissionItem>(index) {
149-
grantButton {
150-
isNotEnabled()
151-
}
152-
}
153-
}
154-
}
155-
}
156-
}
157-
158-
step("Tap Finish installation") {
159-
// The button is in the gesture exclusion zone — use accessibility click
160-
clickFirstAccessibilityNodeByText("Finish installation")
161-
}
162-
163-
step("Wait for IDE setup to complete") {
164-
waitForMainHomeOrEditorUi(
165-
device.uiDevice,
166-
maxWaitMs = 60_000L,
167-
)
168-
}
169-
170-
// ── Phase 2: Project creation + build for first 3 templates ──
171-
172-
ensureOnHomeScreenBeforeCreateProject()
173-
174-
data class TemplateConfig(
175-
val label: String,
176-
val templateResId: Int,
177-
val projectName: String,
178-
)
179-
180-
val templates = listOf(
181-
TemplateConfig("No Activity", R.string.template_no_activity, "TestNoActivity"),
182-
TemplateConfig("Empty Activity", R.string.template_empty, "TestEmptyActivity"),
183-
TemplateConfig("Basic Activity", R.string.template_basic, "TestBasicActivity"),
184-
)
185-
186-
for ((index, config) in templates.withIndex()) {
187-
step("Create+build template ${index + 1}/${templates.size}: ${config.label}") {
188-
clickCreateProjectHomeScreen()
189-
}
190-
selectProjectTemplate("Select ${config.label} template", config.templateResId)
191-
setProjectName(config.projectName)
192-
clickCreateProjectProjectSettings()
193-
initializeProjectAndCancelBuild()
194-
195-
if (index < templates.lastIndex) {
196-
ensureOnHomeScreenBeforeCreateProject()
197-
}
198-
}
199-
200-
// ── Future phases (preferences, more templates, etc.) go here ──
201-
}
38+
private val targetContext
39+
get() = InstrumentationRegistry.getInstrumentation().targetContext
40+
41+
@Test
42+
fun test_endToEnd() =
43+
run {
44+
// ── Launch ──
45+
46+
step("Launch app") {
47+
ActivityScenario.launch(SplashActivity::class.java)
48+
Thread.sleep(1000)
49+
}
50+
51+
// ── Welcome Screen ──
52+
53+
step("Verify welcome screen") {
54+
OnboardingScreen {
55+
greetingTitle.isVisible()
56+
greetingSubtitle.isVisible()
57+
nextButton {
58+
isVisible()
59+
isClickable()
60+
}
61+
}
62+
}
63+
64+
advancePastWelcomeScreen()
65+
66+
// ── Permissions Screen (with privacy disclosure dialog overlay) ──
67+
68+
handlePrivacyDisclosure()
69+
70+
val required = PermissionsHelper.getRequiredPermissions(targetContext)
71+
72+
step("Verify all permission items") {
73+
flakySafely(timeoutMs = 3_000) {
74+
PermissionScreen {
75+
title { isVisible() }
76+
subTitle { isVisible() }
77+
rvPermissions {
78+
isVisible()
79+
isDisplayed()
80+
}
81+
assertEquals(required.size, rvPermissions.getSize())
82+
83+
rvPermissions {
84+
required.forEachIndexed { index, item ->
85+
childAt<PermissionScreen.PermissionItem>(index) {
86+
title {
87+
isVisible()
88+
hasText(item.title)
89+
}
90+
description {
91+
isVisible()
92+
hasText(item.description)
93+
}
94+
grantButton {
95+
isVisible()
96+
if (!item.isGranted && !item.isOptional) {
97+
isClickable()
98+
hasText(R.string.title_grant)
99+
}
100+
}
101+
}
102+
}
103+
}
104+
}
105+
}
106+
}
107+
108+
grantAllRequiredPermissionsThroughOnboardingUi()
109+
110+
step("Confirm all permissions granted") {
111+
flakySafely(timeoutMs = 3_000) {
112+
assertTrue(PermissionsHelper.areAllPermissionsGranted(targetContext))
113+
}
114+
}
115+
116+
step("Confirm all grant buttons disabled") {
117+
device.uiDevice.waitForIdle()
118+
PermissionScreen {
119+
rvPermissions {
120+
required.indices.forEach { index ->
121+
childAt<PermissionScreen.PermissionItem>(index) {
122+
grantButton {
123+
isNotEnabled()
124+
}
125+
}
126+
}
127+
}
128+
}
129+
}
130+
131+
step("Tap Finish installation") {
132+
// The button is in the gesture exclusion zone — use accessibility click
133+
clickFirstAccessibilityNodeByText("Finish installation")
134+
}
135+
136+
step("Wait for IDE setup to complete") {
137+
waitForMainHomeOrEditorUi(device.uiDevice)
138+
}
139+
140+
configureAutomationBuildPreferences()
141+
142+
// ── Phase 2: Project creation + build for first 3 templates ──
143+
144+
ensureOnHomeScreenBeforeCreateProject()
145+
146+
data class TemplateConfig(
147+
val label: String,
148+
val templateResId: Int,
149+
val projectName: String,
150+
)
151+
152+
val templates =
153+
listOf(
154+
TemplateConfig("No Activity", R.string.template_no_activity, "TestNoActivity"),
155+
TemplateConfig("Empty Activity", R.string.template_empty, "TestEmptyActivity"),
156+
TemplateConfig("Basic Activity", R.string.template_basic, "TestBasicActivity"),
157+
)
158+
159+
for ((index, config) in templates.withIndex()) {
160+
step("Create+build template ${index + 1}/${templates.size}: ${config.label}") {
161+
clickCreateProjectHomeScreen()
162+
}
163+
selectProjectTemplate("Select ${config.label} template", config.templateResId)
164+
setProjectName(config.projectName)
165+
clickCreateProjectProjectSettings()
166+
initializeProjectAndCancelBuild()
167+
168+
if (index < templates.lastIndex) {
169+
ensureOnHomeScreenBeforeCreateProject()
170+
}
171+
}
172+
173+
// ── Future phases (preferences, more templates, etc.) go here ──
174+
}
202175
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package com.itsaky.androidide.helper
2+
3+
import android.util.Log
4+
import androidx.test.platform.app.InstrumentationRegistry
5+
import com.itsaky.androidide.preferences.internal.BuildPreferences
6+
import com.kaspersky.kaspresso.testcases.core.testcontext.TestContext
7+
8+
private const val TAG = "AutomationBuildPrefs"
9+
private const val ARG_BUILD_OFFLINE = "androidide.build.offline"
10+
11+
/**
12+
* Optionally override the IDE Gradle offline preference for automation runs.
13+
*
14+
* When the instrumentation argument is omitted, the automation leaves the IDE
15+
* code default/current preference untouched. Pass `androidide.build.offline`
16+
* explicitly to force a deterministic online or offline mode for the run.
17+
*/
18+
fun TestContext<Unit>.configureAutomationBuildPreferences() {
19+
val args = InstrumentationRegistry.getArguments()
20+
val offlineModeArg = args.getString(ARG_BUILD_OFFLINE)
21+
22+
step("Configure automation build preferences") {
23+
if (offlineModeArg == null) {
24+
Log.i(TAG, "Gradle offline mode arg '$ARG_BUILD_OFFLINE' omitted; leaving IDE preference unchanged")
25+
return@step
26+
}
27+
28+
val offlineMode =
29+
checkNotNull(offlineModeArg.toBooleanStrictOrNull()) {
30+
"Instrumentation argument '$ARG_BUILD_OFFLINE' must be 'true' or 'false'"
31+
}
32+
33+
BuildPreferences.isOfflineEnabled = offlineMode
34+
Log.i(TAG, "Gradle offline mode set to $offlineMode (arg '$ARG_BUILD_OFFLINE')")
35+
}
36+
}

0 commit comments

Comments
 (0)