feat(admin): 添加用户管理相关文件
添加用户管理视图、API和状态管理文件
This commit is contained in:
@@ -0,0 +1,393 @@
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user