08ea5fbe98
添加用户管理视图、API和状态管理文件
503 lines
17 KiB
TypeScript
503 lines
17 KiB
TypeScript
import { test, expect, Page } from '@playwright/test';
|
|
|
|
const PERFORMANCE_THRESHOLDS = {
|
|
pageLoadTime: 2000,
|
|
firstContentfulPaint: 1000,
|
|
largestContentfulPaint: 1500,
|
|
timeToInteractive: 2000,
|
|
cumulativeLayoutShift: 0.1,
|
|
firstInputDelay: 100,
|
|
};
|
|
|
|
interface PerformanceMetrics {
|
|
pageLoadTime: number;
|
|
firstContentfulPaint: number;
|
|
largestContentfulPaint: number;
|
|
timeToInteractive: number;
|
|
cumulativeLayoutShift: number;
|
|
firstInputDelay: number;
|
|
}
|
|
|
|
async function measurePagePerformance(page: Page, url: string): Promise<PerformanceMetrics> {
|
|
const startTime = Date.now();
|
|
|
|
await page.goto(url, { waitUntil: 'networkidle' });
|
|
|
|
const pageLoadTime = Date.now() - startTime;
|
|
|
|
const metrics = await page.evaluate(() => {
|
|
const navigation = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming;
|
|
const paint = performance.getEntriesByType('paint');
|
|
const fcp = paint.find(entry => entry.name === 'first-contentful-paint')?.startTime || 0;
|
|
|
|
return {
|
|
pageLoadTime: 0,
|
|
firstContentfulPaint: fcp,
|
|
largestContentfulPaint: 0,
|
|
timeToInteractive: navigation.domInteractive - navigation.startTime,
|
|
cumulativeLayoutShift: 0,
|
|
firstInputDelay: 0,
|
|
};
|
|
});
|
|
|
|
metrics.pageLoadTime = pageLoadTime;
|
|
|
|
const lcp = await page.evaluate(() => {
|
|
return new Promise<number>((resolve) => {
|
|
if (!('PerformanceObserver' in window)) {
|
|
resolve(0);
|
|
return;
|
|
}
|
|
|
|
const observer = new PerformanceObserver((list) => {
|
|
const entries = list.getEntries();
|
|
const lastEntry = entries[entries.length - 1];
|
|
resolve(lastEntry.startTime);
|
|
});
|
|
|
|
observer.observe({ entryTypes: ['largest-contentful-paint'] });
|
|
|
|
setTimeout(() => {
|
|
observer.disconnect();
|
|
resolve(0);
|
|
}, 5000);
|
|
});
|
|
});
|
|
|
|
metrics.largestContentfulPaint = lcp;
|
|
|
|
const cls = await page.evaluate(() => {
|
|
return new Promise<number>((resolve) => {
|
|
if (!('PerformanceObserver' in window)) {
|
|
resolve(0);
|
|
return;
|
|
}
|
|
|
|
let clsValue = 0;
|
|
const observer = new PerformanceObserver((list) => {
|
|
for (const entry of list.getEntries()) {
|
|
if (!entry.hadRecentInput) {
|
|
clsValue += (entry as any).value;
|
|
}
|
|
}
|
|
});
|
|
|
|
observer.observe({ entryTypes: ['layout-shift'] });
|
|
|
|
setTimeout(() => {
|
|
observer.disconnect();
|
|
resolve(clsValue);
|
|
}, 5000);
|
|
});
|
|
});
|
|
|
|
metrics.cumulativeLayoutShift = cls;
|
|
|
|
const fid = await page.evaluate(() => {
|
|
return new Promise<number>((resolve) => {
|
|
if (!('PerformanceObserver' in window)) {
|
|
resolve(0);
|
|
return;
|
|
}
|
|
|
|
const observer = new PerformanceObserver((list) => {
|
|
for (const entry of list.getEntries()) {
|
|
resolve((entry as any).processingStart - entry.startTime);
|
|
}
|
|
});
|
|
|
|
observer.observe({ entryTypes: ['first-input'] });
|
|
|
|
setTimeout(() => {
|
|
observer.disconnect();
|
|
resolve(0);
|
|
}, 5000);
|
|
});
|
|
});
|
|
|
|
metrics.firstInputDelay = fid;
|
|
|
|
return metrics;
|
|
}
|
|
|
|
test.describe('日历页面加载性能测试', () => {
|
|
test('日历首页加载时间应小于2秒', async ({ page }) => {
|
|
const metrics = await measurePagePerformance(page, '/pages/calendar/index');
|
|
|
|
console.log('日历首页性能指标:', metrics);
|
|
|
|
expect(metrics.pageLoadTime).toBeLessThan(PERFORMANCE_THRESHOLDS.pageLoadTime);
|
|
});
|
|
|
|
test('日历首页首次内容绘制应小于1秒', async ({ page }) => {
|
|
const metrics = await measurePagePerformance(page, '/pages/calendar/index');
|
|
|
|
console.log('日历首页FCP:', metrics.firstContentfulPaint);
|
|
|
|
expect(metrics.firstContentfulPaint).toBeLessThan(PERFORMANCE_THRESHOLDS.firstContentfulPaint);
|
|
});
|
|
|
|
test('日历首页最大内容绘制应小于1.5秒', async ({ page }) => {
|
|
const metrics = await measurePagePerformance(page, '/pages/calendar/index');
|
|
|
|
console.log('日历首页LCP:', metrics.largestContentfulPaint);
|
|
|
|
expect(metrics.largestContentfulPaint).toBeLessThan(PERFORMANCE_THRESHOLDS.largestContentfulPaint);
|
|
});
|
|
|
|
test('日历首页可交互时间应小于2秒', async ({ page }) => {
|
|
const metrics = await measurePagePerformance(page, '/pages/calendar/index');
|
|
|
|
console.log('日历首页TTI:', metrics.timeToInteractive);
|
|
|
|
expect(metrics.timeToInteractive).toBeLessThan(PERFORMANCE_THRESHOLDS.timeToInteractive);
|
|
});
|
|
|
|
test('日历首页累积布局偏移应小于0.1', async ({ page }) => {
|
|
const metrics = await measurePagePerformance(page, '/pages/calendar/index');
|
|
|
|
console.log('日历首页CLS:', metrics.cumulativeLayoutShift);
|
|
|
|
expect(metrics.cumulativeLayoutShift).toBeLessThan(PERFORMANCE_THRESHOLDS.cumulativeLayoutShift);
|
|
});
|
|
|
|
test('日历详情页加载时间应小于2秒', async ({ page }) => {
|
|
await page.goto('/pages/calendar/index');
|
|
await page.click('.calendar-day[data-date="2026-02-11"]');
|
|
|
|
const startTime = Date.now();
|
|
await page.waitForURL('**/calendar/detail**');
|
|
const loadTime = Date.now() - startTime;
|
|
|
|
console.log('日历详情页加载时间:', loadTime);
|
|
|
|
expect(loadTime).toBeLessThan(PERFORMANCE_THRESHOLDS.pageLoadTime);
|
|
});
|
|
|
|
test('日历月切换加载时间应小于1秒', async ({ page }) => {
|
|
await page.goto('/pages/calendar/index');
|
|
await page.waitForSelector('.calendar-container');
|
|
|
|
const startTime = Date.now();
|
|
await page.click('.calendar-next-month');
|
|
await page.waitForSelector('.calendar-container');
|
|
const loadTime = Date.now() - startTime;
|
|
|
|
console.log('日历月切换加载时间:', loadTime);
|
|
|
|
expect(loadTime).toBeLessThan(1000);
|
|
});
|
|
|
|
test('日历年切换加载时间应小于1秒', async ({ page }) => {
|
|
await page.goto('/pages/calendar/index');
|
|
await page.waitForSelector('.calendar-container');
|
|
|
|
const startTime = Date.now();
|
|
await page.click('.calendar-year-selector');
|
|
await page.click('.calendar-year-option[data-year="2027"]');
|
|
await page.waitForSelector('.calendar-container');
|
|
const loadTime = Date.now() - startTime;
|
|
|
|
console.log('日历年切换加载时间:', loadTime);
|
|
|
|
expect(loadTime).toBeLessThan(1000);
|
|
});
|
|
|
|
test('日历今日跳转加载时间应小于1秒', async ({ page }) => {
|
|
await page.goto('/pages/calendar/index');
|
|
await page.waitForSelector('.calendar-container');
|
|
|
|
const startTime = Date.now();
|
|
await page.click('.calendar-today-btn');
|
|
await page.waitForSelector('.calendar-container');
|
|
const loadTime = Date.now() - startTime;
|
|
|
|
console.log('日历今日跳转加载时间:', loadTime);
|
|
|
|
expect(loadTime).toBeLessThan(1000);
|
|
});
|
|
|
|
test('日历搜索结果加载时间应小于1秒', async ({ page }) => {
|
|
await page.goto('/pages/calendar/index');
|
|
await page.waitForSelector('.calendar-container');
|
|
|
|
await page.fill('.calendar-search-input', '2026-02-14');
|
|
|
|
const startTime = Date.now();
|
|
await page.click('.calendar-search-btn');
|
|
await page.waitForSelector('.calendar-day[data-date="2026-02-14"]');
|
|
const loadTime = Date.now() - startTime;
|
|
|
|
console.log('日历搜索结果加载时间:', loadTime);
|
|
|
|
expect(loadTime).toBeLessThan(1000);
|
|
});
|
|
});
|
|
|
|
test.describe('黄历页面加载性能测试', () => {
|
|
test('黄历首页加载时间应小于2秒', async ({ page }) => {
|
|
const metrics = await measurePagePerformance(page, '/pages/almanac/index');
|
|
|
|
console.log('黄历首页性能指标:', metrics);
|
|
|
|
expect(metrics.pageLoadTime).toBeLessThan(PERFORMANCE_THRESHOLDS.pageLoadTime);
|
|
});
|
|
|
|
test('黄历首页首次内容绘制应小于1秒', async ({ page }) => {
|
|
const metrics = await measurePagePerformance(page, '/pages/almanac/index');
|
|
|
|
console.log('黄历首页FCP:', metrics.firstContentfulPaint);
|
|
|
|
expect(metrics.firstContentfulPaint).toBeLessThan(PERFORMANCE_THRESHOLDS.firstContentfulPaint);
|
|
});
|
|
|
|
test('黄历首页最大内容绘制应小于1.5秒', async ({ page }) => {
|
|
const metrics = await measurePagePerformance(page, '/pages/almanac/index');
|
|
|
|
console.log('黄历首页LCP:', metrics.largestContentfulPaint);
|
|
|
|
expect(metrics.largestContentfulPaint).toBeLessThan(PERFORMANCE_THRESHOLDS.largestContentfulPaint);
|
|
});
|
|
|
|
test('黄历详情页加载时间应小于2秒', async ({ page }) => {
|
|
await page.goto('/pages/almanac/index');
|
|
await page.click('.almanac-day[data-date="2026-02-11"]');
|
|
|
|
const startTime = Date.now();
|
|
await page.waitForURL('**/almanac/detail**');
|
|
const loadTime = Date.now() - startTime;
|
|
|
|
console.log('黄历详情页加载时间:', loadTime);
|
|
|
|
expect(loadTime).toBeLessThan(PERFORMANCE_THRESHOLDS.pageLoadTime);
|
|
});
|
|
|
|
test('黄历月切换加载时间应小于1秒', async ({ page }) => {
|
|
await page.goto('/pages/almanac/index');
|
|
await page.waitForSelector('.almanac-container');
|
|
|
|
const startTime = Date.now();
|
|
await page.click('.almanac-next-month');
|
|
await page.waitForSelector('.almanac-container');
|
|
const loadTime = Date.now() - startTime;
|
|
|
|
console.log('黄历月切换加载时间:', loadTime);
|
|
|
|
expect(loadTime).toBeLessThan(1000);
|
|
});
|
|
|
|
test('黄历宜忌筛选加载时间应小于1秒', async ({ page }) => {
|
|
await page.goto('/pages/almanac/index');
|
|
await page.waitForSelector('.almanac-container');
|
|
|
|
const startTime = Date.now();
|
|
await page.click('.almanac-filter-btn');
|
|
await page.click('.filter-checkbox[data-value="嫁娶"]');
|
|
await page.click('.filter-confirm-btn');
|
|
await page.waitForSelector('.almanac-container');
|
|
const loadTime = Date.now() - startTime;
|
|
|
|
console.log('黄历宜忌筛选加载时间:', loadTime);
|
|
|
|
expect(loadTime).toBeLessThan(1000);
|
|
});
|
|
|
|
test('黄历搜索结果加载时间应小于1秒', async ({ page }) => {
|
|
await page.goto('/pages/almanac/index');
|
|
await page.waitForSelector('.almanac-container');
|
|
|
|
await page.fill('.almanac-search-input', '春节');
|
|
|
|
const startTime = Date.now();
|
|
await page.click('.almanac-search-btn');
|
|
await page.waitForSelector('.almanac-search-result');
|
|
const loadTime = Date.now() - startTime;
|
|
|
|
console.log('黄历搜索结果加载时间:', loadTime);
|
|
|
|
expect(loadTime).toBeLessThan(1000);
|
|
});
|
|
});
|
|
|
|
test.describe('用户页面加载性能测试', () => {
|
|
test('用户首页加载时间应小于2秒', async ({ page }) => {
|
|
const metrics = await measurePagePerformance(page, '/pages/user/index');
|
|
|
|
console.log('用户首页性能指标:', metrics);
|
|
|
|
expect(metrics.pageLoadTime).toBeLessThan(PERFORMANCE_THRESHOLDS.pageLoadTime);
|
|
});
|
|
|
|
test('用户首页首次内容绘制应小于1秒', async ({ page }) => {
|
|
const metrics = await measurePagePerformance(page, '/pages/user/index');
|
|
|
|
console.log('用户首页FCP:', metrics.firstContentfulPaint);
|
|
|
|
expect(metrics.firstContentfulPaint).toBeLessThan(PERFORMANCE_THRESHOLDS.firstContentfulPaint);
|
|
});
|
|
|
|
test('用户首页最大内容绘制应小于1.5秒', async ({ page }) => {
|
|
const metrics = await measurePagePerformance(page, '/pages/user/index');
|
|
|
|
console.log('用户首页LCP:', metrics.largestContentfulPaint);
|
|
|
|
expect(metrics.largestContentfulPaint).toBeLessThan(PERFORMANCE_THRESHOLDS.largestContentfulPaint);
|
|
});
|
|
|
|
test('用户设置页加载时间应小于2秒', async ({ page }) => {
|
|
await page.goto('/pages/user/index');
|
|
await page.click('.user-settings-btn');
|
|
|
|
const startTime = Date.now();
|
|
await page.waitForURL('**/user/settings**');
|
|
const loadTime = Date.now() - startTime;
|
|
|
|
console.log('用户设置页加载时间:', loadTime);
|
|
|
|
expect(loadTime).toBeLessThan(PERFORMANCE_THRESHOLDS.pageLoadTime);
|
|
});
|
|
|
|
test('用户收藏页加载时间应小于2秒', async ({ page }) => {
|
|
await page.goto('/pages/user/index');
|
|
await page.click('.user-favorites-btn');
|
|
|
|
const startTime = Date.now();
|
|
await page.waitForURL('**/user/favorites**');
|
|
const loadTime = Date.now() - startTime;
|
|
|
|
console.log('用户收藏页加载时间:', loadTime);
|
|
|
|
expect(loadTime).toBeLessThan(PERFORMANCE_THRESHOLDS.pageLoadTime);
|
|
});
|
|
|
|
test('用户历史页加载时间应小于2秒', async ({ page }) => {
|
|
await page.goto('/pages/user/index');
|
|
await page.click('.user-history-btn');
|
|
|
|
const startTime = Date.now();
|
|
await page.waitForURL('**/user/history**');
|
|
const loadTime = Date.now() - startTime;
|
|
|
|
console.log('用户历史页加载时间:', loadTime);
|
|
|
|
expect(loadTime).toBeLessThan(PERFORMANCE_THRESHOLDS.pageLoadTime);
|
|
});
|
|
|
|
test('用户主题切换加载时间应小于1秒', async ({ page }) => {
|
|
await page.goto('/pages/user/index');
|
|
await page.waitForSelector('.user-container');
|
|
|
|
const startTime = Date.now();
|
|
await page.click('.theme-switch-btn');
|
|
await page.waitForSelector('.user-container');
|
|
const loadTime = Date.now() - startTime;
|
|
|
|
console.log('用户主题切换加载时间:', loadTime);
|
|
|
|
expect(loadTime).toBeLessThan(1000);
|
|
});
|
|
});
|
|
|
|
test.describe('搜索页面加载性能测试', () => {
|
|
test('搜索首页加载时间应小于2秒', async ({ page }) => {
|
|
const metrics = await measurePagePerformance(page, '/pages/search/index');
|
|
|
|
console.log('搜索首页性能指标:', metrics);
|
|
|
|
expect(metrics.pageLoadTime).toBeLessThan(PERFORMANCE_THRESHOLDS.pageLoadTime);
|
|
});
|
|
|
|
test('搜索首页首次内容绘制应小于1秒', async ({ page }) => {
|
|
const metrics = await measurePagePerformance(page, '/pages/search/index');
|
|
|
|
console.log('搜索首页FCP:', metrics.firstContentfulPaint);
|
|
|
|
expect(metrics.firstContentfulPaint).toBeLessThan(PERFORMANCE_THRESHOLDS.firstContentfulPaint);
|
|
});
|
|
|
|
test('搜索首页最大内容绘制应小于1.5秒', async ({ page }) => {
|
|
const metrics = await measurePagePerformance(page, '/pages/search/index');
|
|
|
|
console.log('搜索首页LCP:', metrics.largestContentfulPaint);
|
|
|
|
expect(metrics.largestContentfulPaint).toBeLessThan(PERFORMANCE_THRESHOLDS.largestContentfulPaint);
|
|
});
|
|
|
|
test('搜索结果加载时间应小于1秒', async ({ page }) => {
|
|
await page.goto('/pages/search/index');
|
|
await page.waitForSelector('.search-input');
|
|
|
|
await page.fill('.search-input', '春节');
|
|
|
|
const startTime = Date.now();
|
|
await page.click('.search-btn');
|
|
await page.waitForSelector('.search-results');
|
|
const loadTime = Date.now() - startTime;
|
|
|
|
console.log('搜索结果加载时间:', loadTime);
|
|
|
|
expect(loadTime).toBeLessThan(1000);
|
|
});
|
|
|
|
test('搜索建议加载时间应小于500毫秒', async ({ page }) => {
|
|
await page.goto('/pages/search/index');
|
|
await page.waitForSelector('.search-input');
|
|
|
|
await page.fill('.search-input', '春');
|
|
|
|
const startTime = Date.now();
|
|
await page.waitForSelector('.search-suggestions');
|
|
const loadTime = Date.now() - startTime;
|
|
|
|
console.log('搜索建议加载时间:', loadTime);
|
|
|
|
expect(loadTime).toBeLessThan(500);
|
|
});
|
|
|
|
test('搜索历史加载时间应小于1秒', async ({ page }) => {
|
|
await page.goto('/pages/search/index');
|
|
await page.waitForSelector('.search-input');
|
|
|
|
const startTime = Date.now();
|
|
await page.click('.search-history-btn');
|
|
await page.waitForSelector('.search-history-list');
|
|
const loadTime = Date.now() - startTime;
|
|
|
|
console.log('搜索历史加载时间:', loadTime);
|
|
|
|
expect(loadTime).toBeLessThan(1000);
|
|
});
|
|
|
|
test('搜索热门加载时间应小于1秒', async ({ page }) => {
|
|
await page.goto('/pages/search/index');
|
|
await page.waitForSelector('.search-input');
|
|
|
|
const startTime = Date.now();
|
|
await page.click('.search-hot-btn');
|
|
await page.waitForSelector('.search-hot-list');
|
|
const loadTime = Date.now() - startTime;
|
|
|
|
console.log('搜索热门加载时间:', loadTime);
|
|
|
|
expect(loadTime).toBeLessThan(1000);
|
|
});
|
|
|
|
test('搜索分类切换加载时间应小于500毫秒', async ({ page }) => {
|
|
await page.goto('/pages/search/index');
|
|
await page.waitForSelector('.search-input');
|
|
|
|
await page.fill('.search-input', '春节');
|
|
await page.click('.search-btn');
|
|
await page.waitForSelector('.search-results');
|
|
|
|
const startTime = Date.now();
|
|
await page.click('.search-category[data-category="almanac"]');
|
|
await page.waitForSelector('.search-results');
|
|
const loadTime = Date.now() - startTime;
|
|
|
|
console.log('搜索分类切换加载时间:', loadTime);
|
|
|
|
expect(loadTime).toBeLessThan(500);
|
|
});
|
|
});
|