How to Automate Dark Mode Testing (Step-by-Step)

How to Automate Dark Mode Testing (Step-by-Step) begins with understanding why dark mode matters for modern applications. Users expect interfaces that adapt to ambient lighting, reduce eye strain, and

January 31, 2026 · 14 min read · How-To Guides

How to Automate Dark Mode Testing (Step-by-Step) begins with understanding why dark mode matters for modern applications. Users expect interfaces that adapt to ambient lighting, reduce eye strain, and conserve battery on OLED screens. When a feature is visible to every visitor, regressions in dark mode can damage brand perception and increase support tickets. Manual verification of every screen, state, and theme toggle quickly becomes unsustainable as the product grows. Automation provides repeatable coverage, catches contrast failures early, and frees exploratory testers to focus on edge‑case scenarios that scripts cannot anticipate. This guide walks you through a complete, production‑ready process: choosing a framework, building stable tests, managing locators, handling waits, setting up data, integrating with CI, and reporting results. Each step includes concrete code snippets, a test matrix, and a tool‑comparison table to help you decide where to invest effort.

When Automation Pays Off for Dark Mode Testing

Automation is not a universal silver bullet; it shines when certain conditions are met. First, consider the frequency of theme toggles in your release cycle. If you ship UI changes weekly and each change could affect color contrast, automated checks give you immediate feedback. Second, evaluate the number of distinct screens or components that support dark mode. A mature design system with dozens of reusable widgets benefits from a single test suite that validates each variant. Third, assess the risk of visual regressions. Contrast violations, missing overrides, and hard‑coded colors often slip through manual review because they are subtle and depend on the device’s theme setting. Finally, factor in team capacity. When manual testers spend more than an hour per release on theme verification, automating that effort yields a measurable ROI.

Decision Matrix: Manual vs Automated Dark Mode Checks

CriteriaManual TestingAutomated Testing
Test execution time per release60‑120 minutes (depends on screen count)5‑15 minutes (parallel execution)
Ability to catch contrast failuresLow (relies on human perception)High (uses automated contrast algorithms)
Maintenance overheadLow (no code)Medium (test scripts, locator updates)
Scalability to new componentsPoor (each new screen adds time)Good (add test cases or reuse existing)
Flakiness due to timing/theme switchesNone (human waits as needed)Possible (requires explicit waits)
Suitability for exploratory edge casesExcellentLimited (scripted paths)

If your product scores high on frequency, screen count, and risk, move toward automation. Use the matrix as a starting point; adjust weights to reflect your team’s context.

How to Automate Dark Mode Testing (Step-by-Step) – Framework Selection

Choosing the right test framework determines how easily you can switch themes, locate elements, and assert visual properties. For web applications, Playwright and Cypress dominate because they provide built‑in support for emulating CSS media features. For native mobile apps, Appium (with Espresso or XCUITest drivers) lets you query the system’s UI mode and inspect view attributes. Below is a comparison of the most popular options.

Tool‑Comparison Table for Dark Mode Automation

FrameworkLanguage SupportTheme Switch MechanismContrast CheckingParallel ExecutionLearning CurveIdeal For
PlaywrightJS/TS, Python, .NET, Javapage.emulateMedia({ colorScheme: 'dark' })Custom axe‑core integration or manual RGB checksYes (browser contexts)Low‑MediumWeb apps needing cross‑browser
CypressJS/TScy.viewport('preset') + cy.injectAxe()axe‑core pluginLimited (single browser)LowTeams already using Cypress
SeleniumJS, Java, Python, C#, RubyExecute script to toggle CSS class or prefer‑color‑schemeaxe‑selenium or manualYes (Selenium Grid)MediumLegacy suites, broad language support
AppiumJS, Java, Python, Ruby, C#adb shell cmd uimode night yes (Android) or UIAppearance (iOS)UIAutomator/Accessibility inspector + custom contrastYes (parallel sessions)Medium‑HighNative Android/iOS apps
EspressoJava/KotlinUiModeManager#setNightModeAccessibilityTest + customYes (Android Test Orchestrator)HighAndroid‑only, fast UI tests

