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 (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(); }); });