Visual Regression for Adaptive Interfaces: Testing That Crisis Mode Actually Looks Different

Published: (December 13, 2025 at 09:00 AM EST)
5 min read
Source: Dev.to

Source: Dev.to

Introduction

State management is easy to test, but visual transformation isn’t. In a crisis, what the user actually sees is the only thing that matters. This post explains how to verify that emergency (or “crisis”) mode isn’t just flipping booleans—it truly changes the experience.

What unit tests can verify

  • ✅ State updated correctly
  • ✅ CSS classes applied
  • ✅ Component rendered

What unit tests cannot verify

  • ❌ Button actually looks bigger
  • ❌ Contrast ratio actually increased
  • ❌ Layout actually simplified
  • ❌ Text actually readable

The gap between “test passed” and “user helped” is visual. Screenshot testing bridges that gap.

Visual Regression Technique

Capture the same component in normal mode and crisis mode, then verify they’re meaningfully different.

import { test, expect } from '@playwright/test';

test.describe('Crisis Mode Visual Transformations', () => {
  test('emergency mode visually differs from normal mode', async ({ page }) => {
    // Capture normal state
    await page.goto('/pain-entry');
    const normalScreenshot = await page.screenshot();

    // Activate crisis mode
    await page.evaluate(() => {
      window.dispatchEvent(new CustomEvent('activate-crisis-mode', {
        detail: { severity: 'emergency' }
      }));
    });

    // Wait for visual transition
    await page.waitForTimeout(500); // Allow CSS transitions

    // Capture crisis state
    const crisisScreenshot = await page.screenshot();

    // Verify they're different (this is the key assertion)
    expect(Buffer.compare(normalScreenshot, crisisScreenshot)).not.toBe(0);
  });

  test('emergency mode matches approved baseline', async ({ page }) => {
    await page.goto('/pain-entry?crisis=emergency');

    await expect(page).toHaveScreenshot('emergency-mode-pain-entry.png', {
      maxDiffPixels: 100, // Allow minor anti-aliasing differences
    });
  });
});

Component‑level feedback

test.describe('Touch Target Scaling', () => {
  test('buttons scale up in emergency mode', async ({ page }) => {
    await page.goto('/component-preview/button');

    // Normal size
    await expect(page.locator('[data-testid="primary-button"]'))
      .toHaveScreenshot('button-normal.png');

    // Emergency size
    await page.evaluate(() => {
      document.documentElement.setAttribute('data-crisis-mode', 'emergency');
    });

    await expect(page.locator('[data-testid="primary-button"]'))
      .toHaveScreenshot('button-emergency.png');
  });
});

Handling Multiple Adaptive States

Standard visual regression uses a single baseline per test, but adaptive interfaces have many valid states. For a pain‑tracker button, valid combinations include:

ModeContrastFont SizeTouch TargetsBaseline
NormalNormalMediumStandardA
NormalHighMediumStandardB
CrisisNormalLargeExtra‑LargeC
CrisisHighLargeExtra‑LargeD

Matrix testing example

const PREFERENCE_MATRIX = [
  { mode: 'normal', contrast: 'normal', fontSize: 'medium' },
  { mode: 'normal', contrast: 'high',   fontSize: 'medium' },
  { mode: 'normal', contrast: 'normal', fontSize: 'large' },
  { mode: 'crisis', contrast: 'normal', fontSize: 'large' },
  { mode: 'crisis', contrast: 'high',   fontSize: 'large' },
];

for (const prefs of PREFERENCE_MATRIX) {
  test(`pain entry form - ${prefs.mode}/${prefs.contrast}/${prefs.fontSize}`, async ({ page }) => {
    // Set preferences via URL params or localStorage
    await page.goto(
      `/pain-entry?mode=${prefs.mode}&contrast=${prefs.contrast}&fontSize=${prefs.fontSize}`
    );

    // Each combination has its own baseline
    const baselineName = `pain-entry-${prefs.mode}-${prefs.contrast}-${prefs.fontSize}.png`;
    await expect(page).toHaveScreenshot(baselineName);
  });
}

