From 0853bf09a99b89ea0fab1da62eea9e742fd3660f Mon Sep 17 00:00:00 2001 From: net <96362337+netqo@users.noreply.github.com> Date: Wed, 27 May 2026 19:13:04 -0300 Subject: [PATCH 1/2] chore(brand): replace launcher icon + splash glyph with the Stack mark The Android Studio template robot was still shipping as the app icon and as the splash mark. Ports the layered-squares glyph the login hero uses (LoginScreen.StackLogoGlyph) into the launcher + splash slots so the app's branding is consistent from the launcher tap through to the first Compose frame. * ic_launcher_foreground: brand glyph sized for the adaptive-icon safe zone (central 66dp of a 108dp viewport) so the corners survive round / squircle launcher masks. * ic_launcher_background: solid SurfaceBase (#0B0B12) plate that matches the window background, so the splash-to-content transition doesn't flash a different color. * ic_launcher_monochrome: single-tint variant for Android 13+ themed icons (the previous adaptive-icon XML pointed monochrome at the colored foreground, which the platform can't recolor cleanly). * ic_brand_splash: standalone splash glyph that fills the full 108dp viewport. The launcher foreground has to keep its content inside the safe zone and would render visibly small in the SplashScreen API icon area (no mask is applied there); this drawable expands the same three squares so the splash reads as the Stack mark instead of a tiny inset. * themes.xml: splash theme now points at ic_brand_splash. Legacy mipmap-{m,h,xh,xxh,xxxh}dpi PNGs are untouched. They only ship to API 24-25 devices that can't load the adaptive-icon XML (min SDK is 24, so the fallback is required); generating new PNGs from the vector needs a build-time tool that isn't wired in yet. --- app/src/main/res/drawable/ic_brand_splash.xml | 28 +++ .../res/drawable/ic_launcher_background.xml | 168 +----------------- .../res/drawable/ic_launcher_foreground.xml | 45 +++-- .../res/drawable/ic_launcher_monochrome.xml | 26 +++ .../res/mipmap-anydpi-v26/ic_launcher.xml | 2 +- .../mipmap-anydpi-v26/ic_launcher_round.xml | 2 +- app/src/main/res/values/themes.xml | 2 +- 7 files changed, 85 insertions(+), 188 deletions(-) create mode 100644 app/src/main/res/drawable/ic_brand_splash.xml create mode 100644 app/src/main/res/drawable/ic_launcher_monochrome.xml diff --git a/app/src/main/res/drawable/ic_brand_splash.xml b/app/src/main/res/drawable/ic_brand_splash.xml new file mode 100644 index 0000000..252bb87 --- /dev/null +++ b/app/src/main/res/drawable/ic_brand_splash.xml @@ -0,0 +1,28 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml index 07d5da9..44277e9 100644 --- a/app/src/main/res/drawable/ic_launcher_background.xml +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -1,170 +1,16 @@ + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml index 2b068d1..ac3bfdc 100644 --- a/app/src/main/res/drawable/ic_launcher_foreground.xml +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -1,30 +1,27 @@ + - - - - - - - - - \ No newline at end of file + android:fillColor="#00000000" + android:strokeColor="#8B5CF6" + android:strokeWidth="3" + android:pathData="M32,32 h44 v44 h-44 z" /> + + + diff --git a/app/src/main/res/drawable/ic_launcher_monochrome.xml b/app/src/main/res/drawable/ic_launcher_monochrome.xml new file mode 100644 index 0000000..efc18b0 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_monochrome.xml @@ -0,0 +1,26 @@ + + + + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index 6f3b755..b070c76 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -2,5 +2,5 @@ - + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml index 6f3b755..b070c76 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -2,5 +2,5 @@ - + \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 39518bb..a134db5 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -17,7 +17,7 @@ postSplashScreenTheme once the activity is ready. --> From 5d925f32fcfc131750055b03a3782f578ef88cb2 Mon Sep 17 00:00:00 2001 From: net <96362337+netqo@users.noreply.github.com> Date: Wed, 27 May 2026 19:27:51 -0300 Subject: [PATCH 2/2] fix(test): make StackNavHostTest survive routes that use hiltViewModel The smoke suite started failing the moment real screens replaced the nav placeholders, because the default createComposeRule() launches a plain ComponentActivity that Hilt cannot wire ("Given component holder class androidx.activity.ComponentActivity does not implement interface dagger.hilt.internal.GeneratedComponent..."). Adds a bare `@AndroidEntryPoint HiltTestActivity` in `src/debug/` so the debug APK ships it (the instrumentation runner resolves activities through the target APK's PackageManager, so the class cannot live in `src/androidTest/` alone). The test now wires HiltAndroidRule + createAndroidComposeRule() via a RuleChain so Hilt injects before the activity launches; navigation uses defaultPath to support Wallet's optional `?tab={tab}` query arg without re-asserting the bare path. All 26 instrumented tests pass on device after this change. --- .../navigation/StackNavHostTest.kt | 38 +++++++++++++------ app/src/debug/AndroidManifest.xml | 17 +++++++++ .../stackcasino/HiltTestActivity.kt | 23 +++++++++++ 3 files changed, 67 insertions(+), 11 deletions(-) create mode 100644 app/src/debug/AndroidManifest.xml create mode 100644 app/src/debug/java/com/plainstudio/stackcasino/HiltTestActivity.kt diff --git a/app/src/androidTest/java/com/plainstudio/stackcasino/navigation/StackNavHostTest.kt b/app/src/androidTest/java/com/plainstudio/stackcasino/navigation/StackNavHostTest.kt index 9905da2..a44a0cd 100644 --- a/app/src/androidTest/java/com/plainstudio/stackcasino/navigation/StackNavHostTest.kt +++ b/app/src/androidTest/java/com/plainstudio/stackcasino/navigation/StackNavHostTest.kt @@ -1,25 +1,41 @@ package com.plainstudio.stackcasino.navigation -import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.navigation.compose.ComposeNavigator import androidx.navigation.testing.TestNavHostController -import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.plainstudio.stackcasino.HiltTestActivity +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Assert.assertEquals import org.junit.Rule import org.junit.Test +import org.junit.rules.RuleChain import org.junit.runner.RunWith /** - * Smoke-validates that every [Route] declared in the sealed hierarchy is - * actually registered in the nav graph and reachable from the start - * destination. If a route is added to [Route] without a matching + * Smoke-validates that every [Route] declared in the sealed hierarchy + * is actually registered in the nav graph and reachable from the + * start destination. If a route is added to [Route] without a matching * `composable(...)` block in [StackNavHost], the call to [navigate] * here throws and the test fails. + * + * Runs against [HiltTestActivity] (not the default `ComponentActivity` + * the bare `createComposeRule()` would spin up) because several + * destinations call `hiltViewModel()` which only resolves on an + * `@AndroidEntryPoint` host. */ +@HiltAndroidTest @RunWith(AndroidJUnit4::class) class StackNavHostTest { + private val hiltRule = HiltAndroidRule(this) + private val composeRule = createAndroidComposeRule() + + // Hilt has to inject the test before the Compose rule mounts the + // activity, otherwise hiltViewModel() inside the first composed + // screen has no graph to pull from. @get:Rule - val composeRule = createComposeRule() + val ruleChain: RuleChain = RuleChain.outerRule(hiltRule).around(composeRule) private lateinit var navController: TestNavHostController @@ -27,8 +43,8 @@ class StackNavHostTest { fun every_static_route_is_reachable() { composeRule.setContent { navController = - TestNavHostController(ApplicationProvider.getApplicationContext()).apply { - navigatorProvider.addNavigator(androidx.navigation.compose.ComposeNavigator()) + TestNavHostController(composeRule.activity).apply { + navigatorProvider.addNavigator(ComposeNavigator()) } StackNavHost(navController = navController, startDestination = Route.Login.path) } @@ -52,7 +68,7 @@ class StackNavHostTest { ) staticTargets.forEach { route -> - composeRule.runOnUiThread { navController.navigate(route.path) } + composeRule.runOnUiThread { navController.navigate(route.defaultPath) } composeRule.waitForIdle() assertEquals( "Navigation to ${route.path} did not land on the expected destination.", @@ -66,8 +82,8 @@ class StackNavHostTest { fun parametric_routes_resolve_with_arguments() { composeRule.setContent { navController = - TestNavHostController(ApplicationProvider.getApplicationContext()).apply { - navigatorProvider.addNavigator(androidx.navigation.compose.ComposeNavigator()) + TestNavHostController(composeRule.activity).apply { + navigatorProvider.addNavigator(ComposeNavigator()) } StackNavHost(navController = navController, startDestination = Route.Login.path) } diff --git a/app/src/debug/AndroidManifest.xml b/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..60e50b3 --- /dev/null +++ b/app/src/debug/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + + + + + + diff --git a/app/src/debug/java/com/plainstudio/stackcasino/HiltTestActivity.kt b/app/src/debug/java/com/plainstudio/stackcasino/HiltTestActivity.kt new file mode 100644 index 0000000..13eb651 --- /dev/null +++ b/app/src/debug/java/com/plainstudio/stackcasino/HiltTestActivity.kt @@ -0,0 +1,23 @@ +package com.plainstudio.stackcasino + +import androidx.activity.ComponentActivity +import dagger.hilt.android.AndroidEntryPoint + +/** + * Bare-bones `@AndroidEntryPoint` shell used as the host activity for + * Compose tests that mount screens calling `hiltViewModel()`. The + * default `createComposeRule()` spins up a plain `ComponentActivity` + * which Hilt cannot wire (it throws "Given component holder class + * androidx.activity.ComponentActivity does not implement interface + * dagger.hilt.internal.GeneratedComponent..."), so any Compose test + * that needs the Hilt graph has to launch through this activity via + * `createAndroidComposeRule()`. + * + * Lives in `src/debug/` (not `src/androidTest/`) because Android's + * instrumentation runner resolves activities through the target APK's + * PackageManager: a class declared only in the test APK would fail + * with "Unable to resolve activity". Production release builds drop + * the activity entirely since the `debug/` sourceset is variant-scoped. + */ +@AndroidEntryPoint +class HiltTestActivity : ComponentActivity()