feat: 添加异常日志功能并优化UI样式
refactor: 重构后端查询逻辑和API响应处理 fix: 修复用户角色更新和文件上传问题 test: 添加前端性能测试脚本和E2E测试用例 chore: 更新依赖版本和配置文件 docs: 添加环境检查脚本和测试文档 style: 统一表格标签样式和路由命名 perf: 优化前端页面加载速度和响应时间
This commit is contained in:
@@ -0,0 +1,194 @@
|
||||
import { Page } from '@playwright/test';
|
||||
|
||||
export class TestDataManager {
|
||||
private readonly page: Page;
|
||||
private testData: Map<string, any> = new Map();
|
||||
private cleanupCallbacks: Array<() => Promise<void>> = [];
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
}
|
||||
|
||||
generateUniquePrefix(prefix: string): string {
|
||||
const timestamp = Date.now();
|
||||
const random = Math.random().toString(36).substring(2, 8);
|
||||
return `${prefix}_${timestamp}_${random}`;
|
||||
}
|
||||
|
||||
generateTestEmail(prefix: string = 'test'): string {
|
||||
const uniquePart = this.generateUniquePrefix(prefix);
|
||||
return `${uniquePart}@novalon-test.com`;
|
||||
}
|
||||
|
||||
generateTestUsername(prefix: string = 'testuser'): string {
|
||||
return this.generateUniquePrefix(prefix);
|
||||
}
|
||||
|
||||
generateTestFileName(prefix: string = 'testfile'): string {
|
||||
const uniquePart = this.generateUniquePrefix(prefix);
|
||||
return `${uniquePart}.txt`;
|
||||
}
|
||||
|
||||
generateTestConfigName(prefix: string = 'testconfig'): string {
|
||||
return this.generateUniquePrefix(prefix);
|
||||
}
|
||||
|
||||
generateTestDictName(prefix: string = 'testdict'): string {
|
||||
return this.generateUniquePrefix(prefix);
|
||||
}
|
||||
|
||||
generateTestNotificationTitle(prefix: string = 'testnotify'): string {
|
||||
return this.generateUniquePrefix(prefix);
|
||||
}
|
||||
|
||||
generateTestContent(prefix: string = 'content'): string {
|
||||
const timestamp = new Date().toLocaleString('zh-CN');
|
||||
return `测试内容_${prefix}_${timestamp}`;
|
||||
}
|
||||
|
||||
set(key: string, value: any): void {
|
||||
this.testData.set(key, value);
|
||||
}
|
||||
|
||||
get(key: string): any {
|
||||
return this.testData.get(key);
|
||||
}
|
||||
|
||||
has(key: string): boolean {
|
||||
return this.testData.has(key);
|
||||
}
|
||||
|
||||
remove(key: string): boolean {
|
||||
return this.testData.delete(key);
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.testData.clear();
|
||||
}
|
||||
|
||||
registerCleanup(callback: () => Promise<void>): void {
|
||||
this.cleanupCallbacks.push(callback);
|
||||
}
|
||||
|
||||
async cleanup(): Promise<void> {
|
||||
console.log('Starting test data cleanup...');
|
||||
|
||||
for (const callback of this.cleanupCallbacks) {
|
||||
try {
|
||||
await callback();
|
||||
} catch (error) {
|
||||
console.error('Cleanup callback failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
this.cleanupCallbacks = [];
|
||||
this.testData.clear();
|
||||
console.log('Test data cleanup completed');
|
||||
}
|
||||
|
||||
async cleanupTestConfigs(): Promise<void> {
|
||||
console.log('Cleaning up test configurations...');
|
||||
try {
|
||||
await this.page.goto('/system/config');
|
||||
await this.page.waitForLoadState('networkidle');
|
||||
|
||||
const testRows = this.page.locator('.el-table__row').filter({ hasText: 'test' });
|
||||
const count = await testRows.count();
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const row = testRows.nth(i);
|
||||
const deleteButton = row.locator('.el-button--danger').first();
|
||||
|
||||
if (await deleteButton.isVisible()) {
|
||||
await deleteButton.click();
|
||||
|
||||
const confirmButton = this.page.getByRole('button', { name: '确定' });
|
||||
await confirmButton.click();
|
||||
|
||||
await this.page.waitForTimeout(500);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Cleaned up ${count} test configurations`);
|
||||
} catch (error) {
|
||||
console.error('Failed to cleanup test configurations:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async cleanupTestNotifications(): Promise<void> {
|
||||
console.log('Cleaning up test notifications...');
|
||||
try {
|
||||
await this.page.goto('/system/notice');
|
||||
await this.page.waitForLoadState('networkidle');
|
||||
|
||||
const testRows = this.page.locator('.el-table__row').filter({ hasText: '测试通知' });
|
||||
const count = await testRows.count();
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const row = testRows.nth(i);
|
||||
const deleteButton = row.locator('.el-button--danger').first();
|
||||
|
||||
if (await deleteButton.isVisible()) {
|
||||
await deleteButton.click();
|
||||
|
||||
const confirmButton = this.page.getByRole('button', { name: '确定' });
|
||||
await confirmButton.click();
|
||||
|
||||
await this.page.waitForTimeout(500);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Cleaned up ${count} test notifications`);
|
||||
} catch (error) {
|
||||
console.error('Failed to cleanup test notifications:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async cleanupTestFiles(): Promise<void> {
|
||||
console.log('Cleaning up test files...');
|
||||
try {
|
||||
await this.page.goto('/files');
|
||||
await this.page.waitForLoadState('networkidle');
|
||||
|
||||
const testRows = this.page.locator('.el-table__row').filter({ hasText: 'test' });
|
||||
const count = await testRows.count();
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const row = testRows.nth(i);
|
||||
const deleteButton = row.locator('.el-button--danger').first();
|
||||
|
||||
if (await deleteButton.isVisible()) {
|
||||
await deleteButton.click();
|
||||
|
||||
const confirmButton = this.page.getByRole('button', { name: '确定' });
|
||||
await confirmButton.click();
|
||||
|
||||
await this.page.waitForTimeout(500);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Cleaned up ${count} test files`);
|
||||
} catch (error) {
|
||||
console.error('Failed to cleanup test files:', error);
|
||||
}
|
||||
}
|
||||
|
||||
createTestFileContent(fileName: string): string {
|
||||
const timestamp = new Date().toISOString();
|
||||
return `Test file created at ${timestamp}\nFilename: ${fileName}\nThis is a test file for E2E testing purposes.`;
|
||||
}
|
||||
|
||||
async setupTestData(): Promise<void> {
|
||||
console.log('Setting up test data...');
|
||||
this.set('setupTime', new Date().toISOString());
|
||||
}
|
||||
|
||||
getTestSummary(): Record<string, any> {
|
||||
return {
|
||||
testDataCount: this.testData.size,
|
||||
cleanupCallbacksCount: this.cleanupCallbacks.length,
|
||||
testDataKeys: Array.from(this.testData.keys()),
|
||||
setupTime: this.get('setupTime'),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
import { Page, expect } from '@playwright/test';
|
||||
|
||||
export class TestStabilityHelper {
|
||||
private readonly page: Page;
|
||||
private readonly maxRetries: number = 3;
|
||||
private readonly retryDelay: number = 1000;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
}
|
||||
|
||||
async waitForNetworkIdle(timeout: number = 30000): Promise<void> {
|
||||
try {
|
||||
await this.page.waitForLoadState('networkidle', { timeout });
|
||||
} catch (error) {
|
||||
console.log('Network idle timeout, continuing anyway');
|
||||
}
|
||||
}
|
||||
|
||||
async waitForElementVisible(selector: string, timeout: number = 10000): Promise<void> {
|
||||
await this.retry(async () => {
|
||||
const element = this.page.locator(selector);
|
||||
await expect(element).toBeVisible({ timeout });
|
||||
});
|
||||
}
|
||||
|
||||
async safeClick(selector: string): Promise<void> {
|
||||
await this.retry(async () => {
|
||||
const element = this.page.locator(selector);
|
||||
await element.waitFor({ state: 'visible', timeout: 10000 });
|
||||
await element.click({ timeout: 5000 });
|
||||
});
|
||||
}
|
||||
|
||||
async safeFill(selector: string, value: string): Promise<void> {
|
||||
await this.retry(async () => {
|
||||
const element = this.page.locator(selector);
|
||||
await element.waitFor({ state: 'visible', timeout: 10000 });
|
||||
await element.clear();
|
||||
await element.fill(value);
|
||||
});
|
||||
}
|
||||
|
||||
async safeSelect(selector: string, value: string): Promise<void> {
|
||||
await this.retry(async () => {
|
||||
const element = this.page.locator(selector);
|
||||
await element.waitFor({ state: 'visible', timeout: 10000 });
|
||||
await element.selectOption(value);
|
||||
});
|
||||
}
|
||||
|
||||
async waitForURL(urlPattern: RegExp | string, timeout: number = 30000): Promise<void> {
|
||||
await this.retry(async () => {
|
||||
await this.page.waitForURL(urlPattern, { timeout });
|
||||
});
|
||||
}
|
||||
|
||||
async handleModal(): Promise<void> {
|
||||
try {
|
||||
const modal = this.page.locator('.el-dialog, .el-message-box');
|
||||
const isVisible = await modal.isVisible({ timeout: 2000 });
|
||||
|
||||
if (isVisible) {
|
||||
const confirmButton = modal.locator('.el-button--primary').first();
|
||||
const cancelButton = modal.locator('.el-button--default').first();
|
||||
|
||||
if (await confirmButton.isVisible({ timeout: 1000 })) {
|
||||
await confirmButton.click();
|
||||
} else if (await cancelButton.isVisible({ timeout: 1000 })) {
|
||||
await cancelButton.click();
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('No modal found or modal handling failed');
|
||||
}
|
||||
}
|
||||
|
||||
async waitForLoadingComplete(): Promise<void> {
|
||||
try {
|
||||
const loading = this.page.locator('.el-loading-mask, .loading');
|
||||
await loading.waitFor({ state: 'hidden', timeout: 10000 });
|
||||
} catch (error) {
|
||||
console.log('Loading element not found or timeout');
|
||||
}
|
||||
}
|
||||
|
||||
async safeNavigate(url: string): Promise<void> {
|
||||
await this.retry(async () => {
|
||||
await this.page.goto(url, { waitUntil: 'networkidle', timeout: 30000 });
|
||||
});
|
||||
}
|
||||
|
||||
async waitForTableData(tableSelector: string, minRows: number = 1): Promise<void> {
|
||||
await this.retry(async () => {
|
||||
const table = this.page.locator(tableSelector);
|
||||
await expect(table).toBeVisible({ timeout: 10000 });
|
||||
|
||||
const rows = table.locator('.el-table__row');
|
||||
const rowCount = await rows.count();
|
||||
expect(rowCount).toBeGreaterThanOrEqual(minRows);
|
||||
});
|
||||
}
|
||||
|
||||
async safeScrollIntoView(selector: string): Promise<void> {
|
||||
const element = this.page.locator(selector);
|
||||
await element.scrollIntoViewIfNeeded();
|
||||
await this.page.waitForTimeout(500);
|
||||
}
|
||||
|
||||
async clearLocalStorage(): Promise<void> {
|
||||
await this.page.evaluate(() => {
|
||||
localStorage.clear();
|
||||
});
|
||||
}
|
||||
|
||||
async clearSessionStorage(): Promise<void> {
|
||||
await this.page.evaluate(() => {
|
||||
sessionStorage.clear();
|
||||
});
|
||||
}
|
||||
|
||||
async takeScreenshot(name: string): Promise<void> {
|
||||
await this.page.screenshot({ path: `test-results/screenshots/${name}.png`, fullPage: true });
|
||||
}
|
||||
|
||||
async getErrorMessage(): Promise<string | null> {
|
||||
try {
|
||||
const errorElement = this.page.locator('.el-message--error, .error-message');
|
||||
const isVisible = await errorElement.isVisible({ timeout: 2000 });
|
||||
|
||||
if (isVisible) {
|
||||
return await errorElement.textContent();
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async hasErrorMessage(): Promise<boolean> {
|
||||
const errorMessage = await this.getErrorMessage();
|
||||
return errorMessage !== null;
|
||||
}
|
||||
|
||||
private async retry<T>(fn: () => Promise<T>): Promise<T> {
|
||||
let lastError: Error | undefined;
|
||||
|
||||
for (let attempt = 1; attempt <= this.maxRetries; attempt++) {
|
||||
try {
|
||||
return await fn();
|
||||
} catch (error) {
|
||||
lastError = error as Error;
|
||||
console.log(`Attempt ${attempt} failed, retrying...`, error);
|
||||
|
||||
if (attempt < this.maxRetries) {
|
||||
await this.page.waitForTimeout(this.retryDelay);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError || new Error('All retry attempts failed');
|
||||
}
|
||||
|
||||
async waitForElementNotVisible(selector: string, timeout: number = 10000): Promise<void> {
|
||||
await this.retry(async () => {
|
||||
const element = this.page.locator(selector);
|
||||
await expect(element).not.toBeVisible({ timeout });
|
||||
});
|
||||
}
|
||||
|
||||
async safeHover(selector: string): Promise<void> {
|
||||
await this.retry(async () => {
|
||||
const element = this.page.locator(selector);
|
||||
await element.waitFor({ state: 'visible', timeout: 10000 });
|
||||
await element.hover({ timeout: 5000 });
|
||||
});
|
||||
}
|
||||
|
||||
async waitForText(selector: string, text: string, timeout: number = 10000): Promise<void> {
|
||||
await this.retry(async () => {
|
||||
const element = this.page.locator(selector);
|
||||
await expect(element).toContainText(text, { timeout });
|
||||
});
|
||||
}
|
||||
|
||||
async waitForTextNotPresent(selector: string, text: string, timeout: number = 10000): Promise<void> {
|
||||
await this.retry(async () => {
|
||||
const element = this.page.locator(selector);
|
||||
await expect(element).not.toContainText(text, { timeout });
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user