feat(admin): 添加用户管理相关文件
添加用户管理视图、API和状态管理文件
This commit is contained in:
@@ -0,0 +1,425 @@
|
||||
import { test, expect, Page } from '@playwright/test';
|
||||
|
||||
const ANIMATION_THRESHOLDS = {
|
||||
fps: 30,
|
||||
frameTime: 33.33,
|
||||
animationDuration: 500,
|
||||
};
|
||||
|
||||
interface AnimationMetrics {
|
||||
fps: number;
|
||||
frameTime: number;
|
||||
droppedFrames: number;
|
||||
totalFrames: number;
|
||||
}
|
||||
|
||||
async function measureAnimationPerformance(page: Page, animationTrigger: () => Promise<void>, duration: number = 1000): Promise<AnimationMetrics> {
|
||||
const frames: number[] = [];
|
||||
|
||||
await page.evaluate(() => {
|
||||
window.performanceMetrics = {
|
||||
frames: [],
|
||||
startTime: performance.now(),
|
||||
};
|
||||
});
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
const frameCollector = setInterval(async () => {
|
||||
const timestamp = await page.evaluate(() => {
|
||||
const metrics = (window as any).performanceMetrics;
|
||||
if (metrics) {
|
||||
metrics.frames.push(performance.now() - metrics.startTime);
|
||||
}
|
||||
return performance.now();
|
||||
});
|
||||
frames.push(timestamp);
|
||||
}, 16);
|
||||
|
||||
await animationTrigger();
|
||||
|
||||
await page.waitForTimeout(duration);
|
||||
|
||||
clearInterval(frameCollector);
|
||||
|
||||
const metrics = await page.evaluate(() => {
|
||||
const metrics = (window as any).performanceMetrics;
|
||||
return metrics ? metrics.frames : [];
|
||||
});
|
||||
|
||||
const totalFrames = metrics.length;
|
||||
const droppedFrames = metrics.filter((frameTime: number, index: number) => {
|
||||
if (index === 0) return false;
|
||||
return frameTime - metrics[index - 1] > 33.33;
|
||||
}).length;
|
||||
|
||||
const fps = totalFrames / (duration / 1000);
|
||||
const frameTime = duration / totalFrames;
|
||||
|
||||
return {
|
||||
fps,
|
||||
frameTime,
|
||||
droppedFrames,
|
||||
totalFrames,
|
||||
};
|
||||
}
|
||||
|
||||
async function measureThemeSwitchAnimation(page: Page): Promise<AnimationMetrics> {
|
||||
return await measureAnimationPerformance(page, async () => {
|
||||
await page.click('.theme-switch-btn');
|
||||
}, 500);
|
||||
}
|
||||
|
||||
async function measurePageTransitionAnimation(page: Page, targetUrl: string): Promise<AnimationMetrics> {
|
||||
return await measureAnimationPerformance(page, async () => {
|
||||
await page.click(`[data-href="${targetUrl}"]`);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
async function measureComponentAnimation(page: Page, selector: string): Promise<AnimationMetrics> {
|
||||
return await measureAnimationPerformance(page, async () => {
|
||||
await page.click(selector);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
test.describe('主题切换动画性能测试', () => {
|
||||
test('浅色主题切换到深色主题FPS应大于30', async ({ page }) => {
|
||||
await page.goto('/pages/user/index');
|
||||
await page.waitForSelector('.theme-switch-btn');
|
||||
|
||||
const metrics = await measureThemeSwitchAnimation(page);
|
||||
|
||||
console.log('主题切换动画指标:', metrics);
|
||||
|
||||
expect(metrics.fps).toBeGreaterThan(ANIMATION_THRESHOLDS.fps);
|
||||
});
|
||||
|
||||
test('深色主题切换到浅色主题FPS应大于30', async ({ page }) => {
|
||||
await page.goto('/pages/user/index');
|
||||
await page.waitForSelector('.theme-switch-btn');
|
||||
|
||||
await page.click('.theme-switch-btn');
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
const metrics = await measureThemeSwitchAnimation(page);
|
||||
|
||||
console.log('主题切换动画指标:', metrics);
|
||||
|
||||
expect(metrics.fps).toBeGreaterThan(ANIMATION_THRESHOLDS.fps);
|
||||
});
|
||||
|
||||
test('主题切换掉帧数应小于5', async ({ page }) => {
|
||||
await page.goto('/pages/user/index');
|
||||
await page.waitForSelector('.theme-switch-btn');
|
||||
|
||||
const metrics = await measureThemeSwitchAnimation(page);
|
||||
|
||||
console.log('主题切换掉帧数:', metrics.droppedFrames);
|
||||
|
||||
expect(metrics.droppedFrames).toBeLessThan(5);
|
||||
});
|
||||
|
||||
test('主题切换帧时间应小于33.33ms', async ({ page }) => {
|
||||
await page.goto('/pages/user/index');
|
||||
await page.waitForSelector('.theme-switch-btn');
|
||||
|
||||
const metrics = await measureThemeSwitchAnimation(page);
|
||||
|
||||
console.log('主题切换帧时间:', metrics.frameTime);
|
||||
|
||||
expect(metrics.frameTime).toBeLessThan(ANIMATION_THRESHOLDS.frameTime);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('页面切换动画性能测试', () => {
|
||||
test('首页切换到日历页FPS应大于30', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.waitForSelector('[data-href="/pages/calendar/index"]');
|
||||
|
||||
const metrics = await measurePageTransitionAnimation(page, '/pages/calendar/index');
|
||||
|
||||
console.log('页面切换动画指标:', metrics);
|
||||
|
||||
expect(metrics.fps).toBeGreaterThan(ANIMATION_THRESHOLDS.fps);
|
||||
});
|
||||
|
||||
test('首页切换到黄历页FPS应大于30', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.waitForSelector('[data-href="/pages/almanac/index"]');
|
||||
|
||||
const metrics = await measurePageTransitionAnimation(page, '/pages/almanac/index');
|
||||
|
||||
console.log('页面切换动画指标:', metrics);
|
||||
|
||||
expect(metrics.fps).toBeGreaterThan(ANIMATION_THRESHOLDS.fps);
|
||||
});
|
||||
|
||||
test('首页切换到用户页FPS应大于30', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.waitForSelector('[data-href="/pages/user/index"]');
|
||||
|
||||
const metrics = await measurePageTransitionAnimation(page, '/pages/user/index');
|
||||
|
||||
console.log('页面切换动画指标:', metrics);
|
||||
|
||||
expect(metrics.fps).toBeGreaterThan(ANIMATION_THRESHOLDS.fps);
|
||||
});
|
||||
|
||||
test('首页切换到搜索页FPS应大于30', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.waitForSelector('[data-href="/pages/search/index"]');
|
||||
|
||||
const metrics = await measurePageTransitionAnimation(page, '/pages/search/index');
|
||||
|
||||
console.log('页面切换动画指标:', metrics);
|
||||
|
||||
expect(metrics.fps).toBeGreaterThan(ANIMATION_THRESHOLDS.fps);
|
||||
});
|
||||
|
||||
test('日历页切换到黄历页FPS应大于30', async ({ page }) => {
|
||||
await page.goto('/pages/calendar/index');
|
||||
await page.waitForSelector('[data-href="/pages/almanac/index"]');
|
||||
|
||||
const metrics = await measurePageTransitionAnimation(page, '/pages/almanac/index');
|
||||
|
||||
console.log('页面切换动画指标:', metrics);
|
||||
|
||||
expect(metrics.fps).toBeGreaterThan(ANIMATION_THRESHOLDS.fps);
|
||||
});
|
||||
|
||||
test('黄历页切换到日历页FPS应大于30', async ({ page }) => {
|
||||
await page.goto('/pages/almanac/index');
|
||||
await page.waitForSelector('[data-href="/pages/calendar/index"]');
|
||||
|
||||
const metrics = await measurePageTransitionAnimation(page, '/pages/calendar/index');
|
||||
|
||||
console.log('页面切换动画指标:', metrics);
|
||||
|
||||
expect(metrics.fps).toBeGreaterThan(ANIMATION_THRESHOLDS.fps);
|
||||
});
|
||||
|
||||
test('页面切换掉帧数应小于5', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.waitForSelector('[data-href="/pages/calendar/index"]');
|
||||
|
||||
const metrics = await measurePageTransitionAnimation(page, '/pages/calendar/index');
|
||||
|
||||
console.log('页面切换掉帧数:', metrics.droppedFrames);
|
||||
|
||||
expect(metrics.droppedFrames).toBeLessThan(5);
|
||||
});
|
||||
|
||||
test('页面切换帧时间应小于33.33ms', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.waitForSelector('[data-href="/pages/calendar/index"]');
|
||||
|
||||
const metrics = await measurePageTransitionAnimation(page, '/pages/calendar/index');
|
||||
|
||||
console.log('页面切换帧时间:', metrics.frameTime);
|
||||
|
||||
expect(metrics.frameTime).toBeLessThan(ANIMATION_THRESHOLDS.frameTime);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('组件动画性能测试', () => {
|
||||
test('日历月切换动画FPS应大于30', async ({ page }) => {
|
||||
await page.goto('/pages/calendar/index');
|
||||
await page.waitForSelector('.calendar-next-month');
|
||||
|
||||
const metrics = await measureComponentAnimation(page, '.calendar-next-month');
|
||||
|
||||
console.log('日历月切换动画指标:', metrics);
|
||||
|
||||
expect(metrics.fps).toBeGreaterThan(ANIMATION_THRESHOLDS.fps);
|
||||
});
|
||||
|
||||
test('日历年切换动画FPS应大于30', async ({ page }) => {
|
||||
await page.goto('/pages/calendar/index');
|
||||
await page.waitForSelector('.calendar-year-selector');
|
||||
|
||||
await page.click('.calendar-year-selector');
|
||||
await page.waitForSelector('.calendar-year-option[data-year="2027"]');
|
||||
|
||||
const metrics = await measureComponentAnimation(page, '.calendar-year-option[data-year="2027"]');
|
||||
|
||||
console.log('日历年切换动画指标:', metrics);
|
||||
|
||||
expect(metrics.fps).toBeGreaterThan(ANIMATION_THRESHOLDS.fps);
|
||||
});
|
||||
|
||||
test('黄历月切换动画FPS应大于30', async ({ page }) => {
|
||||
await page.goto('/pages/almanac/index');
|
||||
await page.waitForSelector('.almanac-next-month');
|
||||
|
||||
const metrics = await measureComponentAnimation(page, '.almanac-next-month');
|
||||
|
||||
console.log('黄历月切换动画指标:', metrics);
|
||||
|
||||
expect(metrics.fps).toBeGreaterThan(ANIMATION_THRESHOLDS.fps);
|
||||
});
|
||||
|
||||
test('搜索建议展开动画FPS应大于30', async ({ page }) => {
|
||||
await page.goto('/pages/search/index');
|
||||
await page.waitForSelector('.search-input');
|
||||
|
||||
await page.fill('.search-input', '春');
|
||||
await page.waitForSelector('.search-suggestions');
|
||||
|
||||
const metrics = await measureComponentAnimation(page, '.search-suggestions');
|
||||
|
||||
console.log('搜索建议展开动画指标:', metrics);
|
||||
|
||||
expect(metrics.fps).toBeGreaterThan(ANIMATION_THRESHOLDS.fps);
|
||||
});
|
||||
|
||||
test('搜索历史展开动画FPS应大于30', async ({ page }) => {
|
||||
await page.goto('/pages/search/index');
|
||||
await page.waitForSelector('.search-history-btn');
|
||||
|
||||
const metrics = await measureComponentAnimation(page, '.search-history-btn');
|
||||
|
||||
console.log('搜索历史展开动画指标:', metrics);
|
||||
|
||||
expect(metrics.fps).toBeGreaterThan(ANIMATION_THRESHOLDS.fps);
|
||||
});
|
||||
|
||||
test('搜索热门展开动画FPS应大于30', async ({ page }) => {
|
||||
await page.goto('/pages/search/index');
|
||||
await page.waitForSelector('.search-hot-btn');
|
||||
|
||||
const metrics = await measureComponentAnimation(page, '.search-hot-btn');
|
||||
|
||||
console.log('搜索热门展开动画指标:', metrics);
|
||||
|
||||
expect(metrics.fps).toBeGreaterThan(ANIMATION_THRESHOLDS.fps);
|
||||
});
|
||||
|
||||
test('用户设置展开动画FPS应大于30', async ({ page }) => {
|
||||
await page.goto('/pages/user/index');
|
||||
await page.waitForSelector('.user-settings-btn');
|
||||
|
||||
const metrics = await measureComponentAnimation(page, '.user-settings-btn');
|
||||
|
||||
console.log('用户设置展开动画指标:', metrics);
|
||||
|
||||
expect(metrics.fps).toBeGreaterThan(ANIMATION_THRESHOLDS.fps);
|
||||
});
|
||||
|
||||
test('组件动画掉帧数应小于5', async ({ page }) => {
|
||||
await page.goto('/pages/calendar/index');
|
||||
await page.waitForSelector('.calendar-next-month');
|
||||
|
||||
const metrics = await measureComponentAnimation(page, '.calendar-next-month');
|
||||
|
||||
console.log('组件动画掉帧数:', metrics.droppedFrames);
|
||||
|
||||
expect(metrics.droppedFrames).toBeLessThan(5);
|
||||
});
|
||||
|
||||
test('组件动画帧时间应小于33.33ms', async ({ page }) => {
|
||||
await page.goto('/pages/calendar/index');
|
||||
await page.waitForSelector('.calendar-next-month');
|
||||
|
||||
const metrics = await measureComponentAnimation(page, '.calendar-next-month');
|
||||
|
||||
console.log('组件动画帧时间:', metrics.frameTime);
|
||||
|
||||
expect(metrics.frameTime).toBeLessThan(ANIMATION_THRESHOLDS.frameTime);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('滚动动画性能测试', () => {
|
||||
test('日历列表滚动FPS应大于30', async ({ page }) => {
|
||||
await page.goto('/pages/calendar/index');
|
||||
await page.waitForSelector('.calendar-container');
|
||||
|
||||
const metrics = await measureAnimationPerformance(page, async () => {
|
||||
await page.evaluate(() => {
|
||||
const container = document.querySelector('.calendar-container');
|
||||
if (container) {
|
||||
container.scrollTop = 500;
|
||||
}
|
||||
});
|
||||
}, 500);
|
||||
|
||||
console.log('日历列表滚动动画指标:', metrics);
|
||||
|
||||
expect(metrics.fps).toBeGreaterThan(ANIMATION_THRESHOLDS.fps);
|
||||
});
|
||||
|
||||
test('黄历列表滚动FPS应大于30', async ({ page }) => {
|
||||
await page.goto('/pages/almanac/index');
|
||||
await page.waitForSelector('.almanac-container');
|
||||
|
||||
const metrics = await measureAnimationPerformance(page, async () => {
|
||||
await page.evaluate(() => {
|
||||
const container = document.querySelector('.almanac-container');
|
||||
if (container) {
|
||||
container.scrollTop = 500;
|
||||
}
|
||||
});
|
||||
}, 500);
|
||||
|
||||
console.log('黄历列表滚动动画指标:', metrics);
|
||||
|
||||
expect(metrics.fps).toBeGreaterThan(ANIMATION_THRESHOLDS.fps);
|
||||
});
|
||||
|
||||
test('搜索结果滚动FPS应大于30', 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 metrics = await measureAnimationPerformance(page, async () => {
|
||||
await page.evaluate(() => {
|
||||
const container = document.querySelector('.search-results');
|
||||
if (container) {
|
||||
container.scrollTop = 500;
|
||||
}
|
||||
});
|
||||
}, 500);
|
||||
|
||||
console.log('搜索结果滚动动画指标:', metrics);
|
||||
|
||||
expect(metrics.fps).toBeGreaterThan(ANIMATION_THRESHOLDS.fps);
|
||||
});
|
||||
|
||||
test('滚动掉帧数应小于5', async ({ page }) => {
|
||||
await page.goto('/pages/calendar/index');
|
||||
await page.waitForSelector('.calendar-container');
|
||||
|
||||
const metrics = await measureAnimationPerformance(page, async () => {
|
||||
await page.evaluate(() => {
|
||||
const container = document.querySelector('.calendar-container');
|
||||
if (container) {
|
||||
container.scrollTop = 500;
|
||||
}
|
||||
});
|
||||
}, 500);
|
||||
|
||||
console.log('滚动掉帧数:', metrics.droppedFrames);
|
||||
|
||||
expect(metrics.droppedFrames).toBeLessThan(5);
|
||||
});
|
||||
|
||||
test('滚动帧时间应小于33.33ms', async ({ page }) => {
|
||||
await page.goto('/pages/calendar/index');
|
||||
await page.waitForSelector('.calendar-container');
|
||||
|
||||
const metrics = await measureAnimationPerformance(page, async () => {
|
||||
await page.evaluate(() => {
|
||||
const container = document.querySelector('.calendar-container');
|
||||
if (container) {
|
||||
container.scrollTop = 500;
|
||||
}
|
||||
});
|
||||
}, 500);
|
||||
|
||||
console.log('滚动帧时间:', metrics.frameTime);
|
||||
|
||||
expect(metrics.frameTime).toBeLessThan(ANIMATION_THRESHOLDS.frameTime);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,269 @@
|
||||
export interface PerformanceThresholds {
|
||||
pageLoadTime: number;
|
||||
firstContentfulPaint: number;
|
||||
largestContentfulPaint: number;
|
||||
timeToInteractive: number;
|
||||
cumulativeLayoutShift: number;
|
||||
firstInputDelay: number;
|
||||
fps: number;
|
||||
frameTime: number;
|
||||
animationDuration: number;
|
||||
}
|
||||
|
||||
export const PERFORMANCE_THRESHOLDS: PerformanceThresholds = {
|
||||
pageLoadTime: 2000,
|
||||
firstContentfulPaint: 1000,
|
||||
largestContentfulPaint: 1500,
|
||||
timeToInteractive: 2000,
|
||||
cumulativeLayoutShift: 0.1,
|
||||
firstInputDelay: 100,
|
||||
fps: 30,
|
||||
frameTime: 33.33,
|
||||
animationDuration: 500,
|
||||
};
|
||||
|
||||
export interface PerformanceMetrics {
|
||||
pageLoadTime: number;
|
||||
firstContentfulPaint: number;
|
||||
largestContentfulPaint: number;
|
||||
timeToInteractive: number;
|
||||
cumulativeLayoutShift: number;
|
||||
firstInputDelay: number;
|
||||
fps: number;
|
||||
frameTime: number;
|
||||
droppedFrames: number;
|
||||
totalFrames: number;
|
||||
animationDuration: number;
|
||||
}
|
||||
|
||||
export interface PerformanceReport {
|
||||
timestamp: string;
|
||||
url: string;
|
||||
platform: string;
|
||||
metrics: PerformanceMetrics;
|
||||
thresholds: PerformanceThresholds;
|
||||
passed: boolean;
|
||||
issues: PerformanceIssue[];
|
||||
}
|
||||
|
||||
export interface PerformanceIssue {
|
||||
type: 'page-load' | 'animation' | 'rendering';
|
||||
metric: string;
|
||||
actual: number;
|
||||
expected: number;
|
||||
severity: 'critical' | 'warning' | 'info';
|
||||
message: string;
|
||||
}
|
||||
|
||||
export class PerformanceMonitor {
|
||||
private metrics: PerformanceMetrics;
|
||||
private thresholds: PerformanceThresholds;
|
||||
private issues: PerformanceIssue[];
|
||||
|
||||
constructor(thresholds?: Partial<PerformanceThresholds>) {
|
||||
this.thresholds = { ...PERFORMANCE_THRESHOLDS, ...thresholds };
|
||||
this.metrics = this.initializeMetrics();
|
||||
this.issues = [];
|
||||
}
|
||||
|
||||
private initializeMetrics(): PerformanceMetrics {
|
||||
return {
|
||||
pageLoadTime: 0,
|
||||
firstContentfulPaint: 0,
|
||||
largestContentfulPaint: 0,
|
||||
timeToInteractive: 0,
|
||||
cumulativeLayoutShift: 0,
|
||||
firstInputDelay: 0,
|
||||
fps: 0,
|
||||
frameTime: 0,
|
||||
droppedFrames: 0,
|
||||
totalFrames: 0,
|
||||
animationDuration: 0,
|
||||
};
|
||||
}
|
||||
|
||||
public setMetric(key: keyof PerformanceMetrics, value: number): void {
|
||||
this.metrics[key] = value;
|
||||
}
|
||||
|
||||
public getMetric(key: keyof PerformanceMetrics): number {
|
||||
return this.metrics[key];
|
||||
}
|
||||
|
||||
public getMetrics(): PerformanceMetrics {
|
||||
return { ...this.metrics };
|
||||
}
|
||||
|
||||
public checkThresholds(): void {
|
||||
this.issues = [];
|
||||
|
||||
if (this.metrics.pageLoadTime > this.thresholds.pageLoadTime) {
|
||||
this.issues.push({
|
||||
type: 'page-load',
|
||||
metric: 'pageLoadTime',
|
||||
actual: this.metrics.pageLoadTime,
|
||||
expected: this.thresholds.pageLoadTime,
|
||||
severity: 'critical',
|
||||
message: `页面加载时间 ${this.metrics.pageLoadTime}ms 超过阈值 ${this.thresholds.pageLoadTime}ms`,
|
||||
});
|
||||
}
|
||||
|
||||
if (this.metrics.firstContentfulPaint > this.thresholds.firstContentfulPaint) {
|
||||
this.issues.push({
|
||||
type: 'page-load',
|
||||
metric: 'firstContentfulPaint',
|
||||
actual: this.metrics.firstContentfulPaint,
|
||||
expected: this.thresholds.firstContentfulPaint,
|
||||
severity: 'warning',
|
||||
message: `首次内容绘制 ${this.metrics.firstContentfulPaint}ms 超过阈值 ${this.thresholds.firstContentfulPaint}ms`,
|
||||
});
|
||||
}
|
||||
|
||||
if (this.metrics.largestContentfulPaint > this.thresholds.largestContentfulPaint) {
|
||||
this.issues.push({
|
||||
type: 'page-load',
|
||||
metric: 'largestContentfulPaint',
|
||||
actual: this.metrics.largestContentfulPaint,
|
||||
expected: this.thresholds.largestContentfulPaint,
|
||||
severity: 'warning',
|
||||
message: `最大内容绘制 ${this.metrics.largestContentfulPaint}ms 超过阈值 ${this.thresholds.largestContentfulPaint}ms`,
|
||||
});
|
||||
}
|
||||
|
||||
if (this.metrics.timeToInteractive > this.thresholds.timeToInteractive) {
|
||||
this.issues.push({
|
||||
type: 'page-load',
|
||||
metric: 'timeToInteractive',
|
||||
actual: this.metrics.timeToInteractive,
|
||||
expected: this.thresholds.timeToInteractive,
|
||||
severity: 'warning',
|
||||
message: `可交互时间 ${this.metrics.timeToInteractive}ms 超过阈值 ${this.thresholds.timeToInteractive}ms`,
|
||||
});
|
||||
}
|
||||
|
||||
if (this.metrics.cumulativeLayoutShift > this.thresholds.cumulativeLayoutShift) {
|
||||
this.issues.push({
|
||||
type: 'rendering',
|
||||
metric: 'cumulativeLayoutShift',
|
||||
actual: this.metrics.cumulativeLayoutShift,
|
||||
expected: this.thresholds.cumulativeLayoutShift,
|
||||
severity: 'warning',
|
||||
message: `累积布局偏移 ${this.metrics.cumulativeLayoutShift} 超过阈值 ${this.thresholds.cumulativeLayoutShift}`,
|
||||
});
|
||||
}
|
||||
|
||||
if (this.metrics.firstInputDelay > this.thresholds.firstInputDelay) {
|
||||
this.issues.push({
|
||||
type: 'page-load',
|
||||
metric: 'firstInputDelay',
|
||||
actual: this.metrics.firstInputDelay,
|
||||
expected: this.thresholds.firstInputDelay,
|
||||
severity: 'warning',
|
||||
message: `首次输入延迟 ${this.metrics.firstInputDelay}ms 超过阈值 ${this.thresholds.firstInputDelay}ms`,
|
||||
});
|
||||
}
|
||||
|
||||
if (this.metrics.fps < this.thresholds.fps) {
|
||||
this.issues.push({
|
||||
type: 'animation',
|
||||
metric: 'fps',
|
||||
actual: this.metrics.fps,
|
||||
expected: this.thresholds.fps,
|
||||
severity: 'critical',
|
||||
message: `动画帧率 ${this.metrics.fps}fps 低于阈值 ${this.thresholds.fps}fps`,
|
||||
});
|
||||
}
|
||||
|
||||
if (this.metrics.frameTime > this.thresholds.frameTime) {
|
||||
this.issues.push({
|
||||
type: 'animation',
|
||||
metric: 'frameTime',
|
||||
actual: this.metrics.frameTime,
|
||||
expected: this.thresholds.frameTime,
|
||||
severity: 'warning',
|
||||
message: `帧时间 ${this.metrics.frameTime}ms 超过阈值 ${this.thresholds.frameTime}ms`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public getIssues(): PerformanceIssue[] {
|
||||
return [...this.issues];
|
||||
}
|
||||
|
||||
public hasCriticalIssues(): boolean {
|
||||
return this.issues.some(issue => issue.severity === 'critical');
|
||||
}
|
||||
|
||||
public hasWarnings(): boolean {
|
||||
return this.issues.some(issue => issue.severity === 'warning');
|
||||
}
|
||||
|
||||
public generateReport(url: string, platform: string): PerformanceReport {
|
||||
this.checkThresholds();
|
||||
|
||||
return {
|
||||
timestamp: new Date().toISOString(),
|
||||
url,
|
||||
platform,
|
||||
metrics: this.getMetrics(),
|
||||
thresholds: this.thresholds,
|
||||
passed: !this.hasCriticalIssues(),
|
||||
issues: this.getIssues(),
|
||||
};
|
||||
}
|
||||
|
||||
public reset(): void {
|
||||
this.metrics = this.initializeMetrics();
|
||||
this.issues = [];
|
||||
}
|
||||
}
|
||||
|
||||
export const createPerformanceMonitor = (thresholds?: Partial<PerformanceThresholds>): PerformanceMonitor => {
|
||||
return new PerformanceMonitor(thresholds);
|
||||
};
|
||||
|
||||
export const generatePerformanceReportSummary = (reports: PerformanceReport[]): string => {
|
||||
const totalReports = reports.length;
|
||||
const passedReports = reports.filter(report => report.passed).length;
|
||||
const failedReports = totalReports - passedReports;
|
||||
|
||||
const totalIssues = reports.reduce((sum, report) => sum + report.issues.length, 0);
|
||||
const criticalIssues = reports.reduce((sum, report) => sum + report.issues.filter(issue => issue.severity === 'critical').length, 0);
|
||||
const warningIssues = reports.reduce((sum, report) => sum + report.issues.filter(issue => issue.severity === 'warning').length, 0);
|
||||
|
||||
const avgPageLoadTime = reports.reduce((sum, report) => sum + report.metrics.pageLoadTime, 0) / totalReports;
|
||||
const avgFPS = reports.reduce((sum, report) => sum + report.metrics.fps, 0) / totalReports;
|
||||
|
||||
return `
|
||||
性能测试报告摘要
|
||||
================
|
||||
测试时间: ${new Date().toISOString()}
|
||||
测试数量: ${totalReports}
|
||||
通过数量: ${passedReports}
|
||||
失败数量: ${failedReports}
|
||||
通过率: ${((passedReports / totalReports) * 100).toFixed(2)}%
|
||||
|
||||
问题统计
|
||||
--------
|
||||
总问题数: ${totalIssues}
|
||||
严重问题: ${criticalIssues}
|
||||
警告问题: ${warningIssues}
|
||||
|
||||
性能指标
|
||||
--------
|
||||
平均页面加载时间: ${avgPageLoadTime.toFixed(2)}ms
|
||||
平均动画帧率: ${avgFPS.toFixed(2)}fps
|
||||
|
||||
详细信息
|
||||
--------
|
||||
${reports.map((report, index) => `
|
||||
报告 ${index + 1}: ${report.url}
|
||||
- 平台: ${report.platform}
|
||||
- 状态: ${report.passed ? '通过' : '失败'}
|
||||
- 页面加载时间: ${report.metrics.pageLoadTime}ms
|
||||
- 动画帧率: ${report.metrics.fps}fps
|
||||
- 问题数: ${report.issues.length}
|
||||
${report.issues.length > 0 ? ` 问题详情:\n${report.issues.map(issue => ` - [${issue.severity.toUpperCase()}] ${issue.message}`).join('\n')}` : ''}
|
||||
`).join('\n')}
|
||||
`;
|
||||
};
|
||||
@@ -0,0 +1,502 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,175 @@
|
||||
const { execSync } = require('child_process');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const RESULTS_DIR = path.join(__dirname, '../../test-results/performance');
|
||||
const REPORT_FILE = path.join(RESULTS_DIR, 'performance-report.json');
|
||||
const SUMMARY_FILE = path.join(RESULTS_DIR, 'performance-summary.txt');
|
||||
|
||||
function ensureResultsDir() {
|
||||
if (!fs.existsSync(RESULTS_DIR)) {
|
||||
fs.mkdirSync(RESULTS_DIR, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
function runPerformanceTests() {
|
||||
console.log('开始运行性能测试...');
|
||||
console.log('='.repeat(50));
|
||||
|
||||
ensureResultsDir();
|
||||
|
||||
try {
|
||||
const startTime = Date.now();
|
||||
|
||||
execSync('npx playwright test e2e/performance/page-load.spec.ts --reporter=json --reporter-file=test-results/performance/page-load.json', {
|
||||
stdio: 'inherit',
|
||||
cwd: path.join(__dirname, '../..'),
|
||||
});
|
||||
|
||||
execSync('npx playwright test e2e/performance/animation.spec.ts --reporter=json --reporter-file=test-results/performance/animation.json', {
|
||||
stdio: 'inherit',
|
||||
cwd: path.join(__dirname, '../..'),
|
||||
});
|
||||
|
||||
const endTime = Date.now();
|
||||
const duration = ((endTime - startTime) / 1000).toFixed(2);
|
||||
|
||||
console.log('='.repeat(50));
|
||||
console.log(`性能测试完成,耗时: ${duration}秒`);
|
||||
console.log('='.repeat(50));
|
||||
|
||||
generateSummary();
|
||||
|
||||
return { success: true, duration };
|
||||
} catch (error) {
|
||||
console.error('性能测试失败:', error.message);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
function generateSummary() {
|
||||
console.log('生成性能测试摘要...');
|
||||
|
||||
const pageLoadResults = readTestResults('page-load.json');
|
||||
const animationResults = readTestResults('animation.json');
|
||||
|
||||
const summary = generatePerformanceSummary(pageLoadResults, animationResults);
|
||||
|
||||
fs.writeFileSync(SUMMARY_FILE, summary, 'utf-8');
|
||||
|
||||
console.log('性能测试摘要已生成:', SUMMARY_FILE);
|
||||
}
|
||||
|
||||
function readTestResults(filename) {
|
||||
const filePath = path.join(RESULTS_DIR, filename);
|
||||
|
||||
if (!fs.existsSync(filePath)) {
|
||||
return { suites: [], specs: [], tests: [] };
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
return JSON.parse(content);
|
||||
}
|
||||
|
||||
function generatePerformanceSummary(pageLoadResults, animationResults) {
|
||||
const pageLoadTests = pageLoadResults.tests || [];
|
||||
const animationTests = animationResults.tests || [];
|
||||
|
||||
const totalTests = pageLoadTests.length + animationTests.length;
|
||||
const passedTests = [...pageLoadTests, ...animationTests].filter(test => test.status === 'passed').length;
|
||||
const failedTests = totalTests - passedTests;
|
||||
|
||||
const summary = `
|
||||
性能测试报告摘要
|
||||
================
|
||||
测试时间: ${new Date().toISOString()}
|
||||
测试数量: ${totalTests}
|
||||
通过数量: ${passedTests}
|
||||
失败数量: ${failedTests}
|
||||
通过率: ${((passedTests / totalTests) * 100).toFixed(2)}%
|
||||
|
||||
页面加载性能测试
|
||||
----------------
|
||||
测试数量: ${pageLoadTests.length}
|
||||
通过数量: ${pageLoadTests.filter(test => test.status === 'passed').length}
|
||||
失败数量: ${pageLoadTests.filter(test => test.status === 'failed').length}
|
||||
|
||||
动画性能测试
|
||||
------------
|
||||
测试数量: ${animationTests.length}
|
||||
通过数量: ${animationTests.filter(test => test.status === 'passed').length}
|
||||
失败数量: ${animationTests.filter(test => test.status === 'failed').length}
|
||||
|
||||
失败测试详情
|
||||
------------
|
||||
${[...pageLoadTests, ...animationTests]
|
||||
.filter(test => test.status === 'failed')
|
||||
.map((test, index) => `
|
||||
${index + 1}. ${test.title}
|
||||
- 文件: ${test.location.file}
|
||||
- 行号: ${test.location.line}
|
||||
- 错误: ${test.error?.message || '未知错误'}
|
||||
`).join('\n')}
|
||||
|
||||
性能指标
|
||||
--------
|
||||
页面加载性能阈值:
|
||||
- 页面加载时间: < 2000ms
|
||||
- 首次内容绘制: < 1000ms
|
||||
- 最大内容绘制: < 1500ms
|
||||
- 可交互时间: < 2000ms
|
||||
- 累积布局偏移: < 0.1
|
||||
- 首次输入延迟: < 100ms
|
||||
|
||||
动画性能阈值:
|
||||
- 动画帧率: > 30fps
|
||||
- 帧时间: < 33.33ms
|
||||
- 动画持续时间: < 500ms
|
||||
|
||||
建议
|
||||
----
|
||||
${failedTests > 0 ? `
|
||||
1. 优先修复失败的测试用例
|
||||
2. 检查性能指标是否超过阈值
|
||||
3. 优化页面加载性能
|
||||
4. 优化动画性能
|
||||
` : `
|
||||
1. 持续监控性能指标
|
||||
2. 定期运行性能测试
|
||||
3. 记录性能基线
|
||||
4. 及时发现性能退化
|
||||
`}
|
||||
`;
|
||||
|
||||
return summary;
|
||||
}
|
||||
|
||||
function main() {
|
||||
const args = process.argv.slice(2);
|
||||
const command = args[0] || 'run';
|
||||
|
||||
switch (command) {
|
||||
case 'run':
|
||||
const result = runPerformanceTests();
|
||||
process.exit(result.success ? 0 : 1);
|
||||
break;
|
||||
case 'summary':
|
||||
generateSummary();
|
||||
process.exit(0);
|
||||
break;
|
||||
default:
|
||||
console.log('用法: node run-tests.js [run|summary]');
|
||||
console.log(' run - 运行性能测试');
|
||||
console.log(' summary - 生成性能测试摘要');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
main();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
runPerformanceTests,
|
||||
generateSummary,
|
||||
};
|
||||
Reference in New Issue
Block a user