import { test, expect } from '@playwright/test'; /** * 移动端性能基线测试 * * 目标:在 iPhone 14 视口(390x844)下测量关键性能指标, * 建立移动端性能基线,与桌面端对比发现性能差距。 * * 测试指标: * 1. FCP (First Contentful Paint) < 2s * 2. LCP (Largest Contentful Paint) < 2.5s * 3. DOMContentLoaded < 3s * 4. 完整加载时间 < 8s * 5. 长任务计数 * 6. 资源加载分析 * 7. 关键页面加载时间对比 * * 标记:@mobile @performance */ test.setTimeout(90000); // ==================== 1. 核心性能指标 ==================== test.describe('移动端性能 - 核心加载指标', { tag: '@mobile @performance' }, () => { test('首页 FCP < 2s', async ({ page }) => { const fcp = await page.goto('/', { waitUntil: 'domcontentloaded', timeout: 30000 }).then(async () => { await page.waitForTimeout(1500); return page.evaluate(() => { const entries = performance.getEntriesByType('paint'); const fcpEntry = entries.find(e => e.name === 'first-contentful-paint'); return fcpEntry ? fcpEntry.startTime : null; }); }); if (fcp !== null) { console.log(`Mobile FCP: ${fcp.toFixed(0)}ms`); expect(fcp).toBeLessThan(2000); } }); test('首页 LCP < 2.5s', async ({ page }) => { await page.goto('/', { waitUntil: 'domcontentloaded', timeout: 30000 }); await page.waitForTimeout(2000); const lcp = await page.evaluate(() => { return new Promise((resolve) => { const observer = new PerformanceObserver((list) => { const entries = list.getEntries(); if (entries.length > 0) { const lastEntry = entries[entries.length - 1]; resolve(lastEntry.startTime); } }); observer.observe({ type: 'largest-contentful-paint', buffered: true }); // 超时返回 null setTimeout(() => resolve(null), 5000); }); }); if (lcp !== null) { console.log(`Mobile LCP: ${lcp.toFixed(0)}ms`); expect(lcp).toBeLessThan(2500); } }); test('首页 DOMContentLoaded < 3s', async ({ page }) => { const startTime = Date.now(); await page.goto('/', { waitUntil: 'domcontentloaded', timeout: 30000 }); const loadTime = Date.now() - startTime; console.log(`Mobile DOMContentLoaded: ${loadTime}ms`); expect(loadTime).toBeLessThan(3000); }); test('首页完整加载时间 < 8s', async ({ page }) => { const startTime = Date.now(); await page.goto('/', { waitUntil: 'load', timeout: 30000 }); await page.waitForTimeout(1000); const loadTime = Date.now() - startTime; console.log(`Mobile full load: ${loadTime}ms`); expect(loadTime).toBeLessThan(8000); }); test('长任务计数 ≤ 3', async ({ page }) => { await page.goto('/', { waitUntil: 'domcontentloaded', timeout: 30000 }); await page.waitForTimeout(2000); const longTaskCount = await page.evaluate(() => { return new Promise((resolve) => { const observer = new PerformanceObserver((list) => { resolve(list.getEntries().length); }); observer.observe({ entryTypes: ['longtask'] }); setTimeout(() => resolve(0), 3000); }); }); console.log(`Mobile long task count: ${longTaskCount}`); expect(longTaskCount).toBeLessThanOrEqual(3); }); }); // ==================== 2. 关键页面加载对比 ==================== test.describe('移动端性能 - 关键页面加载对比', { tag: '@mobile @performance' }, () => { const pages = [ { path: '/', name: '首页' }, { path: '/about', name: '关于我们' }, { path: '/contact', name: '联系我们' }, { path: '/products', name: '产品中心' }, { path: '/solutions', name: '解决方案' }, { path: '/services', name: '服务' }, { path: '/news', name: '新闻' }, { path: '/cases', name: '案例' }, ]; for (const { path, name } of pages) { test(`${name} (${path}) 加载 < 5s`, async ({ page }) => { const startTime = Date.now(); await page.goto(path, { waitUntil: 'domcontentloaded', timeout: 30000 }); await page.waitForTimeout(1500); const loadTime = Date.now() - startTime; console.log(`Mobile ${name} load: ${loadTime}ms`); expect(loadTime).toBeLessThan(5000); }); } }); // ==================== 3. 资源加载分析 ==================== test.describe('移动端性能 - 资源加载分析', { tag: '@mobile @performance' }, () => { test('图片资源大小合理', async ({ page }) => { await page.goto('/', { waitUntil: 'domcontentloaded', timeout: 30000 }); await page.waitForTimeout(2000); const imageAnalysis = await page.evaluate(() => { const images = Array.from(document.querySelectorAll('img')); let largeImages = 0; let oversizedCount = 0; images.forEach(img => { if (img.naturalWidth > 1920) { largeImages++; } // 检查移动端是否加载了远大于视口的图片 if (img.naturalWidth > 800 && img.naturalWidth > 0) { oversizedCount++; } }); return { totalImages: images.length, largeImageCount: largeImages, oversizedForMobile: oversizedCount, }; }); console.log('Mobile image analysis:', imageAnalysis); expect(imageAnalysis.totalImages).toBeGreaterThan(0); }); test('JS/CSS 资源加载时间合理', async ({ page }) => { await page.goto('/', { waitUntil: 'domcontentloaded', timeout: 30000 }); const resourceTiming = await page.evaluate(() => { const entries = performance.getEntriesByType('resource') as PerformanceResourceTiming[]; return entries .filter(e => e.initiatorType === 'script' || e.initiatorType === 'stylesheet') .map(e => ({ name: e.name.split('/').pop(), type: e.initiatorType, duration: Math.round(e.duration), transferSize: e.transferSize, })); }); console.log('Mobile resource timing:', resourceTiming.slice(0, 10)); const criticalResources = resourceTiming.filter(r => r.name?.includes('main') || r.name?.includes('chunk') || r.name?.includes('app') ); for (const resource of criticalResources) { expect(resource.duration).toBeLessThan(3000); } }); }); // ==================== 4. 移动端渲染性能 ==================== test.describe('移动端性能 - 渲染性能', { tag: '@mobile @performance' }, () => { test('页面无水平滚动条', async ({ page }) => { 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(); }); test('滚动流畅无卡顿', async ({ page }) => { await page.goto('/', { waitUntil: 'domcontentloaded', timeout: 30000 }); await page.waitForTimeout(1500); // 模拟快速滚动 for (let i = 0; i < 5; i++) { await page.evaluate(() => window.scrollBy(0, window.innerHeight)); await page.waitForTimeout(200); } // 滚动后页面不应崩溃 const bodyVisible = await page.locator('body').isVisible(); expect(bodyVisible).toBeTruthy(); }); test('触摸目标尺寸 ≥ 44px(WCAG 2.1 AA)', async ({ page }) => { await page.goto('/', { waitUntil: 'domcontentloaded', timeout: 30000 }); await page.waitForTimeout(1500); const smallTargets = await page.evaluate(() => { const interactiveElements = document.querySelectorAll( 'a, button, input, select, textarea, [role="button"], [tabindex]:not([tabindex="-1"])' ); const issues: Array<{ tag: string; text: string; width: number; height: number }> = []; interactiveElements.forEach(el => { const rect = el.getBoundingClientRect(); // 只检查可见且非零尺寸的元素 if (rect.width > 0 && rect.height > 0 && rect.width < 1000) { if (rect.width < 44 || rect.height < 44) { const text = el.textContent?.trim().slice(0, 30) || el.tagName; // 排除内联文本链接(如段落中的 a 标签) const isInline = el.closest('p, span, li, h1, h2, h3, h4, h5, h6'); if (!isInline) { issues.push({ tag: el.tagName, text: text, width: Math.round(rect.width), height: Math.round(rect.height), }); } } } }); return issues; }); if (smallTargets.length > 0) { console.log('Small touch targets found:', smallTargets.slice(0, 10)); } // 允许少量例外(如小图标按钮),但不应超过 5 个 expect(smallTargets.length).toBeLessThanOrEqual(5); }); });