Files
张翔 08ea5fbe98 feat(admin): 添加用户管理相关文件
添加用户管理视图、API和状态管理文件
2026-03-28 14:37:29 +08:00

394 lines
12 KiB
TypeScript

import { Page, Locator, PageScreenshotOptions } from '@playwright/test';
import { testLogger } from '../shared/utils/test-logger';
import * as path from 'path';
import * as fs from 'fs';
export interface ScreenshotConfig {
outputDir: string;
filename: string;
fullPage: boolean;
quality?: number;
type: 'png' | 'jpeg';
timeout: number;
}
export interface ScreenshotMetadata {
filename: string;
path: string;
timestamp: string;
testName?: string;
description?: string;
}
export class ScreenshotHelper {
private page: Page;
private outputDir: string;
private defaultConfig: Partial<ScreenshotConfig>;
private screenshots: Map<string, ScreenshotMetadata> = new Map();
constructor(page: Page, outputDir: string = 'test-results/screenshots') {
this.page = page;
this.outputDir = outputDir;
this.defaultConfig = {
outputDir,
fullPage: false,
type: 'png',
timeout: 5000
};
this.ensureOutputDir();
testLogger.info(`ScreenshotHelper initialized with output dir: ${outputDir}`);
}
setDefaultConfig(config: Partial<ScreenshotConfig>): void {
this.defaultConfig = { ...this.defaultConfig, ...config };
testLogger.debug('Default screenshot config updated');
}
async capture(config: Partial<ScreenshotConfig> = {}): Promise<string> {
const finalConfig = { ...this.defaultConfig, ...config };
const filename = this.generateFilename(finalConfig.filename);
const filePath = path.join(finalConfig.outputDir || this.outputDir, filename);
testLogger.info(`Capturing screenshot: ${filename}`);
const options: PageScreenshotOptions = {
path: filePath,
type: finalConfig.type,
fullPage: finalConfig.fullPage,
timeout: finalConfig.timeout
};
if (finalConfig.quality && finalConfig.type === 'jpeg') {
options.quality = finalConfig.quality;
}
await this.page.screenshot(options);
const metadata: ScreenshotMetadata = {
filename,
path: filePath,
timestamp: new Date().toISOString()
};
this.screenshots.set(filename, metadata);
testLogger.info(`Screenshot captured: ${filePath}`);
return filePath;
}
async captureElement(locator: Locator, config: Partial<ScreenshotConfig> = {}): Promise<string> {
const finalConfig = { ...this.defaultConfig, ...config };
const filename = this.generateFilename(finalConfig.filename);
const filePath = path.join(finalConfig.outputDir || this.outputDir, filename);
testLogger.info(`Capturing element screenshot: ${filename}`);
const options: PageScreenshotOptions = {
path: filePath,
type: finalConfig.type,
timeout: finalConfig.timeout
};
if (finalConfig.quality && finalConfig.type === 'jpeg') {
options.quality = finalConfig.quality;
}
await locator.screenshot(options);
const metadata: ScreenshotMetadata = {
filename,
path: filePath,
timestamp: new Date().toISOString()
};
this.screenshots.set(filename, metadata);
testLogger.info(`Element screenshot captured: ${filePath}`);
return filePath;
}
async captureFullPage(config: Partial<ScreenshotConfig> = {}): Promise<string> {
return this.capture({ ...config, fullPage: true });
}
async captureViewport(config: Partial<ScreenshotConfig> = {}): Promise<string> {
return this.capture({ ...config, fullPage: false });
}
async captureOnFailure(testName: string, error?: Error): Promise<string> {
const filename = `failure-${testName}-${Date.now()}`;
const filePath = await this.captureFullPage({ filename });
testLogger.error(`Screenshot captured on failure: ${testName}`, error);
if (error) {
const errorLogPath = path.join(this.outputDir, `failure-${testName}-${Date.now()}.log`);
const errorLog = `
Test Name: ${testName}
Timestamp: ${new Date().toISOString()}
Error: ${error.message}
Stack Trace:
${error.stack}
`.trim();
fs.writeFileSync(errorLogPath, errorLog);
testLogger.info(`Error log saved: ${errorLogPath}`);
}
return filePath;
}
async captureWithDescription(description: string, config: Partial<ScreenshotConfig> = {}): Promise<string> {
const filename = `${description}-${Date.now()}`;
const filePath = await this.capture({ ...config, filename });
const metadata = this.screenshots.get(filename);
if (metadata) {
metadata.description = description;
this.screenshots.set(filename, metadata);
}
return filePath;
}
async captureBeforeAction(actionName: string, config: Partial<ScreenshotConfig> = {}): Promise<string> {
const filename = `before-${actionName}-${Date.now()}`;
return this.capture({ ...config, filename });
}
async captureAfterAction(actionName: string, config: Partial<ScreenshotConfig> = {}): Promise<string> {
const filename = `after-${actionName}-${Date.now()}`;
return this.capture({ ...config, filename });
}
async captureStep(stepName: string, config: Partial<ScreenshotConfig> = {}): Promise<string> {
const filename = `step-${stepName}-${Date.now()}`;
return this.capture({ ...config, filename });
}
async captureMultiple(configs: Partial<ScreenshotConfig>[]): Promise<string[]> {
testLogger.info(`Capturing ${configs.length} screenshots`);
const filePaths: string[] = [];
for (let i = 0; i < configs.length; i++) {
const filePath = await this.capture(configs[i]);
filePaths.push(filePath);
}
testLogger.info(`Captured ${filePaths.length} screenshots`);
return filePaths;
}
async captureWithDelay(delay: number, config: Partial<ScreenshotConfig> = {}): Promise<string> {
testLogger.info(`Waiting ${delay}ms before capturing screenshot`);
await this.page.waitForTimeout(delay);
return this.capture(config);
}
async captureOnHover(locator: Locator, config: Partial<ScreenshotConfig> = {}): Promise<string> {
testLogger.info('Capturing screenshot on hover');
await locator.hover();
await this.page.waitForTimeout(300);
return this.capture(config);
}
async captureOnFocus(locator: Locator, config: Partial<ScreenshotConfig> = {}): Promise<string> {
testLogger.info('Capturing screenshot on focus');
await locator.focus();
await this.page.waitForTimeout(300);
return this.capture(config);
}
async captureVisibleArea(config: Partial<ScreenshotConfig> = {}): Promise<string> {
testLogger.info('Capturing visible area screenshot');
const viewportSize = this.page.viewportSize();
if (viewportSize) {
const clip = {
x: 0,
y: 0,
width: viewportSize.width,
height: viewportSize.height
};
const finalConfig = { ...this.defaultConfig, ...config };
const filename = this.generateFilename(finalConfig.filename);
const filePath = path.join(finalConfig.outputDir || this.outputDir, filename);
const options: PageScreenshotOptions = {
path: filePath,
type: finalConfig.type,
clip,
timeout: finalConfig.timeout
};
if (finalConfig.quality && finalConfig.type === 'jpeg') {
options.quality = finalConfig.quality;
}
await this.page.screenshot(options);
const metadata: ScreenshotMetadata = {
filename,
path: filePath,
timestamp: new Date().toISOString()
};
this.screenshots.set(filename, metadata);
testLogger.info(`Visible area screenshot captured: ${filePath}`);
return filePath;
}
return this.captureViewport(config);
}
async captureElementBounds(locator: Locator, config: Partial<ScreenshotConfig> = {}): Promise<string> {
testLogger.info('Capturing element bounds screenshot');
const box = await locator.boundingBox();
if (box) {
const finalConfig = { ...this.defaultConfig, ...config };
const filename = this.generateFilename(finalConfig.filename);
const filePath = path.join(finalConfig.outputDir || this.outputDir, filename);
const options: PageScreenshotOptions = {
path: filePath,
type: finalConfig.type,
clip: box,
timeout: finalConfig.timeout
};
if (finalConfig.quality && finalConfig.type === 'jpeg') {
options.quality = finalConfig.quality;
}
await this.page.screenshot(options);
const metadata: ScreenshotMetadata = {
filename,
path: filePath,
timestamp: new Date().toISOString()
};
this.screenshots.set(filename, metadata);
testLogger.info(`Element bounds screenshot captured: ${filePath}`);
return filePath;
}
return this.captureElement(locator, config);
}
getScreenshotPath(filename: string): string | undefined {
const metadata = this.screenshots.get(filename);
return metadata?.path;
}
getAllScreenshots(): ScreenshotMetadata[] {
return Array.from(this.screenshots.values());
}
getScreenshotCount(): number {
return this.screenshots.size;
}
clearScreenshots(): void {
this.screenshots.clear();
testLogger.info('Screenshots cleared');
}
deleteScreenshot(filename: string): boolean {
const metadata = this.screenshots.get(filename);
if (metadata && fs.existsSync(metadata.path)) {
fs.unlinkSync(metadata.path);
this.screenshots.delete(filename);
testLogger.info(`Screenshot deleted: ${filename}`);
return true;
}
return false;
}
deleteAllScreenshots(): void {
const screenshotValues = Array.from(this.screenshots.values());
for (const metadata of screenshotValues) {
if (fs.existsSync(metadata.path)) {
fs.unlinkSync(metadata.path);
}
}
this.screenshots.clear();
testLogger.info('All screenshots deleted');
}
async compareScreenshots(beforePath: string, afterPath: string): Promise<boolean> {
testLogger.info(`Comparing screenshots: ${beforePath} vs ${afterPath}`);
const beforeExists = fs.existsSync(beforePath);
const afterExists = fs.existsSync(afterPath);
if (!beforeExists || !afterExists) {
testLogger.warn('Screenshot comparison failed: files not found');
return false;
}
const beforeStats = fs.statSync(beforePath);
const afterStats = fs.statSync(afterPath);
const areEqual = beforeStats.size === afterStats.size;
testLogger.info(`Screenshot comparison result: ${areEqual}`);
return areEqual;
}
async createScreenshotReport(): Promise<string> {
testLogger.info('Creating screenshot report');
const reportPath = path.join(this.outputDir, 'screenshot-report.md');
const screenshots = this.getAllScreenshots();
let report = '# Screenshot Report\n\n';
report += `Generated at: ${new Date().toISOString()}\n`;
report += `Total screenshots: ${screenshots.length}\n\n`;
report += '## Screenshots\n\n';
for (const screenshot of screenshots) {
report += `### ${screenshot.filename}\n`;
report += `- **Path**: ${screenshot.path}\n`;
report += `- **Timestamp**: ${screenshot.timestamp}\n`;
if (screenshot.description) {
report += `- **Description**: ${screenshot.description}\n`;
}
report += '\n';
}
fs.writeFileSync(reportPath, report);
testLogger.info(`Screenshot report created: ${reportPath}`);
return reportPath;
}
private generateFilename(filename?: string): string {
if (filename) {
return `${filename}.${this.defaultConfig.type || 'png'}`;
}
return `screenshot-${Date.now()}.${this.defaultConfig.type || 'png'}`;
}
private ensureOutputDir(): void {
if (!fs.existsSync(this.outputDir)) {
fs.mkdirSync(this.outputDir, { recursive: true });
testLogger.info(`Output directory created: ${this.outputDir}`);
}
}
}