feat: 优化网站性能、响应式设计和测试覆盖率
- 更新next.config.ts配置以优化图片和静态资源 - 优化字体加载策略,减少首屏阻塞 - 使用Next.js Image组件替换img标签并实现懒加载 - 重构移动端菜单交互,提升触摸体验 - 新增安全测试和可访问性测试用例 - 修复导航栏滚动定位问题 - 更新部署就绪测试脚本 - 添加相关文档说明优化细节
This commit is contained in:
@@ -1,458 +1,332 @@
|
||||
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.describe('可访问性测试 @accessibility', () => {
|
||||
test('页面应该有lang属性', async ({ page }) => {
|
||||
await page.goto('http://localhost:3000');
|
||||
|
||||
const html = page.locator('html');
|
||||
await expect(html).toHaveAttribute('lang', 'zh-CN');
|
||||
});
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
homePage = new HomePage(page);
|
||||
await homePage.goto();
|
||||
});
|
||||
test('页面应该有正确的标题层级', async ({ page }) => {
|
||||
await page.goto('http://localhost:3000');
|
||||
|
||||
const headings = page.locator('h1, h2, h3, h4, h5, h6');
|
||||
const count = await headings.count();
|
||||
|
||||
expect(count).toBeGreaterThan(0);
|
||||
|
||||
const firstHeading = headings.first();
|
||||
const firstTag = await firstHeading.evaluate(el => el.tagName.toLowerCase());
|
||||
expect(firstTag).toBe('h1');
|
||||
});
|
||||
|
||||
test('应该没有严重的可访问性违规', async () => {
|
||||
await injectAxe(homePage.page);
|
||||
const results = await checkA11y(homePage.page);
|
||||
test('所有图片应该有alt属性', async ({ page }) => {
|
||||
await page.goto('http://localhost:3000');
|
||||
|
||||
const images = page.locator('img');
|
||||
const count = await images.count();
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const img = images.nth(i);
|
||||
const alt = await img.getAttribute('alt');
|
||||
expect(alt).toBeTruthy();
|
||||
expect(alt?.length).toBeGreaterThan(0);
|
||||
}
|
||||
});
|
||||
|
||||
test('表单输入应该有label', async ({ page }) => {
|
||||
await page.goto('http://localhost:3000/contact');
|
||||
|
||||
const inputs = page.locator('input, textarea, select');
|
||||
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.getAttribute('id');
|
||||
const ariaLabel = el.getAttribute('aria-label');
|
||||
const ariaLabelledBy = el.getAttribute('aria-labelledby');
|
||||
const hasLabelFor = id && document.querySelector(`label[for="${id}"]`);
|
||||
const hasParentLabel = el.closest('label');
|
||||
|
||||
return !!(ariaLabel || ariaLabelledBy || hasLabelFor || hasParentLabel);
|
||||
});
|
||||
|
||||
const criticalViolations = results.violations.filter(
|
||||
v => v.impact === 'critical'
|
||||
);
|
||||
expect(hasLabel).toBeTruthy();
|
||||
}
|
||||
});
|
||||
|
||||
test('按钮应该有可访问的名称', async ({ page }) => {
|
||||
await page.goto('http://localhost:3000');
|
||||
|
||||
const buttons = page.locator('button, [role="button"]');
|
||||
const count = await buttons.count();
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const button = buttons.nth(i);
|
||||
const hasAccessibleName = await button.evaluate(el => {
|
||||
const text = el.textContent?.trim();
|
||||
const ariaLabel = el.getAttribute('aria-label');
|
||||
const ariaLabelledBy = el.getAttribute('aria-labelledby');
|
||||
const title = el.getAttribute('title');
|
||||
|
||||
return !!(text || ariaLabel || ariaLabelledBy || title);
|
||||
});
|
||||
|
||||
expect(criticalViolations.length).toBe(0);
|
||||
});
|
||||
expect(hasAccessibleName).toBeTruthy();
|
||||
}
|
||||
});
|
||||
|
||||
test('应该没有严重的可访问性违规', async () => {
|
||||
await injectAxe(homePage.page);
|
||||
const results = await checkA11y(homePage.page);
|
||||
test('链接应该有描述性文本', async ({ page }) => {
|
||||
await page.goto('http://localhost:3000');
|
||||
|
||||
const links = page.locator('a[href]').first(10);
|
||||
const count = await links.count();
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const link = links.nth(i);
|
||||
const hasDescriptiveText = await link.evaluate(el => {
|
||||
const text = el.textContent?.trim();
|
||||
const ariaLabel = el.getAttribute('aria-label');
|
||||
const title = el.getAttribute('title');
|
||||
const hasImg = el.querySelector('img[alt]');
|
||||
|
||||
return !!(text || ariaLabel || title || hasImg);
|
||||
});
|
||||
|
||||
const seriousViolations = results.violations.filter(
|
||||
v => v.impact === 'serious'
|
||||
);
|
||||
expect(hasDescriptiveText).toBeTruthy();
|
||||
}
|
||||
});
|
||||
|
||||
test('焦点元素应该可见', async ({ page }) => {
|
||||
await page.goto('http://localhost:3000');
|
||||
|
||||
const focusableElements = page.locator('a[href], button, input, textarea, select, [tabindex]:not([tabindex="-1"])');
|
||||
const count = await focusableElements.count();
|
||||
|
||||
for (let i = 0; i < Math.min(count, 10); i++) {
|
||||
const element = focusableElements.nth(i);
|
||||
await element.focus();
|
||||
|
||||
expect(seriousViolations.length).toBe(0);
|
||||
});
|
||||
|
||||
test('应该满足WCAG 2.1 AA标准', async () => {
|
||||
await injectAxe(homePage.page);
|
||||
const results = await checkA11y(homePage.page);
|
||||
const isVisible = await element.evaluate(el => {
|
||||
const style = window.getComputedStyle(el);
|
||||
return style.display !== 'none' &&
|
||||
style.visibility !== 'hidden' &&
|
||||
style.opacity !== '0';
|
||||
});
|
||||
|
||||
const wcagViolations = results.violations.filter(
|
||||
v => v.tags.includes('wcag2a') || v.tags.includes('wcag21aa')
|
||||
);
|
||||
expect(isVisible).toBeTruthy();
|
||||
}
|
||||
});
|
||||
|
||||
test('应该可以通过键盘导航', async ({ page }) => {
|
||||
await page.goto('http://localhost:3000');
|
||||
|
||||
const focusableElements = page.locator('a[href], button, input, textarea, select');
|
||||
const count = await focusableElements.count();
|
||||
|
||||
if (count > 0) {
|
||||
await page.keyboard.press('Tab');
|
||||
|
||||
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();
|
||||
const firstFocused = await page.evaluate(() => {
|
||||
return document.activeElement?.tagName;
|
||||
});
|
||||
|
||||
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);
|
||||
expect(['A', 'BUTTON', 'INPUT', 'TEXTAREA', 'SELECT']).toContain(firstFocused || '');
|
||||
}
|
||||
});
|
||||
|
||||
test('颜色对比度应该符合WCAG AA标准', async ({ page }) => {
|
||||
await page.goto('http://localhost:3000');
|
||||
|
||||
const textElements = page.locator('p, h1, h2, h3, h4, h5, h6, span, div').first(20);
|
||||
const count = await textElements.count();
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const element = textElements.nth(i);
|
||||
const contrastRatio = await element.evaluate(el => {
|
||||
const style = window.getComputedStyle(el);
|
||||
const bgColor = style.backgroundColor;
|
||||
const textColor = style.color;
|
||||
|
||||
if (bgColor === 'rgba(0, 0, 0, 0)' || bgColor === 'transparent') {
|
||||
return 21;
|
||||
}
|
||||
|
||||
const getLuminance = (color: string) => {
|
||||
const rgb = color.match(/\d+/g);
|
||||
if (!rgb || rgb.length < 3) return 0;
|
||||
|
||||
const [r, g, b] = rgb.map(Number).map(c => {
|
||||
c = c / 255;
|
||||
return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
|
||||
});
|
||||
|
||||
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
||||
};
|
||||
|
||||
const l1 = getLuminance(textColor);
|
||||
const l2 = getLuminance(bgColor);
|
||||
const lighter = Math.max(l1, l2);
|
||||
const darker = Math.min(l1, l2);
|
||||
|
||||
return (lighter + 0.05) / (darker + 0.05);
|
||||
});
|
||||
|
||||
expect(contrastRatio).toBeGreaterThanOrEqual(4.5);
|
||||
}
|
||||
});
|
||||
|
||||
test('应该有跳过导航链接', async ({ page }) => {
|
||||
await page.goto('http://localhost:3000');
|
||||
|
||||
const skipLink = page.locator('a[href^="#"][class*="skip"], a[href^="#main"], a[href^="#content"]');
|
||||
const hasSkipLink = await skipLink.count() > 0;
|
||||
|
||||
if (hasSkipLink) {
|
||||
await skipLink.first().click();
|
||||
|
||||
const target = await skipLink.first().getAttribute('href');
|
||||
const targetElement = page.locator(target || '');
|
||||
await expect(targetElement.first()).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
test('移动端菜单应该可以通过键盘关闭', async ({ page }) => {
|
||||
await page.setViewportSize({ width: 375, height: 667 });
|
||||
await page.goto('http://localhost:3000');
|
||||
|
||||
const menuButton = page.locator('button[aria-label*="菜单"], button[aria-label*="打开"]');
|
||||
await menuButton.click();
|
||||
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
const mobileMenu = page.locator('[role="navigation"][aria-label*="移动端"]');
|
||||
await expect(mobileMenu).toBeVisible();
|
||||
|
||||
await page.keyboard.press('Escape');
|
||||
|
||||
await page.waitForTimeout(200);
|
||||
|
||||
await expect(mobileMenu).not.toBeVisible();
|
||||
await expect(menuButton).toHaveAttribute('aria-expanded', 'false');
|
||||
});
|
||||
|
||||
test('表单错误应该与输入关联', async ({ page }) => {
|
||||
await page.goto('http://localhost:3000/contact');
|
||||
|
||||
const submitButton = page.locator('button[type="submit"]');
|
||||
await submitButton.click();
|
||||
|
||||
const requiredInputs = page.locator('input[required], textarea[required]');
|
||||
const count = await requiredInputs.count();
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const input = requiredInputs.nth(i);
|
||||
const hasError = await input.evaluate(el => {
|
||||
const id = el.getAttribute('id');
|
||||
const error = document.querySelector(`[role="alert"][for="${id}"], [aria-describedby*="${id}"]`);
|
||||
return !!error;
|
||||
});
|
||||
|
||||
if (hasError) {
|
||||
const ariaDescribedBy = await input.getAttribute('aria-describedby');
|
||||
expect(ariaDescribedBy).toBeTruthy();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test('表单元素应该有正确的标签', async () => {
|
||||
const inputs = homePage.page.locator('input, select, textarea');
|
||||
const count = await inputs.count();
|
||||
test('ARIA标签应该正确使用', async ({ page }) => {
|
||||
await page.goto('http://localhost:3000');
|
||||
|
||||
const ariaElements = page.locator('[aria-label], [aria-labelledby], [aria-describedby]');
|
||||
const count = await ariaElements.count();
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const element = ariaElements.nth(i);
|
||||
const isValidAria = await element.evaluate(el => {
|
||||
const ariaLabel = el.getAttribute('aria-label');
|
||||
const ariaLabelledBy = el.getAttribute('aria-labelledby');
|
||||
const ariaDescribedBy = el.getAttribute('aria-describedby');
|
||||
|
||||
if (ariaLabel) return ariaLabel.trim().length > 0;
|
||||
if (ariaLabelledBy) {
|
||||
const referenced = document.getElementById(ariaLabelledBy);
|
||||
return !!referenced;
|
||||
}
|
||||
if (ariaDescribedBy) {
|
||||
const referenced = document.getElementById(ariaDescribedBy);
|
||||
return !!referenced;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
expect(isValidAria).toBeTruthy();
|
||||
}
|
||||
});
|
||||
|
||||
test('视频/音频应该有字幕', async ({ page }) => {
|
||||
await page.goto('http://localhost:3000');
|
||||
|
||||
const mediaElements = page.locator('video, audio');
|
||||
const count = await mediaElements.count();
|
||||
|
||||
if (count > 0) {
|
||||
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;
|
||||
const media = mediaElements.nth(i);
|
||||
const hasCaptions = await media.evaluate(el => {
|
||||
const tagName = el.tagName.toLowerCase();
|
||||
if (tagName === 'video') {
|
||||
return el.hasAttribute('crossorigin') ||
|
||||
el.querySelector('track[kind="captions"], track[kind="subtitles"]');
|
||||
}
|
||||
return true;
|
||||
});
|
||||
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);
|
||||
}
|
||||
expect(hasCaptions).toBeTruthy();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
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();
|
||||
|
||||
test('表格应该有正确的标题', async ({ page }) => {
|
||||
await page.goto('http://localhost:3000');
|
||||
|
||||
const tables = page.locator('table');
|
||||
const count = await tables.count();
|
||||
|
||||
if (count > 0) {
|
||||
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
|
||||
);
|
||||
const table = tables.nth(i);
|
||||
const hasCaption = await table.evaluate(el => {
|
||||
return !!el.querySelector('caption') ||
|
||||
el.hasAttribute('aria-label') ||
|
||||
el.hasAttribute('title');
|
||||
});
|
||||
|
||||
if (backgroundColor !== 'rgba(0, 0, 0, 0)' && color !== 'rgba(0, 0, 0, 0)') {
|
||||
expect(backgroundColor).not.toBe(color);
|
||||
}
|
||||
expect(hasCaption).toBeTruthy();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
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();
|
||||
|
||||
test('模态对话框应该有正确的ARIA属性', async ({ page }) => {
|
||||
await page.goto('http://localhost:3000');
|
||||
|
||||
const dialogs = page.locator('[role="dialog"], [role="alertdialog"]');
|
||||
const count = await dialogs.count();
|
||||
|
||||
if (count > 0) {
|
||||
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);
|
||||
const dialog = dialogs.nth(i);
|
||||
const hasModalAttributes = await dialog.evaluate(el => {
|
||||
return el.hasAttribute('aria-modal') ||
|
||||
el.hasAttribute('aria-labelledby') ||
|
||||
el.hasAttribute('aria-label');
|
||||
});
|
||||
|
||||
expect(hasModalAttributes).toBeTruthy();
|
||||
}
|
||||
});
|
||||
|
||||
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