When your stack already includes a framework for functional testing, extend it rather than introduce a new one. For example, if you run Cypress for regression, add the cypress-axe plugin and a custom command to emulate dark mode. If you rely on Selenium Grid for cross‑browser tests, wrap the theme switch in a helper method that injects a media query override.

Minimal Setup Example: Playwright (Python)


# dark_mode_test.py
from playwright.sync_api import expect, sync_playwright
import axe_core_playwright  # hypothetical wrapper; replace with actual axe integration

def test_contrast_in_demo_page():
    with sync_playwright() as p:
        browser = p.chromium.launch(headless=True)
        context = browser.new_context()
        page = context.new_page()
        # Emulate dark color scheme
        page.emulate_media_color_scheme('dark')
        page.navigate('https://example.com/dashboard')
        # Wait for main content to appear
        page.wait_for_selector('main')
        # Run axe for contrast violations
        results = axe_core_playwright.run(page)
        violations = [v for v in results['violations'] if v['id'] == 'color-contrast']
        assert len(violations) == 0, f"Contrast violations: {violations}"
        context.close()
        browser.close()

The snippet shows three essential actions: emulating the media feature, waiting for stable DOM, and invoking an accessibility engine to flag contrast problems. Replace axe_core_playwright.run with the actual axe‑core API for your language; the principle stays identical.

How to Automate Dark Mode Testing (Step-by-Step) – Writing Stable Tests

Stability hinges on three pillars: deterministic state, reliable locators, and explicit synchronization. Dark mode introduces an extra dimension of state (the theme) that must be set before any interaction and cleared after each test to avoid bleed‑over.

Isolating Theme State

For web, the simplest approach is to override the prefers-color-scheme media feature via the testing framework. Avoid relying on a UI toggle that may itself be flaky; instead, force the theme at the context level. In mobile, use the platform’s UI mode commands before launching the app or, if the app reads a persisted setting, reset that setting in a setup() method.

#### Web (Playwright) Helper


def new_dark_context(browser):
    return browser.new_context(
        color_scheme='dark',
        viewport={'width': 1280, 'height': 800}
    )

#### Android (Appium) Helper


public void setDarkMode() throws Exception {
    // API 29+ uses UiModeManager
    driver.executeScript("mobile: shell", 
        ImmutableMap.of("command": "cmd", "args": List.of("uimode", "night", "yes")));
}

Always revert to the system default in a teardown hook so that subsequent tests start from a known baseline.

Writing Assertions That Survive Theme Changes

Avoid asserting on hard‑coded color values (e.g., rgb(30,30,30)). Instead, compute contrast ratios dynamically or rely on an accessibility library that understands the current color scheme. If you must check a specific brand color, retrieve the computed style after the theme is applied and compare against a tolerance.

#### Example: Dynamic Contrast Assertion (JavaScript, Playwright)


test('header meets WCAG AA in dark mode', async ({ page }) => {
    await page.emulateMedia({ colorScheme: 'dark' });
    await page.goto('/products');
    const header = page.locator('header');
    const bgColor = await header.evaluate(el => 
        getComputedStyle(el).backgroundColor);
    const textColor = await header.evaluate(el => 
        getComputedStyle(el).color);
    const contrast = await page.evaluate(({bg, txt}) => {
        // simple relative luminance formula; replace with a library if desired
        const lum = c => {
            const rgb = c.match(/\d+/g).map(Number);
            const [r,g,b] = rgb.map(v => {
                v /= 255;
                return v <= 0.03928 ? v/12.92 : Math.pow((v+0.055)/1.055, 2.4);
            });
            return 0.2126*r + 0.7152*g + 0.0722*b;
        };
        const L1 = lum(bg);
        const L2 = lum(txt);
        return (Math.max(L1,L2)+0.05)/(Math.min(L1,L2)+0.05);
    }, {bgColor, textColor});
    expect(contrast).toBeGreaterThanOrEqual(4.5); // AA for normal text
});

This test computes contrast on the fly, making it immune to future design token changes as long as the contrast requirement stays the same.

