08ea5fbe98
添加用户管理视图、API和状态管理文件
394 lines
12 KiB
TypeScript
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}`);
|
|
}
|
|
}
|
|
}
|