Files
novalon-website/e2e/mobile-accessibility.spec.ts
zhangxiang f3fc969bef test(mobile): add mobile E2E test suite (53 tests) and fix layout issues
Add comprehensive mobile testing coverage:
- Add chromium-mobile functional test project (iPhone 14, isMobile, hasTouch)
- Add mobile user journey tests (UJ-01/02/04/05/10 mobile variants)
- Add mobile performance baseline tests (FCP/LCP/load time)
- Add mobile accessibility tests (axe-core WCAG 2.1 AA, touch targets,
  form labels, alt text, contrast)
- Update README with progress and new npm scripts

Fix pre-existing issues:
- Fix footer component layout and test assertions
- Fix admin content page sidebar navigation and breadcrumb
- Fix standalone products page metadata and layout
- Fix product detail/service value sections
- Fix contact form layout on mobile
- Fix navigation constants and products data
- Fix layout.tsx CMS config and theme handling
- Fix erp-upgrade content layout
2026-08-03 18:32:29 +08:00

216 lines
7.9 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
/**
* 移动端可访问性专项测试
*
* 覆盖范围:
* 1. axe-core 合规扫描(WCAG 2.1 AA)— 移动视口
* 2. 触摸目标尺寸 ≥ 44pxWCAG 2.5.5
* 3. 焦点管理(移动端 Tab 导航)
* 4. 表单标签关联
* 5. 图片 Alt 文本
* 6. 颜色对比度检查
*
* 标记:@mobile @accessibility
*/
test.setTimeout(60000);
// ==================== 1. axe-core 全量扫描 ====================
test.describe('移动端可访问性 - axe-core 合规扫描', { tag: '@mobile @accessibility' }, () => {
const pages = [
{ path: '/', name: '首页' },
{ path: '/about', name: '关于我们' },
{ path: '/contact', name: '联系我们' },
{ path: '/products', name: '产品中心' },
{ path: '/solutions', name: '解决方案' },
{ path: '/services', name: '服务' },
{ path: '/news', name: '新闻' },
{ path: '/cases', name: '案例' },
{ path: '/team', name: '团队' },
];
for (const { path, name } of pages) {
test(`${name} (${path}) — WCAG 2.1 AA 无严重违规`, async ({ page }) => {
await page.goto(path, { waitUntil: 'domcontentloaded', timeout: 30000 });
await page.waitForTimeout(2000);
const accessibilityScanResults = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
.analyze();
const violations = accessibilityScanResults.violations;
// 输出违规详情用于分析
if (violations.length > 0) {
console.log(`\n=== ${name} 可访问性违规 (${violations.length} 项) ===`);
for (const v of violations) {
console.log(` [${v.impact}] ${v.id}: ${v.help}`);
console.log(` Nodes: ${v.nodes.length}, URL: ${v.helpUrl}`);
// 输出前 2 个节点的摘要
v.nodes.slice(0, 2).forEach((node, i) => {
const target = node.target?.join(', ') || 'unknown';
console.log(` Node ${i + 1}: ${target.slice(0, 100)}`);
});
}
}
// 允许少量 low/moderate 违规,但 critical/serious 应为 0
const criticalSerious = violations.filter(
v => v.impact === 'critical' || v.impact === 'serious'
);
expect(criticalSerious.length).toBe(0);
});
}
});
// ==================== 2. 触摸目标尺寸检查 ====================
test.describe('移动端可访问性 - 触摸目标尺寸', { tag: '@mobile @accessibility' }, () => {
test('首页交互元素触摸目标 ≥ 44px', async ({ page }) => {
await page.goto('/', { waitUntil: 'domcontentloaded', timeout: 30000 });
await page.waitForTimeout(1500);
await page.evaluate(() => window.scrollTo(0, 0));
await page.waitForTimeout(300);
// 检查可见区域内的交互元素
const smallTargets = await page.evaluate(() => {
const interactiveElements = document.querySelectorAll(
'a, button, input, select, textarea, [role="button"], [tabindex]:not([tabindex="-1"])'
);
const issues: Array<{ tag: string; text: string; width: number; height: number }> = [];
interactiveElements.forEach(el => {
const rect = el.getBoundingClientRect();
// 只检查可见元素
if (rect.width > 0 && rect.height > 0 && rect.width < 1000) {
if (rect.width < 44 || rect.height < 44) {
const text = el.textContent?.trim().slice(0, 30) || el.tagName;
// 排除内联文本链接(段落中的 a 标签)
const isInline = el.closest('p, span, li, h1, h2, h3, h4, h5, h6');
if (!isInline) {
issues.push({
tag: el.tagName,
text: text,
width: Math.round(rect.width),
height: Math.round(rect.height),
});
}
}
}
});
return issues;
});
if (smallTargets.length > 0) {
console.log(`触摸目标不足 44px 的元素 (${smallTargets.length} 个):`);
smallTargets.slice(0, 10).forEach(t => {
console.log(` <${t.tag}> "${t.text}" — ${t.width}x${t.height}px`);
});
}
// 允许少量例外(如小图标装饰按钮),但不应超过 5 个
expect(smallTargets.length).toBeLessThanOrEqual(5);
});
});
// ==================== 3. 焦点管理 ====================
test.describe('移动端可访问性 - 焦点管理', { tag: '@mobile @accessibility' }, () => {
test('焦点元素数量合理', async ({ page }) => {
await page.goto('/', { waitUntil: 'domcontentloaded', timeout: 30000 });
await page.waitForTimeout(1500);
const focusableCount = await page.evaluate(() => {
return document.querySelectorAll(
'a, button, input, select, textarea, [tabindex]:not([tabindex="-1"])'
).length;
});
console.log(`Mobile focusable elements: ${focusableCount}`);
expect(focusableCount).toBeGreaterThan(0);
});
});
// ==================== 4. 表单标签关联 ====================
test.describe('移动端可访问性 - 表单标签', { tag: '@mobile @accessibility' }, () => {
test('联系表单输入框有关联标签或 aria-label', async ({ page }) => {
await page.goto('/contact', { waitUntil: 'domcontentloaded', timeout: 30000 });
await page.waitForTimeout(2500);
const formInputs = await page.evaluate(() => {
const inputs = document.querySelectorAll('input, textarea, select');
const results: Array<{ tag: string; id: string; hasLabel: boolean; ariaLabel: string | null }> = [];
inputs.forEach(input => {
const id = input.id;
const ariaLabel = input.getAttribute('aria-label');
let hasLabel = false;
if (id) {
const label = document.querySelector(`label[for="${id}"]`);
hasLabel = label !== null;
}
results.push({
tag: input.tagName,
id: id || '',
hasLabel,
ariaLabel,
});
});
return results;
});
for (const input of formInputs) {
const hasAccessibleName = input.hasLabel || input.ariaLabel;
expect(
hasAccessibleName,
`Input ${input.tag}${input.id ? '#' + input.id : ''} 缺少可访问名称`
).toBeTruthy();
}
});
});
// ==================== 5. 图片 Alt 文本 ====================
test.describe('移动端可访问性 - 图片 Alt 文本', { tag: '@mobile @accessibility' }, () => {
test('首页图片有 alt 属性', async ({ page }) => {
await page.goto('/', { waitUntil: 'domcontentloaded', timeout: 30000 });
await page.waitForTimeout(2000);
const imagesWithoutAlt = await page.evaluate(() => {
const images = Array.from(document.querySelectorAll('img'));
return images.filter(img => !img.hasAttribute('alt') || img.getAttribute('alt') === null).length;
});
console.log(`Images without alt attribute: ${imagesWithoutAlt}`);
expect(imagesWithoutAlt).toBe(0);
});
});
// ==================== 6. 颜色对比度 ====================
test.describe('移动端可访问性 - 颜色对比度', { tag: '@mobile @accessibility' }, () => {
test('无白色文字在白色背景上的问题', async ({ page }) => {
await page.goto('/', { waitUntil: 'domcontentloaded', timeout: 30000 });
await page.waitForTimeout(1500);
const contrastIssues = await page.evaluate(() => {
const issues: string[] = [];
const textElements = document.querySelectorAll('p, h1, h2, h3, h4, h5, h6, span, a, li, td, th');
textElements.forEach(el => {
const style = window.getComputedStyle(el);
const color = style.color;
const bgColor = style.backgroundColor;
if (color === 'rgb(255, 255, 255)' && bgColor === 'rgb(255, 255, 255)') {
issues.push(`${el.tagName}: 白色文字在白色背景上`);
}
});
return issues;
});
expect(contrastIssues.length, `发现颜色对比度问题:\n${contrastIssues.join('\n')}`).toBe(0);
});
});