Locator Strategies for Dark Mode UI Elements

When a theme change alters visual properties but not the underlying DOM or view hierarchy, locators that rely on appearance (e.g., CSS classes that toggle colors) become brittle. Prefer locators based on semantic attributes, ARIA labels, or stable data‑test identifiers.

Recommended Locator Hierarchy

  1. data-testid (or test-id on Android/iOS) – explicit, immune to styling.
  2. ARIA role + accessible name – works for both web and native when accessibility is prioritized.
  3. CSS attribute selectors that target non‑visual properties (e.g., [type="submit"]).
  4. Text‑based locators – only as a last resort; combine with normalize-space() and consider i18n.

#### Example: Stable Locators in a React App


// Component
<button data-testid="submit-button" aria-label="Submit order">
    Submit
</button>

// Playwright test
await page.locator('[data-testid="submit-button"]').click();

If your design system does not yet ship test IDs, advocate for adding them as part of the definition of done for UI components. For mobile, use Espresso’s withId(R.id.submit_button) or Appium’s accessibility ID.

Handling Dynamic Class Names

Some frameworks (e.g., Tailwind, CSS‑in‑JS) generate hash‑based class names that change on each build. Avoid using them directly. Instead, rely on a parent element with a stable identifier and traverse downward using relative locators (page.locator('button >> text=Submit') in Playwright) or XPath axes that step from a known anchor.

Handling Waits, Timing, and Flakiness in Dark Mode Tests

Flaky tests often stem from race conditions between theme application and UI rendering. The browser or device may apply the new color scheme asynchronously, causing elements to momentarily retain the old colors. Mitigate this by waiting for a visual cue that the theme has taken effect before interacting with the page.

Waiting for Theme Application

#### Web: Wait for a CSS Variable Change

Many design systems expose a CSS custom property (e.g., --color-background) that flips when the theme changes. Wait for that property to reach its expected value.


def wait_for_dark_mode(page, timeout=5000):
    page.wait_for_function(
        """() => {
            const root = getComputedStyle(document.documentElement);
            return root.getPropertyValue('--color-background').trim() === 'rgb(10,10,10)';
        }""",
        timeout=timeout
    )

#### Mobile: Observe UI Mode Property

On Android, you can query the current night mode via UiModeManager#getNightMode. Wrap this in a polling loop.


public boolean isDarkMode(AndroidDriver driver) throws Exception {
    Object result = driver.executeScript("mobile: shell", 
        ImmutableMap.of("command": "cmd", "args": List.of("uimode", "night", "get")));
    return ((String) result).contains("night=yes");
}

// In test
await driver.wait(() => isDarkMode(driver), 5000);

Generic Flakiness Reduction Techniques

Data Setup, Teardown, and Environment Isolation

Dark mode tests frequently depend on user‑specific settings (e.g., saved theme preference) or feature flags that control which components receive dark‑mode styling. Treat these as test data that must be initialized and cleaned up.

Managing User Preferences

If your app stores the preferred theme in localStorage or SharedPreferences, set it explicitly before navigating to the target page.

#### Web Example (localStorage)


await page.context().addInitScript(() => {
    localStorage.setItem('theme', 'dark');
    // Optionally trigger a reload if the app reads storage on load
    window.location.reload();
});
await page.goto('/settings');

#### Android Example (SharedPreferences via ADB)


adb shell am broadcast -a com.example.app.SET_THEME --es theme dark

Or, if the app exposes a content provider, use ContentResolver to update the value.

Teardown Strategies

After each test, revert the preference to the system default or to a known baseline (e.g., light mode). This prevents cross‑test contamination.


# Playwright teardown hook
async def after_each(test_info):
    await test_info.page.context().clear_cookies()
    await test_info.page.context().add_init_script(
        "localStorage.removeItem('theme');"
    )

Environment Variables for CI

Parameterize the theme mode via an environment variable (TEST_THEME=dark) so the same test suite can run in both light and dark configurations without code changes. In your test runner, read the variable and apply the appropriate emulator/context settings.

Running Dark Mode Tests in CI Pipelines

