feat: 创建完整的WCAG 2.1 AA可访问性测试
- 添加首页可访问性测试(严重违规、AA标准、ARIA标签、键盘导航、颜色对比度) - 添加联系页面可访问性测试(表单标签、ARIA属性、必填字段、键盘导航、触摸目标) - 添加响应式可访问性测试(移动端菜单、触摸目标、桌面端导航) - 添加颜色对比度测试(普通文本、大号文本) - 添加键盘导航测试(Tab导航、Enter激活、Escape关闭) - 添加屏幕阅读器兼容性测试(ARIA角色、ARIA标签、live region) - 添加可访问性最佳实践测试(页面标题、meta描述、favicon、跳过导航、焦点管理)~
This commit is contained in:
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user