feat: 创建完整的WCAG 2.1 AA可访问性测试

- 添加首页可访问性测试(严重违规、AA标准、ARIA标签、键盘导航、颜色对比度)
- 添加联系页面可访问性测试(表单标签、ARIA属性、必填字段、键盘导航、触摸目标)
- 添加响应式可访问性测试(移动端菜单、触摸目标、桌面端导航)
- 添加颜色对比度测试(普通文本、大号文本)
- 添加键盘导航测试(Tab导航、Enter激活、Escape关闭)
- 添加屏幕阅读器兼容性测试(ARIA角色、ARIA标签、live region)
- 添加可访问性最佳实践测试(页面标题、meta描述、favicon、跳过导航、焦点管理)~
This commit is contained in:
张翔
2026-02-28 16:35:05 +08:00
parent efda131b8a
commit 71c9b1453f
@@ -0,0 +1,458 @@
import { test, expect } from '@playwright/test';
import { injectAxe, checkA11y, Violation } from '@axe-core/playwright';
import { HomePage } from '../../pages/HomePage';
import { ContactPage } from '../../pages/ContactPage';
import { ACCESSIBILITY_TEST_CASES } from '../../data/test-data';
test.describe('可访问性测试', () => {
test.describe('首页可访问性测试', () => {
let homePage: HomePage;
test.beforeEach(async ({ page }) => {
homePage = new HomePage(page);
await homePage.goto();
});
test('应该没有严重的可访问性违规', async () => {
await injectAxe(homePage.page);
const results = await checkA11y(homePage.page);
const criticalViolations = results.violations.filter(
v => v.impact === 'critical'
);
expect(criticalViolations.length).toBe(0);
});
test('应该没有严重的可访问性违规', async () => {
await injectAxe(homePage.page);
const results = await checkA11y(homePage.page);
const seriousViolations = results.violations.filter(
v => v.impact === 'serious'
);
expect(seriousViolations.length).toBe(0);
});
test('应该满足WCAG 2.1 AA标准', async () => {
await injectAxe(homePage.page);
const results = await checkA11y(homePage.page);
const wcagViolations = results.violations.filter(
v => v.tags.includes('wcag2a') || v.tags.includes('wcag21aa')
);
expect(wcagViolations.length).toBeLessThan(5);
});
test('所有图片应该有alt属性', async () => {
const accessibility = await homePage.verifyAccessibility();
expect(accessibility.hasAltText).toBe(true);
});
test('所有交互元素应该有ARIA标签', async () => {
const accessibility = await homePage.verifyAccessibility();
expect(accessibility.hasAriaLabels).toBe(true);
});
test('应该能够通过键盘导航', async () => {
const accessibility = await homePage.verifyAccessibility();
expect(accessibility.hasKeyboardNavigation).toBe(true);
});
test('应该满足颜色对比度要求', async () => {
const hasValidContrast = await homePage.verifyColorContrast();
expect(hasValidContrast).toBe(true);
});
test('导航链接应该有正确的focus状态', async () => {
const navLinks = homePage.page.locator('nav a');
const count = await navLinks.count();
for (let i = 0; i < count; i++) {
const link = navLinks.nth(i);
await link.focus();
const isFocused = await homePage.page.evaluate((el) =>
document.activeElement === el
);
expect(isFocused).toBe(true);
}
});
test('表单元素应该有正确的标签', async () => {
const inputs = homePage.page.locator('input, select, textarea');
const count = await inputs.count();
for (let i = 0; i < count; i++) {
const input = inputs.nth(i);
const hasLabel = await input.evaluate((el) => {
const id = (el as HTMLInputElement).id;
const label = document.querySelector(`label[for="${id}"]`);
return label !== null || (el as HTMLInputElement).labels.length > 0;
});
expect(hasLabel).toBe(true);
}
});
test('应该有正确的页面标题结构', async () => {
const h1Count = await homePage.page.locator('h1').count();
const h2Count = await homePage.page.locator('h2').count();
const h3Count = await homePage.page.locator('h3').count();
expect(h1Count).toBeLessThanOrEqual(1);
expect(h2Count).toBeGreaterThan(0);
expect(h3Count).toBeGreaterThan(0);
});
test('应该有正确的语言属性', async () => {
const html = homePage.page.locator('html');
const lang = await html.getAttribute('lang');
expect(lang).toBeTruthy();
expect(lang).toMatch(/^(zh|zh-CN|en-US)$/);
});
test('应该有正确的skip导航链接', async () => {
const skipLinks = homePage.page.locator('a[href^="#"]');
const count = await skipLinks.count();
for (let i = 0; i < count; i++) {
const link = skipLinks.nth(i);
const ariaLabel = await link.getAttribute('aria-label');
expect(ariaLabel).toBeTruthy();
}
});
});
test.describe('联系页面可访问性测试', () => {
let contactPage: ContactPage;
test.beforeEach(async ({ page }) => {
contactPage = new ContactPage(page);
await contactPage.goto();
});
test('应该没有严重的可访问性违规', async () => {
await injectAxe(contactPage.page);
const results = await checkA11y(contactPage.page);
const criticalViolations = results.violations.filter(
v => v.impact === 'critical'
);
expect(criticalViolations.length).toBe(0);
});
test('表单字段应该有正确的标签', async () => {
const labels = await contactPage.verifyFormLabels();
expect(labels.nameLabel).toBeTruthy();
expect(labels.emailLabel).toBeTruthy();
expect(labels.phoneLabel).toBeTruthy();
expect(labels.messageLabel).toBeTruthy();
});
test('表单字段应该有正确的ARIA属性', async () => {
const attributes = await contactPage.getFormAccessibilityAttributes();
expect(attributes.nameAriaLabel || attributes.submitAriaLabel).toBeTruthy();
expect(attributes.emailAriaLabel).toBeTruthy();
expect(attributes.phoneAriaLabel).toBeTruthy();
expect(attributes.messageAriaLabel).toBeTruthy();
});
test('必填字段应该有正确的标记', async () => {
const requiredFields = await contactPage.verifyRequiredFields();
expect(requiredFields.nameRequired).toBe(true);
expect(requiredFields.emailRequired).toBe(true);
expect(requiredFields.phoneRequired).toBe(true);
expect(requiredFields.messageRequired).toBe(true);
});
test('表单应该能够通过键盘导航', async () => {
const isAccessible = await contactPage.isFormKeyboardAccessible();
expect(isAccessible).toBe(true);
});
test('错误消息应该与相关字段关联', async () => {
await contactPage.fillContactForm({
name: '',
email: 'invalid-email',
phone: '123',
message: '',
});
await contactPage.submitForm();
await contactPage.waitForTimeout(1000);
const nameError = await contactPage.getNameError();
const emailError = await contactPage.getEmailError();
const phoneError = await contactPage.getPhoneError();
const messageError = await contactPage.getMessageError();
expect(nameError || emailError || phoneError || messageError).toBeTruthy();
});
test('提交按钮应该有正确的ARIA标签', async () => {
const attributes = await contactPage.getFormAccessibilityAttributes();
expect(attributes.submitAriaLabel).toBeTruthy();
});
test('表单应该有正确的autocomplete属性', async () => {
const autocomplete = await contactPage.getFormAutocompleteAttributes();
expect(autocomplete.nameAutocomplete).toBeTruthy();
expect(autocomplete.emailAutocomplete).toBeTruthy();
expect(autocomplete.phoneAutocomplete).toBeTruthy();
});
test('触摸目标应该足够大', async () => {
const touchTargets = contactPage.page.locator('button, a, input[type="submit"]');
const count = await touchTargets.count();
for (let i = 0; i < count; i++) {
const target = touchTargets.nth(i);
const boundingBox = await target.boundingBox();
if (boundingBox) {
const minSize = ACCESSIBILITY_TEST_CASES.touchTargetSize;
expect(boundingBox.width).toBeGreaterThanOrEqual(minSize);
expect(boundingBox.height).toBeGreaterThanOrEqual(minSize);
}
}
});
});
test.describe('响应式可访问性测试', () => {
test('移动端应该有可访问的菜单', async ({ page }) => {
const homePage = new HomePage(page);
await homePage.page.setViewportSize({ width: 375, height: 667 });
await homePage.goto();
const isMobileMenuAccessible = await homePage.verifyMobileMenu();
expect(isMobileMenuAccessible).toBe(true);
});
test('移动端触摸目标应该足够大', async ({ page }) => {
const homePage = new HomePage(page);
await homePage.page.setViewportSize({ width: 375, height: 667 });
await homePage.goto();
const touchTargets = homePage.page.locator('button, a');
const count = await touchTargets.count();
for (let i = 0; i < count; i++) {
const target = touchTargets.nth(i);
const boundingBox = await target.boundingBox();
if (boundingBox) {
expect(boundingBox.width).toBeGreaterThanOrEqual(44);
expect(boundingBox.height).toBeGreaterThanOrEqual(44);
}
}
});
test('桌面端应该有可访问的导航', async ({ page }) => {
const homePage = new HomePage(page);
await homePage.page.setViewportSize({ width: 1280, height: 720 });
await homePage.goto();
const isNavigationAccessible = await homePage.navigation.isVisible();
expect(isNavigationAccessible).toBe(true);
});
});
test.describe('颜色对比度测试', () => {
test('普通文本应该满足4.5:1对比度', async ({ page }) => {
const homePage = new HomePage(page);
await homePage.goto();
const textElements = homePage.page.locator('p, h1, h2, h3');
const count = await textElements.count();
for (let i = 0; i < count; i++) {
const element = textElements.nth(i);
const backgroundColor = await element.evaluate((el) =>
window.getComputedStyle(el).backgroundColor
);
const color = await element.evaluate((el) =>
window.getComputedStyle(el).color
);
if (backgroundColor !== 'rgba(0, 0, 0, 0)' && color !== 'rgba(0, 0, 0, 0)') {
expect(backgroundColor).not.toBe(color);
}
}
});
test('大号文本应该满足3.0:1对比度', async ({ page }) => {
const homePage = new HomePage(page);
await homePage.goto();
const largeTextElements = homePage.page.locator('h1, h2');
const count = await largeTextElements.count();
for (let i = 0; i < count; i++) {
const element = largeTextElements.nth(i);
const backgroundColor = await element.evaluate((el) =>
window.getComputedStyle(el).backgroundColor
);
const color = await element.evaluate((el) =>
window.getComputedStyle(el).color
);
if (backgroundColor !== 'rgba(0, 0, 0, 0)' && color !== 'rgba(0, 0, 0, 0)') {
expect(backgroundColor).not.toBe(color);
}
}
});
});
test.describe('键盘导航测试', () => {
test('应该能够使用Tab键导航', async ({ page }) => {
const homePage = new HomePage(page);
await homePage.goto();
const focusableElements = homePage.page.locator(
'a[href], button, input, select, textarea'
);
const count = await focusableElements.count();
for (let i = 0; i < count; i++) {
await homePage.pressKey('Tab');
const activeElement = await homePage.page.evaluate(() =>
document.activeElement?.tagName
);
expect(['A', 'BUTTON', 'INPUT', 'SELECT', 'TEXTAREA']).toContain(activeElement);
}
});
test('应该能够使用Enter键激活链接', async ({ page }) => {
const homePage = new HomePage(page);
await homePage.goto();
const firstLink = homePage.page.locator('a[href]').first();
await firstLink.focus();
await homePage.pressKey('Enter');
await homePage.waitForTimeout(1000);
const currentURL = homePage.getCurrentURL();
expect(currentURL).not.toBe('/');
});
test('应该能够使用Escape键关闭模态框', async ({ page }) => {
const homePage = new HomePage(page);
await homePage.goto();
await homePage.openMobileMenu();
await homePage.pressKey('Escape');
const isMenuVisible = await homePage.mobileMenu.isVisible();
expect(isMenuVisible).toBe(false);
});
});
test.describe('屏幕阅读器兼容性测试', () => {
test('应该有正确的ARIA角色', async ({ page }) => {
const homePage = new HomePage(page);
await homePage.goto();
const navigation = homePage.navigation;
const role = await navigation.getAttribute('role');
expect(role).toBe('navigation');
const main = homePage.page.locator('main');
const mainRole = await main.getAttribute('role');
expect(mainRole).toBe('main');
const footer = homePage.footer;
const footerRole = await footer.getAttribute('role');
expect(footerRole).toBe('contentinfo');
});
test('应该有正确的ARIA标签', async ({ page }) => {
const homePage = new HomePage(page);
await homePage.goto();
const logo = homePage.logo;
const altText = await logo.getAttribute('alt');
expect(altText).toBeTruthy();
const contactButton = homePage.page.locator('a:has-text("立即咨询")');
const ariaLabel = await contactButton.getAttribute('aria-label');
expect(ariaLabel).toBeTruthy();
});
test('应该有正确的live region', async ({ page }) => {
const homePage = new HomePage(page);
await homePage.goto();
const liveRegions = homePage.page.locator('[aria-live]');
const count = await liveRegions.count();
for (let i = 0; i < count; i++) {
const region = liveRegions.nth(i);
const polite = await region.getAttribute('aria-live');
expect(['polite', 'assertive', 'off']).toContain(polite || '');
}
});
});
test.describe('可访问性最佳实践测试', () => {
test('应该有正确的页面标题', async ({ page }) => {
const homePage = new HomePage(page);
await homePage.goto();
const title = await homePage.getTitle();
expect(title).toBeTruthy();
expect(title.length).toBeGreaterThan(10);
expect(title.length).toBeLessThan(60);
});
test('应该有正确的meta描述', async ({ page }) => {
const homePage = new HomePage(page);
await homePage.goto();
const metaDescription = await homePage.page.evaluate(() => {
const meta = document.querySelector('meta[name="description"]');
return meta ? meta.getAttribute('content') : null;
});
expect(metaDescription).toBeTruthy();
expect(metaDescription!.length).toBeGreaterThan(50);
expect(metaDescription!.length).toBeLessThan(160);
});
test('应该有正确的favicon', async ({ page }) => {
const homePage = new HomePage(page);
await homePage.goto();
const favicon = await homePage.page.evaluate(() => {
const link = document.querySelector('link[rel="icon"]');
return link ? link.getAttribute('href') : null;
});
expect(favicon).toBeTruthy();
});
test('应该有正确的跳过导航链接', async ({ page }) => {
const homePage = new HomePage(page);
await homePage.goto();
const skipLink = homePage.page.locator('a[href="#main"]');
const isVisible = await skipLink.isVisible();
expect(isVisible).toBe(true);
});
test('应该有正确的焦点管理', async ({ page }) => {
const homePage = new HomePage(page);
await homePage.goto();
const initialFocus = await homePage.page.evaluate(() =>
document.activeElement?.tagName
);
await homePage.pressKey('Tab');
const afterTabFocus = await homePage.page.evaluate(() =>
document.activeElement?.tagName
);
expect(initialFocus).not.toBe(afterTabFocus);
});
});
});