From 71c9b1453fec5568272dc838ff43779002a5e663 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E7=BF=94?= Date: Sat, 28 Feb 2026 16:35:05 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=88=9B=E5=BB=BA=E5=AE=8C=E6=95=B4?= =?UTF-8?q?=E7=9A=84WCAG=202.1=20AA=E5=8F=AF=E8=AE=BF=E9=97=AE=E6=80=A7?= =?UTF-8?q?=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加首页可访问性测试(严重违规、AA标准、ARIA标签、键盘导航、颜色对比度) - 添加联系页面可访问性测试(表单标签、ARIA属性、必填字段、键盘导航、触摸目标) - 添加响应式可访问性测试(移动端菜单、触摸目标、桌面端导航) - 添加颜色对比度测试(普通文本、大号文本) - 添加键盘导航测试(Tab导航、Enter激活、Escape关闭) - 添加屏幕阅读器兼容性测试(ARIA角色、ARIA标签、live region) - 添加可访问性最佳实践测试(页面标题、meta描述、favicon、跳过导航、焦点管理)~ --- .../tests/accessibility/accessibility.spec.ts | 458 ++++++++++++++++++ 1 file changed, 458 insertions(+) create mode 100644 e2e/src/tests/accessibility/accessibility.spec.ts diff --git a/e2e/src/tests/accessibility/accessibility.spec.ts b/e2e/src/tests/accessibility/accessibility.spec.ts new file mode 100644 index 0000000..0a38e1f --- /dev/null +++ b/e2e/src/tests/accessibility/accessibility.spec.ts @@ -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); + }); + }); +});