feat(admin): 添加用户管理相关文件

添加用户管理视图、API和状态管理文件
This commit is contained in:
张翔
2026-03-28 14:37:29 +08:00
commit 08ea5fbe98
1643 changed files with 255646 additions and 0 deletions
@@ -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,
};