diff --git a/app/build.gradle.kts b/app/build.gradle.kts index b4a72b95..e8563c58 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -83,6 +83,10 @@ android { } buildTypes { + getByName("debug").apply { + // Note that the build-conventions defines the res configs + isPseudoLocalesEnabled = true + } getByName("release").apply { isMinifyEnabled = project.property("IS_MINIFY_ENABLED").toString().toBoolean() isShrinkResources = project.property("IS_MINIFY_ENABLED").toString().toBoolean() @@ -143,14 +147,14 @@ dependencies { implementation(projects.spackleAndroidLib) implementation(projects.uiLib) - androidTestImplementation(projects.testLib) androidTestImplementation(libs.androidx.compose.test.junit) androidTestImplementation(libs.androidx.navigation.compose) androidTestImplementation(libs.androidx.uiAutomator) androidTestImplementation(libs.bundles.androidx.test) androidTestImplementation(projects.sdkExtLib) - androidTestImplementation(projects.spackleLib) androidTestImplementation(projects.sdkExtUiLib) + androidTestImplementation(projects.spackleLib) + androidTestImplementation(projects.testLib) if (isOrchestratorEnabled) { androidTestUtil(libs.androidx.test.orchestrator) { diff --git a/app/src/androidTest/java/co/electriccoin/zcash/app/ScreenshotTest.kt b/app/src/androidTest/java/co/electriccoin/zcash/app/ScreenshotTest.kt index c52c56a8..699f4d39 100644 --- a/app/src/androidTest/java/co/electriccoin/zcash/app/ScreenshotTest.kt +++ b/app/src/androidTest/java/co/electriccoin/zcash/app/ScreenshotTest.kt @@ -1,5 +1,8 @@ package co.electriccoin.zcash.app +import android.content.Context +import android.os.Build +import android.os.LocaleList import androidx.compose.ui.test.assertCountEquals import androidx.compose.ui.test.hasContentDescription import androidx.compose.ui.test.hasTestTag @@ -20,15 +23,18 @@ import androidx.test.espresso.Espresso.onView import androidx.test.espresso.matcher.ViewMatchers.isRoot import androidx.test.espresso.screenshot.captureToBitmap import androidx.test.ext.junit.rules.ActivityScenarioRule -import androidx.test.filters.SmallTest +import androidx.test.filters.LargeTest +import androidx.test.filters.MediumTest +import androidx.test.filters.SdkSuppress import cash.z.ecc.sdk.ext.ui.model.MonetarySeparators import cash.z.ecc.sdk.fixture.SeedPhraseFixture import cash.z.ecc.sdk.fixture.WalletAddressFixture -import co.electriccoin.zcash.app.test.getStringResource import co.electriccoin.zcash.spackle.FirebaseTestLabUtil import co.electriccoin.zcash.test.UiTestPrerequisites import co.electriccoin.zcash.ui.MainActivity import co.electriccoin.zcash.ui.R +import co.electriccoin.zcash.ui.design.component.ConfigurationOverride +import co.electriccoin.zcash.ui.design.component.UiMode import co.electriccoin.zcash.ui.screen.backup.BackupTag import co.electriccoin.zcash.ui.screen.home.viewmodel.SecretState import co.electriccoin.zcash.ui.screen.restore.RestoreTag @@ -38,13 +44,34 @@ import kotlinx.coroutines.withContext import org.junit.Rule import org.junit.Test +/* + * This screenshot implementation does not change the system-wide configuration, but rather + * injects a Context with a modified Configuration to change the uiMode and Locale. + * + * This works by: + * 1. Main Activity wraps the Composable with an Override + * 2. Main Activity exposes a Flow where a ConfigurationOverride can be set + * 3. We use an altered resContext in the tests instead of Application Context in order to load + * the right resources for comparison. + * + * Benefits of this implementation are that we do not modify system-wide values and don't require + * additional permissions to run these tests. + * + * Limitations of this implementation are that any views outside of Compose will not be updated, which + * can include the on-screen keyboard, system dialogs (like permissions), or other UI elements. + * + * An alternative implementation would be to use AppCompatActivity as the parent class for MainActivity, + * then rely on the AppCompat APIs for changing uiMode and Locale. This doesn't bring much benefit over + * our approach (it still has the problem with system dialogs and the keyboard), and it requires that + * we pull in the appcompat library. + */ class ScreenshotTest : UiTestPrerequisites() { companion object { - fun takeScreenshot(screenshotName: String) { + fun takeScreenshot(tag: String, screenshotName: String) { onView(isRoot()) .captureToBitmap() - .writeToTestStorage(screenshotName) + .writeToTestStorage("$screenshotName - $tag") } } @@ -57,9 +84,52 @@ class ScreenshotTest : UiTestPrerequisites() { } } + private fun runWith(uiMode: UiMode, locale: String, action: (Context, String) -> Unit) { + val configurationOverride = ConfigurationOverride(uiMode, LocaleList.forLanguageTags(locale)) + composeTestRule.activity.configurationOverrideFlow.value = configurationOverride + + val applicationContext = ApplicationProvider.getApplicationContext() + val configuration = configurationOverride.newConfiguration(applicationContext.resources.configuration) + val resContext = applicationContext.createConfigurationContext(configuration) + + action(resContext, "$uiMode-$locale") + } + @Test - @SmallTest - fun take_screenshots_for_restore_wallet() { + @MediumTest + fun take_screenshots_for_restore_wallet_light_en_XA() { + runWith(UiMode.Light, "en-XA") { context, tag -> + take_screenshots_for_restore_wallet(context, tag) + } + } + + @Test + @MediumTest + fun take_screenshots_for_restore_wallet_light_ar_XB() { + runWith(UiMode.Light, "ar-XB") { context, tag -> + take_screenshots_for_restore_wallet(context, tag) + } + } + + @Test + @MediumTest + fun take_screenshots_for_restore_wallet_light_en_US() { + runWith(UiMode.Light, "en-US") { context, tag -> + take_screenshots_for_restore_wallet(context, tag) + } + } + + // Dark mode was introduced in Android Q + @Test + @MediumTest + @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q) + fun take_screenshots_for_restore_wallet_dark_en_US() { + runWith(UiMode.Dark, "en-US") { context, tag -> + take_screenshots_for_restore_wallet(context, tag) + } + } + + private fun take_screenshots_for_restore_wallet(resContext: Context, tag: String) { // TODO [#286]: Screenshot tests fail on Firebase Test Lab if (FirebaseTestLabUtil.isFirebaseTestLab(ApplicationProvider.getApplicationContext())) { return @@ -67,25 +137,25 @@ class ScreenshotTest : UiTestPrerequisites() { composeTestRule.waitUntil { composeTestRule.activity.walletViewModel.secretState.value is SecretState.None } - composeTestRule.onNodeWithText(getStringResource(R.string.onboarding_1_header)).also { + composeTestRule.onNodeWithText(resContext.getString(R.string.onboarding_1_header)).also { it.assertExists() } - composeTestRule.onNodeWithText(getStringResource(R.string.onboarding_skip)).also { + composeTestRule.onNodeWithText(resContext.getString(R.string.onboarding_skip)).also { it.assertExists() it.performClick() } - composeTestRule.onNodeWithText(getStringResource(R.string.onboarding_4_import_existing_wallet)).also { + composeTestRule.onNodeWithText(resContext.getString(R.string.onboarding_4_import_existing_wallet)).also { it.assertExists() it.performClick() } - composeTestRule.onNodeWithText(getStringResource(R.string.restore_header)).also { + composeTestRule.onNodeWithText(resContext.getString(R.string.restore_header)).also { it.assertExists() } - takeScreenshot("Import 1") + takeScreenshot(tag, "Import 1") val seedPhraseSplitLength = SeedPhraseFixture.new().split.size SeedPhraseFixture.new().split.forEachIndexed { index, string -> @@ -94,199 +164,231 @@ class ScreenshotTest : UiTestPrerequisites() { // Take a screenshot half-way through filling in the seed phrase if (index == seedPhraseSplitLength / 2) { - takeScreenshot("Import 2") + takeScreenshot(tag, "Import 2") } } } - composeTestRule.onNodeWithText(getStringResource(R.string.restore_complete_header)).also { + composeTestRule.onNodeWithText(resContext.getString(R.string.restore_complete_header)).also { it.assertExists() } - takeScreenshot("Import 3") + takeScreenshot(tag, "Import 3") } @Test - @SmallTest - fun take_screenshots_for_new_wallet_and_rest_of_app() { + @LargeTest + fun take_screenshots_for_new_wallet_and_rest_of_app_light_en_XA() { + runWith(UiMode.Light, "en-XA") { context, tag -> + take_screenshots_for_new_wallet_and_rest_of_app(context, tag) + } + } + + @Test + @LargeTest + fun take_screenshots_for_new_wallet_and_rest_of_app_light_ar_XB() { + runWith(UiMode.Light, "ar-XB") { context, tag -> + take_screenshots_for_new_wallet_and_rest_of_app(context, tag) + } + } + + @Test + @LargeTest + fun take_screenshots_for_new_wallet_and_rest_of_app_light_en_US() { + runWith(UiMode.Light, "en-US") { context, tag -> + take_screenshots_for_new_wallet_and_rest_of_app(context, tag) + } + } + + // Dark mode was introduced in Android Q + @Test + @LargeTest + @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q) + fun take_screenshots_for_new_wallet_and_rest_of_app_dark_en_US() { + runWith(UiMode.Dark, "en-US") { context, tag -> + take_screenshots_for_new_wallet_and_rest_of_app(context, tag) + } + } + + private fun take_screenshots_for_new_wallet_and_rest_of_app(resContext: Context, tag: String) { // TODO [#286]: Screenshot tests fail on Firebase Test Lab if (FirebaseTestLabUtil.isFirebaseTestLab(ApplicationProvider.getApplicationContext())) { return } - onboardingScreenshots(composeTestRule) - backupScreenshots(composeTestRule) - homeScreenshots(composeTestRule) + onboardingScreenshots(resContext, tag, composeTestRule) + backupScreenshots(resContext, tag, composeTestRule) + homeScreenshots(resContext, tag, composeTestRule) // Profile screen // navigateTo(MainActivity.NAV_PROFILE) - composeTestRule.onNode(hasContentDescription(getStringResource(R.string.home_profile_content_description))).also { + composeTestRule.onNode(hasContentDescription(resContext.getString(R.string.home_profile_content_description))).also { it.assertExists() it.performClick() } - profileScreenshots(composeTestRule) + profileScreenshots(resContext, tag, composeTestRule) // Settings is a subscreen of profile - composeTestRule.onNode(hasText(getStringResource(R.string.profile_settings))).also { + composeTestRule.onNode(hasText(resContext.getString(R.string.profile_settings))).also { it.assertExists() it.performClick() } - settingsScreenshots(composeTestRule) + settingsScreenshots(resContext, tag, composeTestRule) // Back to profile - composeTestRule.onNode(hasContentDescription(getStringResource(R.string.settings_back_content_description))).also { + composeTestRule.onNode(hasContentDescription(resContext.getString(R.string.settings_back_content_description))).also { it.assertExists() it.performClick() } // Address Details is a subscreen of profile - composeTestRule.onNode(hasText(getStringResource(R.string.profile_see_address_details))).also { + composeTestRule.onNode(hasText(resContext.getString(R.string.profile_see_address_details))).also { it.assertExists() it.performClick() } - addressDetailsScreenshots(composeTestRule) + addressDetailsScreenshots(resContext, tag, composeTestRule) // Back to profile - composeTestRule.onNode(hasContentDescription(getStringResource(R.string.wallet_address_back_content_description))).also { + composeTestRule.onNode(hasContentDescription(resContext.getString(R.string.wallet_address_back_content_description))).also { it.assertExists() it.performClick() } // Contact Support is a subscreen of profile - composeTestRule.onNode(hasText(getStringResource(R.string.profile_support))).also { + composeTestRule.onNode(hasText(resContext.getString(R.string.profile_support))).also { it.performScrollTo() it.assertExists() it.performClick() } - supportScreenshots(composeTestRule) + supportScreenshots(resContext, tag, composeTestRule) // Back to profile - composeTestRule.onNode(hasContentDescription(getStringResource(R.string.support_back_content_description))).also { + composeTestRule.onNode(hasContentDescription(resContext.getString(R.string.support_back_content_description))).also { it.assertExists() it.performClick() } - composeTestRule.onNode(hasText(getStringResource(R.string.profile_title))).also { + composeTestRule.onNode(hasText(resContext.getString(R.string.profile_title))).also { it.assertExists() it.performClick() } // About is a subscreen of profile - composeTestRule.onNode(hasText(getStringResource(R.string.profile_about))).also { + composeTestRule.onNode(hasText(resContext.getString(R.string.profile_about))).also { it.performScrollTo() it.assertExists() it.performClick() } - aboutScreenshots(composeTestRule) + aboutScreenshots(resContext, tag, composeTestRule) // Back to profile - composeTestRule.onNode(hasContentDescription(getStringResource(R.string.about_back_content_description))).also { + composeTestRule.onNode(hasContentDescription(resContext.getString(R.string.about_back_content_description))).also { it.assertExists() it.performClick() } // Back to home - composeTestRule.onNode(hasContentDescription(getStringResource(R.string.settings_back_content_description))).also { + composeTestRule.onNode(hasContentDescription(resContext.getString(R.string.settings_back_content_description))).also { it.assertExists() it.performClick() } composeTestRule.waitUntil { composeTestRule.activity.walletViewModel.secretState.value is SecretState.Ready } - composeTestRule.onNode(hasText(getStringResource(R.string.home_button_request))).also { + composeTestRule.onNode(hasText(resContext.getString(R.string.home_button_request))).also { it.assertExists() it.performClick() } composeTestRule.waitUntil { composeTestRule.activity.walletViewModel.walletSnapshot.value != null } - requestZecScreenshots(composeTestRule) + requestZecScreenshots(resContext, tag, composeTestRule) navigateTo(MainActivity.NAV_HOME) composeTestRule.waitUntil { composeTestRule.activity.walletViewModel.secretState.value is SecretState.Ready } - composeTestRule.onNode(hasText(getStringResource(R.string.home_button_send))).also { + composeTestRule.onNode(hasText(resContext.getString(R.string.home_button_send))).also { it.assertExists() it.performClick() } composeTestRule.waitUntil { composeTestRule.activity.walletViewModel.synchronizer.value != null } composeTestRule.waitUntil { composeTestRule.activity.walletViewModel.spendingKey.value != null } composeTestRule.waitUntil { composeTestRule.activity.walletViewModel.walletSnapshot.value != null } - sendZecScreenshots(composeTestRule) + sendZecScreenshots(resContext, tag, composeTestRule) navigateTo(MainActivity.NAV_HOME) } } -private fun onboardingScreenshots(composeTestRule: AndroidComposeTestRule, MainActivity>) { +private fun onboardingScreenshots(resContext: Context, tag: String, composeTestRule: AndroidComposeTestRule, MainActivity>) { composeTestRule.waitUntil { composeTestRule.activity.walletViewModel.secretState.value is SecretState.None } - composeTestRule.onNodeWithText(getStringResource(R.string.onboarding_1_header)).also { + composeTestRule.onNodeWithText(resContext.getString(R.string.onboarding_1_header)).also { it.assertExists() } - ScreenshotTest.takeScreenshot("Onboarding 1") + ScreenshotTest.takeScreenshot(tag, "Onboarding 1") - composeTestRule.onNodeWithText(getStringResource(R.string.onboarding_next)).also { + composeTestRule.onNodeWithText(resContext.getString(R.string.onboarding_next)).also { it.performClick() } - composeTestRule.onNodeWithText(getStringResource(R.string.onboarding_2_header)).also { + composeTestRule.onNodeWithText(resContext.getString(R.string.onboarding_2_header)).also { it.assertExists() - ScreenshotTest.takeScreenshot("Onboarding 2") + ScreenshotTest.takeScreenshot(tag, "Onboarding 2") } - composeTestRule.onNodeWithText(getStringResource(R.string.onboarding_next)).also { + composeTestRule.onNodeWithText(resContext.getString(R.string.onboarding_next)).also { it.performClick() } - composeTestRule.onNodeWithText(getStringResource(R.string.onboarding_3_header)).also { + composeTestRule.onNodeWithText(resContext.getString(R.string.onboarding_3_header)).also { it.assertExists() - ScreenshotTest.takeScreenshot("Onboarding 3") + ScreenshotTest.takeScreenshot(tag, "Onboarding 3") } - composeTestRule.onNodeWithText(getStringResource(R.string.onboarding_next)).also { + composeTestRule.onNodeWithText(resContext.getString(R.string.onboarding_next)).also { it.performClick() } - composeTestRule.onNodeWithText(getStringResource(R.string.onboarding_4_header)).also { + composeTestRule.onNodeWithText(resContext.getString(R.string.onboarding_4_header)).also { it.assertExists() - ScreenshotTest.takeScreenshot("Onboarding 4") + ScreenshotTest.takeScreenshot(tag, "Onboarding 4") } - composeTestRule.onNodeWithText(getStringResource(R.string.onboarding_4_create_new_wallet)).also { + composeTestRule.onNodeWithText(resContext.getString(R.string.onboarding_4_create_new_wallet)).also { it.performClick() } } -private fun backupScreenshots(composeTestRule: AndroidComposeTestRule, MainActivity>) { +private fun backupScreenshots(resContext: Context, tag: String, composeTestRule: AndroidComposeTestRule, MainActivity>) { composeTestRule.waitUntil { composeTestRule.activity.walletViewModel.secretState.value is SecretState.NeedsBackup } - composeTestRule.onNodeWithText(getStringResource(R.string.new_wallet_1_header)).also { + composeTestRule.onNodeWithText(resContext.getString(R.string.new_wallet_1_header)).also { it.assertExists() } - ScreenshotTest.takeScreenshot("Backup 1") + ScreenshotTest.takeScreenshot(tag, "Backup 1") - composeTestRule.onNodeWithText(getStringResource(R.string.new_wallet_1_button)).also { + composeTestRule.onNodeWithText(resContext.getString(R.string.new_wallet_1_button)).also { it.performClick() } - composeTestRule.onNodeWithText(getStringResource(R.string.new_wallet_2_header)).also { + composeTestRule.onNodeWithText(resContext.getString(R.string.new_wallet_2_header)).also { it.assertExists() } - ScreenshotTest.takeScreenshot("Backup 2") + ScreenshotTest.takeScreenshot(tag, "Backup 2") - composeTestRule.onNodeWithText(getStringResource(R.string.new_wallet_2_button)).also { + composeTestRule.onNodeWithText(resContext.getString(R.string.new_wallet_2_button)).also { it.performClick() } - composeTestRule.onNodeWithText(getStringResource(R.string.new_wallet_3_header)).also { + composeTestRule.onNodeWithText(resContext.getString(R.string.new_wallet_3_header)).also { it.assertExists() } - ScreenshotTest.takeScreenshot("Backup 3") + ScreenshotTest.takeScreenshot(tag, "Backup 3") - composeTestRule.onNodeWithText(getStringResource(R.string.new_wallet_3_button_finished)).also { + composeTestRule.onNodeWithText(resContext.getString(R.string.new_wallet_3_button_finished)).also { it.performClick() } - composeTestRule.onNodeWithText(getStringResource(R.string.new_wallet_4_header_verify)).also { + composeTestRule.onNodeWithText(resContext.getString(R.string.new_wallet_4_header_verify)).also { it.assertExists() } - ScreenshotTest.takeScreenshot("Backup 4") + ScreenshotTest.takeScreenshot(tag, "Backup 4") // Fail test first composeTestRule.onAllNodesWithTag(BackupTag.DROPDOWN_CHIP).also { @@ -302,21 +404,21 @@ private fun backupScreenshots(composeTestRule: AndroidComposeTestRule, MainActivity>) { +private fun homeScreenshots(resContext: Context, tag: String, composeTestRule: AndroidComposeTestRule, MainActivity>) { composeTestRule.waitUntil { composeTestRule.activity.walletViewModel.secretState.value is SecretState.Ready } composeTestRule.waitUntil { composeTestRule.activity.walletViewModel.walletSnapshot.value != null } - composeTestRule.onNode(hasText(getStringResource(R.string.home_button_send))).also { + composeTestRule.onNode(hasText(resContext.getString(R.string.home_button_send))).also { it.assertExists() - ScreenshotTest.takeScreenshot("Home 1") + ScreenshotTest.takeScreenshot(tag, "Home 1") } } -private fun profileScreenshots(composeTestRule: AndroidComposeTestRule, MainActivity>) { +private fun profileScreenshots(resContext: Context, tag: String, composeTestRule: AndroidComposeTestRule, MainActivity>) { composeTestRule.waitUntil { composeTestRule.activity.walletViewModel.addresses.value != null } - composeTestRule.onNode(hasText(getStringResource(R.string.profile_title))).also { + composeTestRule.onNode(hasText(resContext.getString(R.string.profile_title))).also { it.assertExists() } - ScreenshotTest.takeScreenshot("Profile 1") + ScreenshotTest.takeScreenshot(tag, "Profile 1") } -private fun settingsScreenshots(composeTestRule: ComposeTestRule) { - composeTestRule.onNode(hasText(getStringResource(R.string.settings_header))).also { +private fun settingsScreenshots(resContext: Context, tag: String, composeTestRule: ComposeTestRule) { + composeTestRule.onNode(hasText(resContext.getString(R.string.settings_header))).also { it.assertExists() } - ScreenshotTest.takeScreenshot("Settings 1") + ScreenshotTest.takeScreenshot(tag, "Settings 1") } -private fun addressDetailsScreenshots(composeTestRule: ComposeTestRule) { - composeTestRule.onNode(hasText(getStringResource(R.string.wallet_address_title))).also { +private fun addressDetailsScreenshots(resContext: Context, tag: String, composeTestRule: ComposeTestRule) { + composeTestRule.onNode(hasText(resContext.getString(R.string.wallet_address_title))).also { it.assertExists() } - ScreenshotTest.takeScreenshot("Addresses 1") + ScreenshotTest.takeScreenshot(tag, "Addresses 1") } -private fun requestZecScreenshots(composeTestRule: ComposeTestRule) { - composeTestRule.onNode(hasText(getStringResource(R.string.request_title))).also { +private fun requestZecScreenshots(resContext: Context, tag: String, composeTestRule: ComposeTestRule) { + composeTestRule.onNode(hasText(resContext.getString(R.string.request_title))).also { it.assertExists() } - ScreenshotTest.takeScreenshot("Request 1") + ScreenshotTest.takeScreenshot(tag, "Request 1") } -private fun sendZecScreenshots(composeTestRule: ComposeTestRule) { - composeTestRule.onNode(hasText(getStringResource(R.string.send_title))).also { +private fun sendZecScreenshots(resContext: Context, tag: String, composeTestRule: ComposeTestRule) { + composeTestRule.onNode(hasText(resContext.getString(R.string.send_title))).also { it.assertExists() } - ScreenshotTest.takeScreenshot("Send 1") + ScreenshotTest.takeScreenshot(tag, "Send 1") - composeTestRule.onNodeWithText(getStringResource(R.string.send_amount)).also { + composeTestRule.onNodeWithText(resContext.getString(R.string.send_amount)).also { val separators = MonetarySeparators.current() it.performTextInput("{${separators.decimal}}123") } - composeTestRule.onNodeWithText(getStringResource(R.string.send_to)).also { + composeTestRule.onNodeWithText(resContext.getString(R.string.send_to)).also { it.performTextInput(WalletAddressFixture.UNIFIED_ADDRESS_STRING) } - composeTestRule.onNodeWithText(getStringResource(R.string.send_create)).also { + composeTestRule.onNodeWithText(resContext.getString(R.string.send_create)).also { it.performClick() } composeTestRule.waitForIdle() - ScreenshotTest.takeScreenshot("Send 2") + ScreenshotTest.takeScreenshot(tag, "Send 2") } -private fun supportScreenshots(composeTestRule: ComposeTestRule) { - composeTestRule.onNode(hasText(getStringResource(R.string.support_header))).also { +private fun supportScreenshots(resContext: Context, tag: String, composeTestRule: ComposeTestRule) { + composeTestRule.onNode(hasText(resContext.getString(R.string.support_header))).also { it.assertExists() } - ScreenshotTest.takeScreenshot("Support 1") + ScreenshotTest.takeScreenshot(tag, "Support 1") } -private fun aboutScreenshots(composeTestRule: ComposeTestRule) { - composeTestRule.onNode(hasText(getStringResource(R.string.about_title))).also { +private fun aboutScreenshots(resContext: Context, tag: String, composeTestRule: ComposeTestRule) { + composeTestRule.onNode(hasText(resContext.getString(R.string.about_title))).also { it.assertExists() } - ScreenshotTest.takeScreenshot("About 1") + ScreenshotTest.takeScreenshot(tag, "About 1") } diff --git a/app/src/androidTest/java/co/electriccoin/zcash/app/test/GetStringResource.kt b/app/src/androidTest/java/co/electriccoin/zcash/app/test/GetStringResource.kt deleted file mode 100644 index eb2d3ca7..00000000 --- a/app/src/androidTest/java/co/electriccoin/zcash/app/test/GetStringResource.kt +++ /dev/null @@ -1,7 +0,0 @@ -package co.electriccoin.zcash.app.test - -import android.content.Context -import androidx.annotation.StringRes -import androidx.test.core.app.ApplicationProvider - -fun getStringResource(@StringRes resId: Int) = ApplicationProvider.getApplicationContext().getString(resId) diff --git a/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/Override.kt b/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/Override.kt new file mode 100644 index 00000000..7d886533 --- /dev/null +++ b/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/Override.kt @@ -0,0 +1,64 @@ +package co.electriccoin.zcash.ui.design.component + +import android.content.res.Configuration +import android.os.LocaleList +import android.view.ContextThemeWrapper +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.collectAsState +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalContext +import kotlinx.coroutines.flow.StateFlow + +/** + * Wrap a Composable with a way to override the Android Configuration. This is primarily useful + * for automated tests. + */ +@Composable +fun Override(configurationOverrideFlow: StateFlow, content: @Composable () -> Unit) { + val configurationOverride = configurationOverrideFlow.collectAsState().value + + if (null == configurationOverride) { + content() + } else { + val configuration = configurationOverride.newConfiguration(LocalConfiguration.current) + + val contextWrapper = run { + val context = LocalContext.current + object : ContextThemeWrapper() { + init { + attachBaseContext(context) + applyOverrideConfiguration(configuration) + } + } + } + + CompositionLocalProvider( + LocalConfiguration provides configuration, + LocalContext provides contextWrapper + ) { + content() + } + } +} + +data class ConfigurationOverride(val uiMode: UiMode?, val locale: LocaleList?) { + fun newConfiguration(fromConfiguration: Configuration) = Configuration(fromConfiguration).apply { + this@ConfigurationOverride.uiMode?.let { + uiMode = (uiMode and Configuration.UI_MODE_NIGHT_MASK.inv()) or it.flag() + } + + this@ConfigurationOverride.locale?.let { + setLocales(it) + } + } +} + +enum class UiMode { + Light, Dark; +} + +private fun UiMode.flag() = when (this) { + UiMode.Light -> Configuration.UI_MODE_NIGHT_NO + UiMode.Dark -> Configuration.UI_MODE_NIGHT_YES +} diff --git a/ui-lib/build.gradle.kts b/ui-lib/build.gradle.kts index c587c73e..308d5a60 100644 --- a/ui-lib/build.gradle.kts +++ b/ui-lib/build.gradle.kts @@ -81,7 +81,7 @@ dependencies { implementation(projects.sdkExtLib) implementation(projects.sdkExtUiLib) implementation(projects.spackleAndroidLib) - implementation(projects.uiDesignLib) + api(projects.uiDesignLib) androidTestImplementation(projects.testLib) androidTestImplementation(libs.bundles.androidx.test) diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/MainActivity.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/MainActivity.kt index e5ffc988..564139bd 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/MainActivity.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/MainActivity.kt @@ -33,7 +33,9 @@ import cash.z.ecc.sdk.type.fromResources import co.electriccoin.zcash.spackle.EmulatorWtfUtil import co.electriccoin.zcash.spackle.FirebaseTestLabUtil import co.electriccoin.zcash.ui.design.compat.FontCompat +import co.electriccoin.zcash.ui.design.component.ConfigurationOverride import co.electriccoin.zcash.ui.design.component.GradientSurface +import co.electriccoin.zcash.ui.design.component.Override import co.electriccoin.zcash.ui.design.theme.ZcashTheme import co.electriccoin.zcash.ui.screen.about.WrapAbout import co.electriccoin.zcash.ui.screen.backup.WrapBackup @@ -59,6 +61,7 @@ import co.electriccoin.zcash.ui.screen.update.AppUpdateCheckerImp import co.electriccoin.zcash.ui.screen.update.WrapUpdate import co.electriccoin.zcash.ui.screen.update.model.UpdateState import co.electriccoin.zcash.ui.screen.wallet_address.view.WalletAddresses +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlin.time.Duration @@ -84,6 +87,9 @@ class MainActivity : ComponentActivity() { @VisibleForTesting(otherwise = VisibleForTesting.NONE) lateinit var navControllerForTesting: NavHostController + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + val configurationOverrideFlow = MutableStateFlow(null) + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -120,25 +126,27 @@ class MainActivity : ComponentActivity() { private fun setupUiContent() { setContent { - ZcashTheme { - GradientSurface( - Modifier - .fillMaxWidth() - .fillMaxHeight() - ) { - when (val secretState = walletViewModel.secretState.collectAsState().value) { - SecretState.Loading -> { - // For now, keep displaying splash screen using condition above. - // In the future, we might consider displaying something different here. + Override(configurationOverrideFlow) { + ZcashTheme { + GradientSurface( + Modifier + .fillMaxWidth() + .fillMaxHeight() + ) { + when (val secretState = walletViewModel.secretState.collectAsState().value) { + SecretState.Loading -> { + // For now, keep displaying splash screen using condition above. + // In the future, we might consider displaying something different here. + } + SecretState.None -> { + WrapOnboarding() + } + is SecretState.NeedsBackup -> WrapBackup( + secretState.persistableWallet, + onBackupComplete = { walletViewModel.persistBackupComplete() } + ) + is SecretState.Ready -> Navigation() } - SecretState.None -> { - WrapOnboarding() - } - is SecretState.NeedsBackup -> WrapBackup( - secretState.persistableWallet, - onBackupComplete = { walletViewModel.persistBackupComplete() } - ) - is SecretState.Ready -> Navigation() } } } diff --git a/ui-lib/src/main/res/ui/common/values/themes.xml b/ui-lib/src/main/res/ui/common/values/themes.xml index 4e88851b..4cfc967b 100644 --- a/ui-lib/src/main/res/ui/common/values/themes.xml +++ b/ui-lib/src/main/res/ui/common/values/themes.xml @@ -3,6 +3,6 @@