Storybook & Chromatic

If you use Storybook, Chromatic can handle matrix testing elegantly.

// Button.stories.tsx
export default {
  title: 'Components/Button',
  component: Button,
};

export const Normal = () => Save Entry;

export const Emergency = () => (
  
    Save Entry
  
);

export const HighContrast = () => (
  
    Save Entry
  
);

export const EmergencyHighContrast = () => (
  
    
      Save Entry
    
  
);

Chromatic captures each story as a separate baseline, ensuring visual changes are intentional.

Accessibility Structure Checks

Visual changes should preserve semantic meaning. The following tests verify that the DOM structure adapts correctly.

test.describe('Crisis Mode Accessibility Structure', () => {
  test('normal mode has full navigation structure', async ({ page }) => {
    await page.goto('/pain-entry');

    await expect(page.locator('main')).toMatchAriaSnapshot(`
      - main:
        - heading "Log Pain Entry" [level=1]
        - navigation "Entry sections":
          - link "Basic Info"
          - link "Location"
          - link "Symptoms"
          - link "Triggers"
          - link "Medications"
        - form "Pain entry form":
          - group "Pain Level":
            - slider "Pain intensity"
          - group "Location":
            - button "Select body areas"
          - group "Notes":
            - textbox "Additional notes"
        - button "Save Entry"
        - button "Cancel"
    `);
  });

  test('emergency mode simplifies to essentials', async ({ page }) => {
    await page.goto('/pain-entry?crisis=emergency');

    // Emergency mode should have FEWER elements, not more
    await expect(page.locator('main')).toMatchAriaSnapshot(`
      - main:
        - heading "Quick Pain Log" [level=1]
        - form "Simplified pain entry":
          - group "How bad?":
            - slider "Pain level"
          - group "Where?":
            - button "Tap body location"
        - button "Save Now"
        - button "Need Help?"
    `);
  });
});

test('emergency mode reduces cognitive load', async ({ page }) => {
  // Count interactive elements in normal mode
  await page.goto('/pain-entry');
  const normalInteractive = await page
    .locator('[role="button"], [role="link"], input, select, textarea')
    .count();

  // Count in emergency mode
  await page.goto('/pain-entry?crisis=emergency');
  const emergencyInteractive = await page
    .locator('[role="button"], [role="link"], input, select, textarea')
    .count();

  // Emergency mode should have fewer interactive elements
  expect(emergencyInteractive).toBeLessThan(normalInteractive);

  // But not zero – core functionality must remain
  expect(emergencyInteractive).toBeGreaterThan(2);
});

Contrast Verification

High‑contrast mode is meaningless if the contrast ratios don’t meet WCAG thresholds. Automated tests can verify this.

import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';

test.describe('Contrast Verification', () => {
  test('normal mode meets WCAG AA (4.5:1)', async ({ page }) => {
    await page.goto('/pain-entry');

    const results = await new AxeBuilder({ page })
      .withRules(['color-contrast'])
      .analyze();

    expect(results.violations).toHaveLength(0);
  });

  test('high contrast mode exceeds WCAG AAA (7:1)', async ({ page }) => {
    await page.goto('/pain-entry?contrast=high');

    const results = await new AxeBuilder({ page })
      .withRules(['color-contrast-enhanced']) // AAA level
      .analyze();

    expect(results.violations).toHaveLength(0);
  });
});

Conclusion

Visual regression testing is essential for adaptive interfaces, especially when “crisis mode” must reliably convey larger touch targets, higher contrast, and simplified layouts. By combining screenshot comparisons, matrix testing, Storybook/Chromatic baselines, ARIA structure snapshots, and contrast analysis, you can ensure that the UI not only flips state flags but also delivers a genuinely safer experience for users in distress.

Back to Blog

Related posts

Read more »