Integrating dark mode verification into continuous integration guarantees that every commit is evaluated for contrast compliance and theme‑related bugs. The key is to parallelize test execution, cache dependencies, and publish actionable artifacts.

Pipeline Stages

  1. Install – retrieve language‑specific dependencies, install browsers (Playwright) or emulators (Android SDK).
  2. Build – compile the application if needed (e.g., APK, Docker image).
  3. Test – execute the test matrix (light + dark) across browsers/devices.
  4. Archive – store screenshots, videos, and accessibility reports.
  5. Report – publish a summary to the PR comment or dashboard.

Example: GitHub Actions Workflow (Playwright)


name: Dark Mode CI

on:
  push:
    branches: [ main ]
  pull_request:

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        browser: [ chromium, firefox, webkit ]
        theme: [ light, dark ]
    steps:
      - uses: actions/checkout@v3
      - name: Setup Node
        uses: actions/setup-node@v3
        with:
          node-version: '20'
      - name: Install deps
        run: npm ci
      - name: Install Playwright browsers
        run: npx playwright install --with-deps
      - name: Run tests
        env:
          TEST_THEME: ${{ matrix.theme }}
        run: npx playwright test --project=${{ matrix.browser }} --reporter=html
      - name: Upload report
        if: always()
        uses: actions/upload-artifact@v3
        with:
          name: playwright-report-${{ matrix.browser }}-${{ matrix.theme }}
          path: playwright-report/

This matrix runs each test suite twice (light and dark) across three browsers, yielding six parallel jobs. Adjust the TEST_THEME environment variable to drive the context creation inside your test code.

Mobile CI (GitLab CI with Firebase Test Lab)


