Files
novalon-website/e2e/p3-compatibility.spec.ts
T
zhangxiang 6bb7c557ee test(qa): e2e 装配修正 + 集成/门禁测试与工具链重构
- e2e:修死选择器与蜜罐误命中、补断言、GA4/security-headers 用例诚实标注
  skip 边界;新增 assert-helpers/fixtures/hydrated/primary-nav/touch-targets
  等复用桩。
- 集成层:config/test/jest.integration.config.js + tests-integration/ 真库
  一次性 SQLite 用例,teardown 守卫开发库指纹。
- 门禁工具链:jest.setup 归位到根、scripts/accessibility 全站 axe 节点计数、
  check-brand-text-token/check-motion-constraints 机械守卫、stryker/lighthouse
  配置收敛。
2026-09-28 10:48:08 +08:00

259 lines
11 KiB
TypeScript
Raw 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 './fixtures';
import { pollBoundingBox } from './assert-helpers';
/**
* P3: 多浏览器与多设备兼容性测试
*
* 目标:验证网站在不同浏览器和设备上的显示一致性
*
* 测试矩阵:
* - 浏览器:Chrome, Firefox, Safari (WebKit)
* - 设备:Desktop, Tablet, Mobile (多种尺寸)
* - 关键页面:首页、关于、联系、产品等
*/
// 设置超时时间
test.setTimeout(60000);
// ==================== 关键页面的跨浏览器一致性测试 ====================
test.describe('跨浏览器 - 核心页面渲染', { tag: '@regression' }, () => {
const criticalPages = [
{ path: '/', name: '首页' },
{ path: '/about', name: '关于我们' },
{ path: '/contact', name: '联系我们' },
];
for (const { path, name } of criticalPages) {
test(`${name} (${path}) - 页面基本元素可见`, async ({ page }) => {
await page.goto(path, { waitUntil: 'domcontentloaded', timeout: 30000 });
await page.waitForTimeout(2000);
// 基本结构检查
await expect(page.locator('header')).toBeVisible();
await expect(page.locator('main, [role="main"]').first()).toBeVisible();
await expect(page.locator('footer, [data-testid="footer"]')).toBeVisible();
});
test(`${name} (${path}) - Logo 显示正常`, async ({ page }) => {
await page.goto(path, { waitUntil: 'domcontentloaded', timeout: 30000 });
await page.waitForTimeout(1500);
// N-27 修复:原守卫 `if (logo.count() > 0)` 让 Logo 消失时静默通过。
// 证据:header.tsx:96-121 恒定渲染 a[aria-label="返回首页"],内含明暗两版
// logo(按 html[data-theme] 显隐,恰有一个可见),CSS h-8 md:h-10 ⇒
// 高度 32/40px、宽度随 160:40 比例自动缩放,桌面与移动断点均成立。
const logo = page.locator('header a[aria-label="返回首页"] img:visible').first();
await expect(logo).toBeVisible();
// 检查 Logo 尺寸合理
const box = await pollBoundingBox(logo);
expect(box.width).toBeGreaterThan(50);
expect(box.height).toBeGreaterThan(20);
});
test(`${name} (${path}) - 导航菜单可交互`, async ({ page }) => {
await page.goto(path, { waitUntil: 'domcontentloaded', timeout: 30000 });
await page.waitForTimeout(1500);
// 桌面端导航或移动端按钮应该存在
const desktopNav = page.locator('[data-testid="desktop-navigation"]');
const mobileMenuBtn = page.locator('[data-testid="mobile-menu-button"]');
const hasDesktopNav = await desktopNav.count() > 0 && await desktopNav.first().isVisible();
const hasMobileBtn = await mobileMenuBtn.count() > 0 && await mobileMenuBtn.first().isVisible();
expect(hasDesktopNav || hasMobileBtn).toBeTruthy();
});
}
});
// ==================== 多设备视口适配测试 ====================
test.describe('多设备 - 视口响应式布局', { tag: '@regression' }, () => {
const viewports = [
{ name: 'Desktop XL', width: 1920, height: 1080, type: 'desktop' as const },
{ name: 'Desktop', width: 1280, height: 800, type: 'desktop' as const },
{ name: 'Laptop', width: 1024, height: 768, type: 'laptop' as const },
{ name: 'Tablet Landscape', width: 768, height: 1024, type: 'tablet' as const },
{ name: 'Tablet Portrait', width: 768, height: 1024, type: 'tablet' as const },
{ name: 'Mobile Large', width: 428, height: 926, type: 'mobile' as const },
{ name: 'Mobile Medium', width: 375, height: 667, type: 'mobile' as const },
{ name: 'Mobile Small', width: 320, height: 568, type: 'mobile' as const },
];
for (const viewport of viewports) {
test(`${viewport.name} (${viewport.width}x${viewport.height}) - 首页布局完整`, async ({ page }) => {
await page.setViewportSize({ width: viewport.width, height: viewport.height });
await page.goto('/', { waitUntil: 'domcontentloaded', timeout: 30000 });
await page.waitForTimeout(1500);
// Header 可见
await expect(page.locator('header')).toBeVisible();
// Main content 可见
const mainContent = page.locator('main, [role="main"], #main-content').first();
await expect(mainContent).toBeVisible();
// Footer 可见
await expect(page.locator('footer, [data-testid="footer"]')).toBeVisible();
});
test(`${viewport.name} (${viewport.width}x${viewport.height}) - 无水平滚动条`, async ({ page }) => {
await page.setViewportSize({ width: viewport.width, height: viewport.height });
await page.goto('/', { waitUntil: 'domcontentloaded', timeout: 30000 });
await page.waitForTimeout(1500);
// 检查是否出现水平滚动条
const hasHorizontalScroll = await page.evaluate(() => {
return document.documentElement.scrollWidth > document.documentElement.clientWidth;
});
expect(hasHorizontalScroll).toBeFalsy();
});
if (viewport.type === 'mobile') {
test(`${viewport.name} - 移动端导航可用`, async ({ page }) => {
await page.setViewportSize({ width: viewport.width, height: viewport.height });
await page.goto('/', { waitUntil: 'domcontentloaded', timeout: 30000 });
await page.waitForTimeout(1500);
// N-27 修复:本组用例显式设到移动视口(≤428px < md 断点),
// header.tsx:192 的 mobile-menu-button(min 44×44px)必须可见。
// 原守卫 `if (isVisible)` ⇒ 按钮消失时静默通过;改为硬断言 + 轮询盒模型。
const mobileMenuBtn = page.locator('[data-testid="mobile-menu-button"]').first();
await expect(mobileMenuBtn).toBeVisible();
// 按钮应在可视区域内(pollBoundingBox 等待节点稳定,超时即红)
const box = await pollBoundingBox(mobileMenuBtn);
expect(box.x).toBeGreaterThanOrEqual(0);
expect(box.y).toBeGreaterThanOrEqual(0);
expect(box.x + box.width).toBeLessThanOrEqual(viewport.width + 10); // 允许小误差
});
}
}
});
// ==================== 特定浏览器行为测试 ====================
test.describe('特定浏览器 - 行为差异检查', { tag: '@regression' }, () => {
test('字体回退机制工作正常', async ({ page }) => {
await page.goto('/', { waitUntil: 'domcontentloaded', timeout: 30000 });
await page.waitForTimeout(1500);
// 检查 body 的 font-family 是否设置
const fontFamily = await page.evaluate(() => {
return window.getComputedStyle(document.body).fontFamily;
});
expect(fontFamily).toBeTruthy();
expect(fontFamily.length).toBeGreaterThan(0);
// 应该包含中文字体或通用字体族
expect(fontFamily.toLowerCase()).toMatch(/sans-serif|serif|system-ui|pingfang|microsoft|hei/i);
});
test('CSS Grid/Flexbox 兼容性', async ({ page }) => {
await page.goto('/', { waitUntil: 'domcontentloaded', timeout: 30000 });
await page.waitForTimeout(1500);
// 检查是否有使用现代布局的容器
const modernLayoutUsed = await page.evaluate(() => {
const allElements = document.querySelectorAll('*');
let usesGrid = false;
let usesFlex = false;
for (const el of allElements) {
const style = window.getComputedStyle(el);
if (style.display === 'grid') usesGrid = true;
if (style.display === 'flex') usesFlex = true;
if (usesGrid && usesFlex) break;
}
return { usesGrid, usesFlex };
});
// 网站应该使用现代CSS布局
expect(modernLayoutUsed.usesGrid || modernLayoutUsed.usesFlex).toBeTruthy();
});
test('图片懒加载兼容性', async ({ page }) => {
await page.goto('/', { waitUntil: 'domcontentloaded', timeout: 30000 });
await page.waitForTimeout(2000);
// 检查图片是否正确加载
const images = page.locator('img');
const imageCount = await images.count();
// N-27 修复:原 `if (imageCount > 0)` 守卫——首页 img 数为 0 时本例静默通过。
// header.tsx:103-121 恒渲染两枚 logo <img>(priority/eager),img 数为 0 即真实回归。
expect(imageCount).toBeGreaterThan(0);
let loadedCount = 0;
for (let i = 0; i < Math.min(imageCount, 10); i++) {
const img = images.nth(i);
const naturalWidth = await img.evaluate((el: HTMLImageElement) => el.naturalWidth);
if (naturalWidth > 0) loadedCount++;
}
// 至少应该有一些图片加载成功(无条件断言)
expect(loadedCount).toBeGreaterThan(0);
});
test('表单元素样式一致', async ({ page }) => {
await page.goto('/contact', { waitUntil: 'domcontentloaded', timeout: 30000 });
await page.waitForTimeout(3000);
// N-27 修复:原双守卫 `if (inputCount > 0)` + `if (box)` 使本例在表单消失时
// 静默通过。联系页表单为恒定契约(contact-content-v3 渲染必填输入),
// 改为主内容区内作用域的硬断言 + 轮询盒模型。
const inputs = page.locator('main input:visible, main textarea:visible');
const inputCount = await inputs.count();
expect(inputCount).toBeGreaterThan(0);
for (let i = 0; i < Math.min(inputCount, 5); i++) {
const input = inputs.nth(i);
// Verify input is visible and has reasonable dimensions
const box = await pollBoundingBox(input);
expect(box.width).toBeGreaterThan(50);
expect(box.height).toBeGreaterThan(20);
}
});
});
// ==================== 触摸设备模拟测试 ====================
test.describe('触摸设备 - 手势交互', { tag: '@regression' }, () => {
test('触摸滑动流畅(无卡顿)', async ({ page, browserName }) => {
// touchscreen API is only available in Chromium with touch emulation
test.skip(browserName !== 'chromium', 'Touchscreen API only available in Chromium');
await page.setViewportSize({ width: 375, height: 667 });
await page.goto('/', { waitUntil: 'domcontentloaded', timeout: 30000 });
await page.waitForTimeout(1500);
// Simulate scroll via mouse wheel instead of touchscreen (cross-browser compatible)
await page.mouse.wheel(0, 300);
await page.waitForTimeout(500);
// 页面不应该崩溃或有错误
const bodyVisible = await page.locator('body').isVisible();
expect(bodyVisible).toBeTruthy();
});
test('双击缩放不会破坏布局', async ({ page }) => {
await page.setViewportSize({ width: 375, height: 667 });
await page.goto('/', { waitUntil: 'domcontentloaded', timeout: 30000 });
await page.waitForTimeout(1500);
// 模拟双击
const header = page.locator('header').first();
await header.dblclick();
await page.waitForTimeout(500);
// Header 应该仍然可见
await expect(header).toBeVisible();
});
});