10404dbb36
同步工作区剩余变更,主要包括: - 营销页面组件与布局持续优化(about/news/services/solutions/team 等) - 详情页四层叙事组件、布局组件、UI 组件调整 - CMS 数据模型、API 路由、权限、工作流、站内通知、媒体管理扩展 - 新增/补充单元测试与 E2E 测试(cms-workflow.spec.ts 等) - ESLint 9 迁移、jest/tsconfig 配置更新、依赖调整 - 新增 ADR、CMS 评估文档、Release Review / Acceptance 报告 - 移除水墨装饰组件与大体积未使用字体文件
254 lines
9.5 KiB
TypeScript
254 lines
9.5 KiB
TypeScript
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;
|
|
|
|
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();
|
|
});
|
|
});
|