dark_mode_android:
  image: openjdk:11
  variables:
    ANDROID_SDK_ROOT: /usr/local/android-sdk
  script:
    - echo "y" | $ANDROID_SDK_ROOT/tools/bin/sdkmanager "platforms;android-33" "emulator"
    - $ANDROID_SDK_ROOT/emulator/emulator -avd Pixel_4_API_33 -no-window -no-audio &
    - adb wait-for-device
    - adb shell cmd uimode night yes   # enforce dark mode
    - ./gradlew connectedAndroidTest   # runs Espresso/UIAutomator tests
  artifacts:
    when: always
    reports:
      junit: build/test-results/connected/androidTest/debug/*.xml
    paths:
      - build/outputs/screenshots/

The pipeline flips the device to night mode before launching the test suite, ensuring that all UI components start under dark conditions.

Reporting, Metrics, and Continuous Improvement

Raw pass/fail counts are insufficient for dark mode work; you need insight into contrast failures, theme‑switch latency, and regressions over time.

Capturing Contrast Violations

Integrate an accessibility engine (axe‑core, @accessibility‑insights/react, or Android’s AccessibilityTestFramework) that returns a list of violations. Filter for color-contrast and record the element selector, actual ratio, and required ratio.

#### Sample JSON Report Fragment


{
  "test": "checkout_page_dark",
  "status": "failed",
  "violations": [
    {
      "id": "color-contrast",
      "impact": "serious",
      "description": "Text does not meet contrast ratio of 4.5:1",
      "nodes": [
        { "target": ["#promo-banner .price"], "failureSummary": "Expected contrast 4.5, actual 3.2" }
      ]
    }
  ],
  "screenshot": "artifacts/checkout_page_dark_failure.png"
}

Store each run’s report in a time‑series database (e.g., Elasticsearch) or a simple CSV appended to an artifact bucket. Over time, you can chart the trend of contrast violations per release.

Metrics Dashboard

These metrics guide investment: if MTTD is high, tighten the feedback loop by running dark‑mode tests on pre‑merge branches; if flakiness is prevalent, revisit locator strategies and wait conditions.

Feedback Loop to Developers

Automatically post a comment on pull requests that summarizes new contrast violations, includes screenshots, and links to the full report. Provide a quick‑fix guide: “Ensure that text colors are derived from the --color-text CSS variable, not hard‑coded hex values.” Over time, developers internalize the pattern and produce fewer regressions.

Leveraging Autonomous Exploration to Bootstrap Dark Mode Automation (SUSA Mention)

Even with a solid test framework, writing the initial suite of dark‑mode checks can be time‑consuming, especially for large legacy applications. Autonomous QA platforms can explore the application without predefined scripts, discover reachable states, and generate baseline test artifacts that you then refine for dark mode.

How Autonomous Exploration Works

  1. Instrumentation – the platform injects a lightweight agent that records UI events, network calls, and state changes.
  2. Exploration – guided by personas (curated by personas (curious, impatient, novice, etc.), the agent traverses the app, toggling theme settings whenever it encounters a UI switch.
  3. Capture – for each visited screen, the agent saves a snapshot of the DOM/view hierarchy, a screenshot, and any accessibility violations detected.
  4. Export – the collected data is converted into executable test scripts (e.g., Playwright or Appium) and a data‑driven test matrix.

When you point such a tool at your staging build and instruct it to respect the prefers-color-scheme media flag (or Android night mode), it will automatically generate a set of dark‑mode test cases covering every reachable screen. You can then:

Practical Steps with SUSA

Assuming you have installed the SUSA CLI (pip install susatest-agent), run:


susatest explore \
  --url https://staging.example.com \
  --mode dark \
  --output ./darkmode-tests \
  --framework playwright \
  --personas curious,impatient,elderly

The command launches a headless browser, simulates the listed personas, forces dark mode via page.emulate_media_color_scheme('dark'), and writes a folder containing:

You now have a functional dark‑mode regression suite that you can commit, extend with additional assertions, and integrate into your CI pipeline. The autonomous phase eliminates the blank‑page problem and gives you a concrete starting point for manual refinement.

> Note: Mention of SUSA appears only here to illustrate how autonomous exploration can seed dark‑mode automation without turning the article into a product pitch.

Checklist for Reliable Dark Mode Automation

Use this list before marking a dark‑mode test suite as “ready for CI”.

✅ ItemWhy It Matters
Theme is forced at the context/device level, not via UI toggleGuarantees deterministic starting state
All locators use semantic attributes (data‑testid, ARIA, accessible name)Immune to theme‑driven class changes
Contrast assertions are computed dynamically or via axe‑corePasses even if design tokens evolve
Each test waits for a stable theme indicator before interactingEliminates race‑condition flakiness
Screenshots/videos are captured only on failureKeeps artifact storage manageable
Tests run in parallel for both light and dark modesProvides fast feedback
CI job publishes a structured report (JUnit, JSON, or SARIF)Enables trend tracking and PR comments
Test data (user preferences, feature flags) is reset in teardownPrevents cross‑test contamination
Flaky retries are limited to ≤2 attempts with back‑offMasks genuine instability without hiding it
Baseline test suite generated via autonomous exploration is reviewedEnsures generated code meets quality standards

Run through this checklist after each major UI refactor or when adding a new component set.

Closing Takeaways

Automating dark‑mode testing transforms a subjective, manual chore into an objective, repeatable gate that protects visual accessibility and brand consistency. Start by quantifying the cost of manual verification and the risk of contrast regressions; if the numbers justify investment, select a framework that already supports theme emulation (Playwright, Appium, or your existing Selenium/Cypress stack). Build tests around stable locators, explicit theme waits, and dynamic contrast assertions rather than brittle color values. Isolate theme state in setup and teardown, parameterize the mode via environment variables, and run the matrix in parallel across browsers or devices. Feed the results into a CI pipeline that archives screenshots, publishes accessibility reports, and surfaces new violations in pull‑request comments. Finally, consider using an autonomous exploration tool to generate an initial suite of dark‑mode scenarios, then refine those scripts with the patterns discussed here.

By following the step‑by‑step process outlined above, you will achieve reliable dark‑mode coverage that scales with your product, reduces regression escape rates, and frees your QA team to focus on the kinds of exploratory, human‑centric testing that no script can fully automate. The payoff is fewer late‑night hotfixes, higher user satisfaction, and a confidence ship‑dark that matches the confidence you already have in your light‑mode releases.

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