自适应界面的视觉回归:测试危机模式实际看起来不同
Source: Dev.to
介绍
状态管理容易测试,但视觉转换却不易。在危机情况下,用户实际看到的内容才是唯一重要的。本文说明如何验证紧急(或“危机”)模式不仅仅是翻转布尔值,而是真正改变了用户体验。
单元测试可以验证的内容
- ✅ 状态正确更新
- ✅ 正确应用 CSS 类
- ✅ 组件已渲染
单元测试无法验证的内容
- ❌ 按钮实际看起来更大
- ❌ 对比度实际提升
- ❌ 布局实际简化
- ❌ 文本实际可读
“测试通过”和“帮助用户”之间的差距是视觉上的。截图测试弥合了这道鸿沟。
可视回归技术
在普通模式和危机模式下捕获相同组件的截图,然后验证它们在视觉上有意义的差异。
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
});
});
});
组件级反馈
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');
});
});
处理多种自适应状态
标准的可视回归对每个测试只使用单一基准,但自适应界面有许多有效状态。以疼痛追踪按钮为例,有效组合包括:
| 模式 | 对比度 | 字体大小 | 触控目标 | 基准 |
|---|---|---|---|---|
| 正常 | 正常 | 中等 | 标准 | A |
| 正常 | 高 | 中等 | 标准 | B |
| 危机 | 正常 | 大 | 超大 | C |
| 危机 | 高 | 大 | 超大 | D |
| … | … | … | … | … |
矩阵测试示例
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
如果你使用 Storybook,Chromatic 可以优雅地处理矩阵测试。
// 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 将每个 story 捕获为单独的基准,确保视觉变化是有意为之。
可访问性结构检查
视觉变化应保留语义含义。以下测试验证 DOM 结构是否正确适配。
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);
});
对比度验证
如果对比度比例未达到 WCAG 阈值,高对比模式就毫无意义。自动化测试可以验证这一点。
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);
});
});
结论
可视回归测试对自适应界面至关重要,尤其是在“危机模式”必须可靠地呈现更大的触控目标、更高的对比度和简化布局时。通过结合截图比较、矩阵测试、Storybook/Chromatic 基准、ARIA 结构快照以及对比度分析,你可以确保 UI 不仅翻转状态标记,还能为处于困境的用户提供真正更安全的体验。