Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -1460,6 +1460,197 @@ class PinPasswordActivityTest {
}
}

@Test
fun testPinPassword_withAdmin_forgetPin_showsConfirmDataResetDialog() {
setUpTestApplicationComponent()
launch<PinPasswordActivity>(
PinPasswordActivity.createPinPasswordActivityIntent(
context = context,
adminPin = adminPin,
profileId = adminId
)
).use {
testCoroutineDispatchers.runCurrent()
onView(withId(R.id.pin_password_input_pin_edit_text))
.perform(appendText(""), closeSoftKeyboard())
// Click "Forgot PIN?" to open the first dialog.
onView(withId(R.id.forgot_pin)).perform(click())
// Click the positive button ("Reset <AppName> Data") on the first dialog to open the
// confirmation dialog.
onView(withText(containsString("Reset")))
.inRoot(isDialog())
.perform(click())
// Verify the confirmation dialog is displayed.
onView(
withText(
containsString(
context.resources.getString(R.string.admin_confirm_app_wipe_positive_button_text)
)
)
).inRoot(isDialog()).check(matches(isDisplayed()))
}
}

@Test
fun testPinPassword_withAdmin_forgetPin_confirmDataReset_deletesAllProfiles() {
setUpTestApplicationComponent()
val scenario = launch<PinPasswordActivity>(
PinPasswordActivity.createPinPasswordActivityIntent(
context = context,
adminPin = adminPin,
profileId = adminId
)
)
testCoroutineDispatchers.runCurrent()
onView(withId(R.id.pin_password_input_pin_edit_text))
.perform(appendText(""), closeSoftKeyboard())
onView(withId(R.id.forgot_pin)).perform(click())
onView(withText(containsString("Reset")))
.inRoot(isDialog())
.perform(click())
onView(withText(context.getString(R.string.admin_confirm_app_wipe_positive_button_text)))
.inRoot(isDialog())
.perform(click())
testCoroutineDispatchers.runCurrent()
scenario.onActivity { activity ->
assertThat(activity.isFinishing).isTrue()
}
// The activity's onDestroy calls exitProcess(0) when confirmedDeletion is true, which the test
// framework intercepts with a SecurityException. Catch it since it confirms the expected
// behavior.
try {
scenario.close()
} catch (e: SecurityException) {
// Expected: exitProcess(0) is called during onDestroy after confirming deletion.
}
}

@Test
fun testPinPassword_withAdmin_forgetPin_confirmDataReset_routesToSplashActivity() {
setUpTestApplicationComponent()
val scenario = launch<PinPasswordActivity>(
PinPasswordActivity.createPinPasswordActivityIntent(
context = context,
adminPin = adminPin,
profileId = adminId
)
)
testCoroutineDispatchers.runCurrent()
onView(withId(R.id.pin_password_input_pin_edit_text))
.perform(appendText(""), closeSoftKeyboard())
onView(withId(R.id.forgot_pin)).perform(click())
onView(withText(containsString("Reset")))
.inRoot(isDialog())
.perform(click())
onView(withText(context.getString(R.string.admin_confirm_app_wipe_positive_button_text)))
.inRoot(isDialog())
.perform(click())
testCoroutineDispatchers.runCurrent()
// After deletion, finishAffinity() is called. The activity should be finishing which will
// eventually lead to the app restarting via the splash activity on next launch.
scenario.onActivity { activity ->
assertThat(activity.isFinishing).isTrue()
}
try {
scenario.close()
} catch (e: SecurityException) {
// Expected: exitProcess(0) is called during onDestroy after confirming deletion.
}
}

@Test
fun testPinPassword_withAdmin_forgetPin_cancelDataReset_dismissesDialog_profilesRemain() {
setUpTestApplicationComponent()
launch<PinPasswordActivity>(
PinPasswordActivity.createPinPasswordActivityIntent(
context = context,
adminPin = adminPin,
profileId = adminId
)
).use { scenario ->
testCoroutineDispatchers.runCurrent()
onView(withId(R.id.pin_password_input_pin_edit_text))
.perform(appendText(""), closeSoftKeyboard())
onView(withId(R.id.forgot_pin)).perform(click())
onView(withText(containsString("Reset")))
.inRoot(isDialog())
.perform(click())
onView(withText(context.getString(R.string.admin_confirm_app_wipe_negative_button_text)))
.inRoot(isDialog())
.perform(click())
testCoroutineDispatchers.runCurrent()
scenario.onActivity { activity ->
assertThat(activity.isFinishing).isFalse()
}
}
}

