import { test, expect } from '@playwright/test'; /** * P3: 多浏览器与多设备兼容性测试 * * 目标:验证网站在不同浏览器和设备上的显示一致性 * * 测试矩阵: * - 浏览器:Chrome, Firefox, Safari (WebKit) * - 设备:Desktop, Tablet, Mobile (多种尺寸) * - 关键页面:首页、关于、联系、产品等 */ // 设置超时时间 test.setTimeout(60000); // ==================== 关键页面的跨浏览器一致性测试 ==================== test.describe('跨浏览器 - 核心页面渲染', () => { 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); const logo = page.locator('header img, header svg').first(); if (await logo.count() > 0) { await expect(logo).toBeVisible(); // 检查 Logo 尺寸合理 const box = await logo.boundingBox(); expect(box).not.toBeNull(); 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('多设备 - 视口响应式布局', () => { 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); const mobileMenuBtn = page.locator('[data-testid="mobile-menu-button"]').first(); if (await mobileMenuBtn.isVisible()) { // 按钮应在可视区域内 const box = await mobileMenuBtn.boundingBox(); expect(box).not.toBeNull(); expect(box!.x).toBeGreaterThanOrEqual(0); expect(box!.y).toBeGreaterThanOrEqual(0); expect(box!.x + box!.width).toBeLessThanOrEqual(viewport.width + 10); // 允许小误差 } }); } } }); // ==================== 特定浏览器行为测试 ==================== test.describe('特定浏览器 - 行为差异检查', () => { 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(); let loadedCount = 0; let errorCount = 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++; } // 至少应该有一些图片加载成功 if (imageCount > 0) { expect(loadedCount).toBeGreaterThan(0); } }); test('表单元素样式一致', async ({ page }) => { await page.goto('/contact', { waitUntil: 'domcontentloaded', timeout: 30000 }); await page.waitForTimeout(3000); // Check for any visible form inputs (selectors vary by page implementation) const inputs = page.locator('input:visible, textarea:visible'); const inputCount = await inputs.count(); if (inputCount > 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 input.boundingBox(); if (box) { expect(box.width).toBeGreaterThan(50); expect(box.height).toBeGreaterThan(20); } } } }); }); // ==================== 触摸设备模拟测试 ==================== test.describe('触摸设备 - 手势交互', () => { 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(); }); });