How to Test Dark Mode on Android (Complete Guide)
Dark mode is no longer a novelty; it is a user expectation baked into Android since API 29. When an app fails to respect the system theme, users notice immediately: text becomes unreadable, icons disa
Why Dark Mode Testing Matters on Android
Dark mode is no longer a novelty; it is a user expectation baked into Android since API 29. When an app fails to respect the system theme, users notice immediately: text becomes unreadable, icons disappear, and the overall experience feels broken. Beyond aesthetics, dark mode impacts battery life on OLED screens, reduces eye strain in low‑light environments, and can affect accessibility compliance.
From a quality perspective, dark mode introduces a second visual state that must be validated alongside the light state. Many defects only surface when the theme switches: hard‑coded colors, missing night‑mode resources, incorrect contrast ratios, and unintended side‑effects in custom views. Because the theme can change at runtime—via system settings, battery saver, or an in‑app toggle—tests must cover both static and dynamic scenarios.
Neglecting dark‑mode testing leads to poor user ratings, increased support tickets, and potential violations of accessibility guidelines (WCAG 2.1 AA contrast). In regulated industries, such as finance or health, a contrast failure can even trigger compliance issues. Therefore, a systematic approach to dark‑mode validation is essential for any Android release pipeline.
Dark Mode Fundamentals: System UI Modes and App Themes
Android provides two primary mechanisms for dark mode: the system‑wide UI mode (UI_MODE_NIGHT_YES/UI_MODE_NIGHT_NO) and AppCompat’s AppCompatDelegate.setDefaultNightMode. Understanding how these interact helps you design tests that cover the full spectrum of user‑initiated changes.
UI Mode vs. AppDelegate Night Mode
- UI Mode is read from the system configuration (`Configuration.uiMode. It is in night mode when the user enables the system dark theme or when Battery Saver forces it.
- AppDelegate Night Mode lets the app override the system choice. Values include
MODE_NIGHT_FOLLOW_SYSTEM,MODE_NIGHT_AUTO,MODE_NIGHT_YES, andMODE_NIGHT_NO.
When the app calls setDefaultNightMode, Android recreates the activity (unless you handle uiMode changes yourself). This recreation is a prime moment for bugs: resources that rely on android:night qualifiers may not be loaded, or view inflation may use stale cached values.
Resource Qualifiers and Theme Attributes
Resources placed in values-night/ are automatically selected when uiMode & UI_MODE_NIGHT_MASK == UI_MODE_NIGHT_YES. Similarly, theme attributes like ?attr/colorBackground or custom attributes` app‑switch colors without creating duplicate XML files.
Custom views that read colors directly from ContextCompat.getColor(context, R.color.my_red) ignore night qualifiers unless the color itself is defined in a night‑specific resource file. This is a common source of hard‑coded color bugs.
Configuration Changes
If you declare android:configChanges="uiMode" in the manifest, the activity will not be recreated on theme change. Instead, you receive a callback in onConfigurationChanged. Failing to update UI elements in that callback leads to stale colors after a switch.
Understanding these mechanics gives you a map of where to look for dark‑mode regressions: resource loading, activity recreation, manual view updates, and third‑party library behavior.
Test Matrix for Dark Mode (Happy Path, Error Paths, Edge Cases, Accessibility, Security/Privacy)
A structured matrix ensures you cover the dimensions that matter. Below is a comprehensive table you can adapt to your test plan. Each cell lists a concrete verification step, the expected result, and notes on automation feasibility.
| Category | Test ID | Description | Expected Result | Automation Feasibility |
|---|---|---|---|---|
| Happy Path | HP‑01 | Launch app with system dark theme enabled | All screens render using night resources; no hard‑coded light colors | High (UI Automator/Espresso) |
| HP‑02 | Toggle dark mode via in‑app setting (if provided) | UI updates instantly without restart; night resources applied | Medium (requires setting interaction) | |
| HP‑03 | Navigate through core flow (login → home → settings → logout) in dark mode | Every screen maintains correct contrast and element visibility | High | |
| HP‑04 | Rotate device while in dark mode | Configuration change handled; UI remains consistent | High | |
| HP‑05 | Return from background after system theme switch | App reflects new theme instantly | Medium (depends on handling of onConfigurationChanged) | |
| Error Paths | EP‑01 | Attempt to use a color resource missing night qualifier | Fallback to default color; no crash | Low (detect via screenshot diff) |
| EP‑02 | Custom view reads color via Resources.getSystem() | Color ignores night mode, causing contrast break | Low (static analysis) | |
| EP‑03 | Third‑party library forces light theme via AppCompatDelegate.setDefaultNightMode(MODE_NIGHT_NO) | App overrides user preference; potential accessibility fail | Medium (requires spy or instrumentation) | |
| EP‑04 | Activity launched with android:theme forcing light mode regardless of system | UI stays light; violates user setting | High (manifest inspection) | |
| Edge Cases | EC‑01 | Enable dark mode while Battery Saver is on (may force night) | UI stays dark; no flicker | Medium |
| EC‑02 | Switch theme while a dialog is open | Dialog updates or dismisses cleanly | Low (depends on dialog implementation) | |
| EC‑03 | Use multi‑window mode; one half in dark, other half in light (rare but possible on some OEM skins) | Each window respects its own theme | Low (device‑specific) | |
| EC‑04 | Launch app on a foldable device with dual screens; inner screen dark, outer screen light | Each screen applies correct theme | Low | |
| EC‑05 | Change font scale to 200% while in dark mode | Text scales, contrast remains ≥ 4.5:1 | Medium (requires accessibility tools) | |
| Accessibility | AC‑01 | Verify contrast ratio of all text against background ≥ 4.5:1 (AA) or 3:1 (large text) | Passes WCAG AA | High (using Android Accessibility Test Framework) |
| AC‑02 | Ensure icons and non‑text UI meet 3:1 contrast | Passes WCAG AA for UI components | High | |
| AC‑03 | Run TalkBack navigation; all focusable elements announce correctly | No missing labels, proper reading order | Medium | |
| AC‑04 | Test with color blindness simulator (deuteranopia, protanopia) | Information not conveyed solely by color | Low (requires external tool) | |
| AC‑05 | Verify that dynamic theme change does not reset accessibility font size | Font size persists after theme switch | Medium | |
| Security/Privacy | SE‑01 | Confirm that screenshot flag (Secure) is respected in dark mode | Screenshot of secure area yields black image | High (adb shell screencap) |
| SE‑02 | Ensure that password fields do not reveal characters via tooltip or hint color change in night mode | Hint remains obscured | Low (visual inspection) | |
| SE‑03 | Check that dark mode does not inadvertently expose hidden debug overlays | No overlay visible | Low | |
| SE‑04 | Verify that intent extras containing sensitive data are not logged when theme changes | No logcat output with PII | Medium (logcat monitoring) | |
| SE‑05 | Test that biometric prompt background follows dark theme and does not leak preview frames | Prompt uses night background; no frame leakage | Low (device‑specific) |
How to Use the Matrix
- Manual testing: Pick a subset (e.g., all HP and AC items) for each release candidate.
- Automated testing: Prioritize HP, EP, and AC items that have high feasibility. Use instrumentation tests for UI state, screenshot diff for EP‑01, and the Accessibility Test Framework for AC‑01/AC‑02.
- Exploratory testing: Reserve EC and SE items for sessions where testers can try unusual configurations (multi‑window, foldable, font scaling).
The matrix also serves as a living document: add new rows when you discover a theme‑related defect in production.
Manual Testing Step‑by‑Step Guide
Even with automation, a disciplined manual pass catches nuances that scripts overlook—especially those tied to system UI, OEM skins, or user‑generated content.
Preparing Devices and Emulators
- Select a matrix of devices: Include at least one OLED phone (for battery‑saver dark), one LCD phone, a tablet, and an emulator with API 33+.
- Enable Developer options:
Settings → About phone → Tap build number 7 times. - Turn on “Show layout bounds” and “Show surface updates” to visualize invalidations.
- Install the app under test via
adb install -r app-debug.apk. - Clear app data (
adb shell pm clear com.example.app) to start from a clean state each round.
Switching System Theme
- Via Settings:
Settings → Display → Theme → Dark. - Via ADB (useful for scripted manual runs):
adb shell settings put global ui_mode_night 2 # 0 = unspecified, 1 = no night, 2 = night
adb shell am broadcast -a android.intent.action.CONFIGURATION_CHANGED
Verify with adb shell cmd uimode night.
Navigating Core Flows
- Launch the app from launcher or via
adb shell monkey -p com.example.app -c android.intent.category.LAUNCHER 1. - Perform the primary user journey (e.g., sign‑up → verify email → add payment method → purchase).
- At each screen, pause for 2‑3 seconds to let any animations finish and observe:
- Text legibility
- Icon visibility (especially vector assets with
android:fillColor="?attr/colorOnSurface") - Divider and spacing correctness
- Any unexpected flashing or flicker
Checking UI Elements
- Contrast check: Use the built‑in “Accessibility Scanner” (available from Play Store) or the “Contrast Checker” in Android Studio’s Layout Inspector. Point the scanner at the screen; it will highlight any element below 4.5:1.
- Touch target size: Ensure tappable areas are at least 48 dp. Use the “Layout bounds” overlay to see hit‑boxes.
- State changes: Press buttons, toggle switches, and verify that pressed/focused states also respect night colors (often defined via
?attr/colorControlActivated).
Logging and Capturing Issues
- Logcat: Run
adb logcat -v time | grep -E "Activity|View|Theme"while switching themes to catch missing resource warnings. - Screenshots: Capture before/after theme switch:
adb shell screencap -p /sdcard/before.png
adb shell screencap -p /sdcard/after.png
adb pull /sdcard/before.png . && adb pull /sdcard/after.png .
Compare with a perceptual diff tool (e.g., ImageMagick compare -metric AE).
- Video: Record a short clip with
adb shell screenrecord /sdcard/demo.mp4to demonstrate flicker or delayed updates.
Post‑Test Cleanup
- Reset system theme to light (
adb shell settings put global ui_mode_night 0). - Clear app data again if you plan to test a different user persona (e.g., elderly with large font).
Repeating this checklist for each build ensures you catch regressions early and provides reproducible steps for bug reports.
Automated Testing Approaches
Manual checks are indispensable, but scaling them across dozens of device configurations requires automation. Below are strategies that integrate dark‑mode‑specific, ranging from unit‑level assertions to full‑blown UI tests.
Using UI Automator and Espresso for Dark Mode Checks
Both frameworks let you query the current UI mode and assert on view properties.
Espresso test to validate night‑mode resources:
@Test
fun `dark mode applies night colors`() {
// Force night mode via UiModeManager (requires API 29+)
val uiMode = InstrumentationRegistry.getInstrumentation()
.targetContext.getSystemService(UiModeManager::class.java)
uiMode.nightMode = UiModeManager.MODE_NIGHT_YES
// Launch activity under test
ActivityScenario.launch(MainActivity::class.java)
// Verify a TextView uses the night color
onView(withId(R.id.title_text))
.check(matches(withTextColor(R.color.title_night))) // custom matcher
}
*The withTextColor matcher can be built using ContextCompat.getColor.*
UI Automator for system‑wide theme toggle:
@Test
public void themeSwitchViaSettings() throws Exception {
UiDevice device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
// Open Settings
device.pressHome();
new UiObject(new UiSelector().description("Apps")).click();
new UiObject(new UiSelector().text("Settings")).click();
// Navigate to Display → Theme
new UiObject(new UiSelector().text("Display")).click();
new UiObject(new UiSelector().text("Theme")).click();
new UiObject(new UiSelector().text("Dark")).click();
// Return to app and verify
device.pressHome();
new UiObject(new UiSelector().text("MyApp")).click();
// Example: check that a button background is dark
UiObject button = new UiObject(new UiSelector().resourceId("com.example.app:id/action_button"));
assertTrue(button.getBackgroundColor() == Color.DKGRAY);
}
These tests can be run on a device farm (Firebase Test Lab, AWS Device Farm) to cover multiple API levels and OEM skins.
Leveraging Android Jetpack’s UiModeManager
Instead of relying on external intents, you can directly manipulate the UI mode inside your test suite:
@Before
fun setNightMode() {
val uiModeManager = ApplicationProvider.getApplicationContext()
.getSystemService(UiModeManager::class.java)
uiModeManager.nightMode = UiModeManager.MODE_NIGHT_YES
}
Remember to call ActivityScenario.recreate() after changing the mode if your activity does not handle uiMode config changes.
Screenshot Comparison with PixelMatch or Falco
Visual regression testing catches subtle color shifts that assertions miss.
Falco setup (Gradle):
dependencies {
debugImplementation "com.github.zhuinden:falco:1.5.0"
}
Test example:
@Rule
@JvmField
val falcoRule = FalcoRule()
@Test
fun `dark mode screen matches baseline`() {
// Ensure night mode
val uiMode = ApplicationProvider.getApplicationContext()
.getSystemService(UiModeManager::class.java)
uiMode.nightMode = UiModeManager.MODE_NIGHT_YES
launchActivity<MainActivity>()
falcoRule.capture("main_screen_dark")
}
Falco stores a baseline image on the first run (usually light mode) and compares subsequent captures using a perceptual diff algorithm. You can maintain separate baselines for light and night by switching the UI mode before capture.
Accessibility Automated Checks (Accessibility Test Framework)
Google’s androidx.test.espresso.accessibility library runs checks like contrast, touch target size, and content description.
@Test
fun `accessibility in dark mode`() {
// Set night mode via UiModeManager as before
// ...
// Enable accessibility checks
AccessibilityChecks.enable()
launchActivity<MainActivity>()
// Espresso actions that navigate through the screen
onView(withId(R.id.next_button)).perform(click())
}
The framework will fail the test if any view violates WCAG AA contrast or lacks a description.
Integrating Dark Mode Checks into CI
- Unit‑level: Run Espresso/UI Automator tests on every PR against a matrix of API levels (e.g., 21, 28, 30, 33) with both light and night modes forced via
UiModeManager. - Screenshot: Use Falco or Shot in a separate job that pushes baseline images to a storage bucket; compare new PR screenshots and comment on the PR with a diff link.
- Accessibility: Add the Accessibility Test Framework as a Gradle task (
connectedAndroidTest) and enforce a minimum score (e.g., no contrast failures). - Exploratory: Trigger a SUSA agent run (see later) on a nightly basis to surface persona‑driven issues that scripted tests miss.
By layering these techniques, you achieve both deterministic verification and the ability to spot unexpected regressions.
Tooling and Frameworks Specific to Android Dark Mode
Beyond general UI testing libraries, several tools shine when diagnosing theme‑related problems.
Android Studio Layout Inspector
- Run your app on a device or emulator.
- Open View → Tool Windows → Layout Inspector.
- The inspector shows the resolved value of each attribute (e.g.,
background). Switch the system theme while the inspector is attached; you’ll see values change in real time if the app reacts correctly. - Use the “Value Tracker” to log when a specific view’s background color changes—helpful for spotting missed updates.
Dark Mode Debugging with ADB Commands
- Query current UI mode:
adb shell cmd uimode night
# Returns 0 (unspecified), 1 (no night), 2 (night)
adb shell dumpsys activity activities | grep mUiMode
android:configChanges="uiMode"):
adb shell am broadcast -a android.intent.action.CONFIGURATION_CHANGED \
--ei android.intent.extra.UI_MODE_NIGHT 2
Screenshot Testing Libraries (Shot, Paparazzi)
- Shot (by Facebook) captures a view hierarchy and renders it to a bitmap, allowing pixel‑level comparison.
- Paparazzi (by CashApp) works similarly but integrates directly with Gradle and provides a CLI to generate diffs.
Both let you define test scenarios like:
@Test
fun `shot dark mode`() {
val scenario = launchActivity<MainActivity>()
scenario.onActivity { activity ->
Shot.create(activity.findViewById(R.id.root))
.record("main_dark")
}
}
Run the test twice—once with light mode forced, once with night mode forced—to generate two baselines.
Using SUSA (Autonomous QA) for Persona‑Driven Exploration
SUSA explores an app without pre‑written scripts, simulating distinct user personalities. When you point SUSA at an APK or a web URL and enable dark mode (via system settings or an in‑app toggle), it will:
- Curious persona: Taps every visible element, including obscure settings that might trigger a theme switch.
- Impatient persona: Rapidly rotates the device, fires back‑to‑back theme toggles, and checks for flicker or missing UI updates.
- Elderly persona: Applies large font scaling and high‑contrast mode alongside dark mode, surfacing contrast or layout‑break issues that younger testers might overlook.
- Accessibility persona: Activates TalkBack and Switch Control, verifying that focus order and announcements remain correct after each theme change.
- Adversarial persona: Attempts to force the app into impossible states (e.g., setting
uiModevia ADB while a dialog is open) to expose race conditions.
Because SUSA builds a session‑level memory of visited screens and dead ends, each subsequent run becomes smarter: it avoids re‑testing paths that previously passed and concentrates on unexplored edge cases—such as a screen that only appears after a failed network request in dark mode.
The output includes a detailed report with screenshots, logcat excerpts, and a PASS/FAIL verdict for each explored flow (login, signup, settings, etc.). Integrating SUSA into your nightly CI pipeline provides a safety net that catches defects no unit test would think to probe.
Edge Cases That Only Appear in Production
Even the most thorough test matrix can miss issues that arise from real‑world usage patterns, device fragmentation, or interactions with external services. Below are some of the most insidious dark‑mode bugs observed in the wild.
Dynamic Theme Switching via App Settings
Many apps offer a separate “Dark mode” toggle that overrides the system setting. Bugs appear when:
- The toggle stores its preference in
SharedPreferencesbut the UI does not react to changes until a full activity restart. - The app reads the preference only at
onCreate, ignoring subsequent changes while the activity is in the background. - A library (e.g., a navigation component) caches the theme at initialization and never re‑queries the preference.
Detection tip: Use Espresso to toggle the setting, then immediately navigate to a different fragment and assert that colors updated without a manual restart.
Third‑Party Libraries with Hardcoded Colors
Popular ad SDKs, analytics tools, or UI kits sometimes ship resources that lack night qualifiers. When your app enables dark mode, those views remain bright, creating a visual patchwork.
Mitigation:
- Run the
lintcheckMissingDefaultResourceto flag resources lacking night variants. - Use the Android Studio APK Analyzer to inspect the final APK for
drawableorcolorfiles absent a-nightqualifier. - If the library source is unavailable, apply a runtime theme overlay:
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
// After library init, force its views to inherit your theme:
ViewCompat.setOnApplyWindowInsetsListener(view) { v, insets ->
// no‑op, just ensures the view gets a fresh theme pass
v.onApplyWindowInsets(insets)
}
Night Mode with Battery Saver
On Android 9+, Battery Saver can automatically activate night mode regardless of the user’s system theme setting. Some OEMs also expose a “Battery saver theme” switch that forces a dark UI.
Bugs manifest when:
- Your app assumes night mode equals user preference and disables certain animations, causing a jarring experience when Battery Saver triggers unexpectedly.
- Background services read
UiModeManager.NIGHT_MODE_NOand decide to skip work, while the UI is actually dark.
Testing approach: Enable Battery Saver (adb shell dumpsys battery set saver true) and verify that UI still respects the user‑chosen theme (or that the app gracefully degrades to a low‑power mode without breaking readability).
Multi‑Display and Foldable Devices
Devices with secondary screens (e.g., LG V series, Microsoft Surface Duo) or foldables (Samsung Galaxy Z Fold) can report different UI modes per display.
Issues include:
- An activity launched on the secondary screen inherits the primary screen’s UI mode, leading to a bright panel on a dark‑intended display.
- Layouts that rely on
windowInsetsmay miscalculate padding when the screen state changes (folded ↔ unfolded).
Testing matrix addition:
| Test ID | Description | Expected |
|---|---|---|
| FD‑01 | Launch app on inner screen while outer screen is in light mode | Inner screen uses dark theme if system dark enabled |
| FD‑02 | Fold/unfold device while app is in foreground | Theme remains consistent; no flicker |
| FD‑03 | Drag app window to secondary display on a dual‑screen phone | UI updates to match display’s theme |
Accessibility Services Overriding Theme
Services like font magnifiers, color inversion, or dark‑mode overlays can composite with your app’s theme, sometimes resulting in contrast that is either too low or too high.
Example: A user enables Color inversion (Settings → Accessibility → Color inversion) while your app is already in dark mode; the inversion flips dark to light, making text disappear against a light background.
Testing tip: Enable the inversion service (adb shell settings put secure accessibility_display_inversion_enabled 1) and run your standard dark‑mode matrix. Verify that all critical information remains perceivable (use TalkBack to confirm).
Checklist for Dark Mode Release
Before you tag a release as “dark‑mode ready,” run through this concise list. Mark each item as PASS or FAIL; any FAIL blocks the release.
| # | Check | How to Verify |
|---|---|---|
| 1 | All launcher icons and adaptive icons have night variants | Inspect mipmap-* folders; verify ic_launcher-night.png exists |
| 2 | No hard‑coded color literals in Java/Kotlin (use androidx.core.content.ContextCompat.getColor) | Run detekt rule HardcodedColor or custom lint |
| 3 | Every values-night/ resource has a matching default in values/ | Use ./gradlew :app:androidDependencies and inspect merged resources |
| 4 | Activities correctly handle uiMode config changes (either recreate or update in onConfigurationChanged) | Toggle theme via ADB while activity is in foreground; observe no stale colors |
| 5 | Contrast ratio ≥ 4.5:1 for all text, ≥ 3:1 for icons and UI components | Run Accessibility Scanner or Espresso AccessibilityChecks.enable() |
| 6 | TalkBack navigation reads all labels correctly in dark mode | Enable TalkBack, swipe through screens, listen for missing descriptions |
| 7 | Screenshot of secure fields (FLAG_SECURE) remains black in dark mode | Use adb shell screencap on a secure activity; verify black output |
| 8 | Third‑party libraries do not force light mode (no setDefaultNightMode(MODE_NIGHT_NO)) | Search APK smali for Landroidx/appcompat/app/AppCompatDelegate;->setDefaultNightMode(I)V with argument 0 |
| 9 | Battery Saver activation does not break UI readability | Enable saver, verify contrast and element visibility |
| 10 | On foldable/multi‑display, each screen respects its own theme | Test on a device with secondary screen or use Android Studio’s emulator with multiple displays |
| 11 | No flicker or delayed updates when switching theme rapidly | Perform 10 quick toggles via ADB; visually inspect or record with screenrecord |
| 12 | Accessibility services (magnification, inversion, color correction) do not render UI unusable | Enable each service, run core flow, confirm usability |
If any item fails, create a bug with reproduction steps, logcat snippet, and a screenshot comparing light vs. night states.
Closing Takeaways
Dark mode is more than a cosmetic toggle; it is a dual‑state contract between your app and the system. Treating it as a first‑class feature means:
- Test both states systematically: Use the matrix above as a living checklist, updating it whenever you add new screens or integrate third‑party SDKs.
- Automate what you can: Espresso/UI Automator for state assertions, Falco/Shot for visual regression, and the Accessibility Test Framework for contrast guarantees.
- Explore what you cannot script: Person‑driven, autonomous tools like SUSA uncover the surprising interactions that only appear when real users—curious, impatient, elderly, or with assistive tech—exercise the app in dark mode.
- Watch the seams: Theme changes happen at activity boundaries, during configuration changes, and via external triggers (Battery Saver, multi‑window). Ensure your code responds consistently in every lifecycle callback.
- Document and share: Keep a centralized Confluence page or wiki page with the matrix, baseline screenshots, and known‑issue log. This reduces duplicate investigative work when a regression surfaces.
By combining rigorous manual procedures, targeted automation, and exploratory, persona‑driven testing, you can ship Android apps that look great, feel comfortable, and remain accessible whether the user prefers light or dark. The effort pays off in higher user satisfaction, fewer support tickets, and confidence that your app respects the system contract that millions of Android users rely on every day. Happy testing!
Test Your App Autonomously
Upload your APK or URL. SUSA explores like 10 real users — finds bugs, accessibility violations, and security issues. No scripts.
Try SUSA Free