@Test
fun testPinPassword_withAdmin_forgetPin_afterReset_profileChooserIsEmpty() {
setUpTestApplicationComponent()
val scenario = launch<PinPasswordActivity>(
PinPasswordActivity.createPinPasswordActivityIntent(
context = context,
adminPin = adminPin,
profileId = adminId
)
)
testCoroutineDispatchers.runCurrent()
onView(withId(R.id.pin_password_input_pin_edit_text))
.perform(appendText(""), closeSoftKeyboard())
onView(withId(R.id.forgot_pin)).perform(click())
onView(withText(containsString("Reset")))
.inRoot(isDialog())
.perform(click())
onView(withText(context.getString(R.string.admin_confirm_app_wipe_positive_button_text)))
.inRoot(isDialog())
.perform(click())
testCoroutineDispatchers.runCurrent()
// After deletion, the activity calls finishAffinity() so that the next launch will go through
// the splash activity which would show an empty profile chooser.
scenario.onActivity { activity ->
assertThat(activity.isFinishing).isTrue()
}
try {
scenario.close()
} catch (e: SecurityException) {
// Expected: exitProcess(0) is called during onDestroy after confirming deletion.
}
}

@Test
fun testPinPassword_withAdmin_forgetPin_afterReset_localeIsNotReset() {
setUpTestApplicationComponent()
val scenario = launch<PinPasswordActivity>(
PinPasswordActivity.createPinPasswordActivityIntent(
context = context,
adminPin = adminPin,
profileId = adminId
)
)
testCoroutineDispatchers.runCurrent()
onView(withId(R.id.pin_password_input_pin_edit_text))
.perform(appendText(""), closeSoftKeyboard())
onView(withId(R.id.forgot_pin)).perform(click())
onView(withText(containsString("Reset")))
.inRoot(isDialog())
.perform(click())
onView(withText(context.getString(R.string.admin_confirm_app_wipe_positive_button_text)))
.inRoot(isDialog())
.perform(click())
testCoroutineDispatchers.runCurrent()
// Verify that the locale configuration is preserved after the reset.
scenario.onActivity { activity ->
val currentLocale = activity.resources.configuration.locales[0]
assertThat(currentLocale).isNotNull()
}
try {
scenario.close()
} catch (e: SecurityException) {
// Expected: exitProcess(0) is called during onDestroy after confirming deletion.
}
}

private fun getAppName(): String = context.resources.getString(R.string.app_name)

private fun getPinPasswordForgotMessage(): String =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1194,6 +1194,38 @@ class SplashActivityTest {
}
}

@Test
fun testSplashActivity_afterDataReset_recreatesActivitySuccessfully() {
simulateAppAlreadyOnboarded()
initializeTestApplication()
// Simulate a data reset by resetting the onboarding state (which deleteAllProfiles would do).
appStartupStateController.resetOnboardingState()
testCoroutineDispatchers.runCurrent()

// Verify the splash activity can successfully launch after reset without crashing.
launchSplashActivityFully {
intended(hasComponent(OnboardingActivity::class.java.name))
}
}

@Test
fun testSplashActivity_afterDataReset_localeInitialization_succeeds() {
simulateAppAlreadyOnboarded()
initializeTestApplication()
forceDefaultLocale(Locale.ENGLISH)
// Simulate a data reset.
appStartupStateController.resetOnboardingState()
testCoroutineDispatchers.runCurrent()

launchSplashActivityFully {
// Verify locale initialization still works after reset. The locale handler should be
// re-initialized with the system's default locale.
val displayLocale = appLanguageLocaleHandler.getDisplayLocale()
val context = displayLocale.localeContext
assertThat(context.languageDefinition.language).isEqualTo(ENGLISH)
}
}

private fun simulateAppAlreadyOnboarded() {
// Simulate the app was already onboarded by creating an isolated onboarding flow controller and
// saving the onboarding status on the system before the activity is opened. Note that this has
Expand Down
Loading