feat: 添加测试框架和覆盖率报告功能

feat(测试): 新增Playwright和Vitest测试配置
feat(测试): 添加测试覆盖率报告生成功能
feat(测试): 实现前后端测试脚本集成

fix(测试): 修复测试密码不匹配问题
fix(测试): 修正URL等待策略
fix(测试): 调整错误消息选择器

refactor(测试): 重构测试目录结构
refactor(测试): 优化测试用例组织方式

docs: 更新测试报告文档
docs: 添加测试覆盖率报告模板

ci: 添加Docker测试环境配置
ci: 实现测试自动化脚本

chore: 更新依赖版本
chore: 添加测试相关配置文件
This commit is contained in:
张翔
2026-03-25 09:03:37 +08:00
parent 117978e148
commit e2ad1331cc
126 changed files with 18083 additions and 7805 deletions
@@ -0,0 +1,181 @@
import { APIRequestContext } from '@playwright/test';
export interface TestUser {
username: string;
nickname?: string;
email: string;
phone: string;
password: string;
roleIds?: number[];
}
export interface TestRole {
roleName: string;
roleKey: string;
roleSort: string;
status: string;
remark?: string;
}
export class TestDataManager {
private static testData: Map<string, any> = new Map();
private static apiBaseUrl: string;
static initialize(apiBaseUrl: string = 'http://localhost:8084') {
this.apiBaseUrl = apiBaseUrl;
}
static generateTimestamp(): string {
return Date.now().toString();
}
static generateTestUser(override?: Partial<TestUser>): TestUser {
const timestamp = this.generateTimestamp();
return {
username: `testuser_${timestamp}`,
nickname: `测试用户${timestamp}`,
email: `test_${timestamp}@example.com`,
phone: '13800138000',
password: 'Test123!@#',
roleIds: [],
...override,
};
}
static generateTestRole(override?: Partial<TestRole>): TestRole {
const timestamp = this.generateTimestamp();
return {
roleName: `测试角色_${timestamp}`,
roleKey: `test_role_${timestamp}`,
roleSort: '1',
status: '1',
remark: `测试角色备注_${timestamp}`,
...override,
};
}
static async createTestUser(request: APIRequestContext, userData: TestUser): Promise<any> {
const response = await request.post(`${this.apiBaseUrl}/api/users`, {
data: userData,
});
if (!response.ok()) {
throw new Error(`Failed to create test user: ${await response.text()}`);
}
const result = await response.json();
const userId = result.data?.id || result.id;
this.testData.set(`user_${userData.username}`, {
id: userId,
...userData,
});
return result;
}
static async createTestRole(request: APIRequestContext, roleData: TestRole): Promise<any> {
const response = await request.post(`${this.apiBaseUrl}/api/roles`, {
data: roleData,
});
if (!response.ok()) {
throw new Error(`Failed to create test role: ${await response.text()}`);
}
const result = await response.json();
const roleId = result.data?.id || result.id;
this.testData.set(`role_${roleData.roleKey}`, {
id: roleId,
...roleData,
});
return result;
}
static async deleteTestUser(request: APIRequestContext, username: string): Promise<void> {
const userData = this.testData.get(`user_${username}`);
if (!userData || !userData.id) {
return;
}
const response = await request.delete(`${this.apiBaseUrl}/api/users/${userData.id}`);
if (!response.ok()) {
console.warn(`Failed to delete test user ${username}: ${await response.text()}`);
}
this.testData.delete(`user_${username}`);
}
static async deleteTestRole(request: APIRequestContext, roleKey: string): Promise<void> {
const roleData = this.testData.get(`role_${roleKey}`);
if (!roleData || !roleData.id) {
return;
}
const response = await request.delete(`${this.apiBaseUrl}/api/roles/${roleData.id}`);
if (!response.ok()) {
console.warn(`Failed to delete test role ${roleKey}: ${await response.text()}`);
}
this.testData.delete(`role_${roleKey}`);
}
static async cleanupTestData(request: APIRequestContext): Promise<void> {
const cleanupPromises: Promise<void>[] = [];
const entries = Array.from(this.testData.entries());
for (const [key, data] of entries) {
if (key.startsWith('user_')) {
cleanupPromises.push(this.deleteTestUser(request, data.username));
} else if (key.startsWith('role_')) {
cleanupPromises.push(this.deleteTestRole(request, data.roleKey));
}
}
await Promise.allSettled(cleanupPromises);
this.testData.clear();
}
static getTestData(key: string): any {
return this.testData.get(key);
}
static getAllTestData(): Map<string, any> {
return new Map(this.testData);
}
static clearTestData(): void {
this.testData.clear();
}
}
export class DatabaseHelper {
private static apiBaseUrl: string;
static initialize(apiBaseUrl: string = 'http://localhost:8084') {
this.apiBaseUrl = apiBaseUrl;
}
static async resetDatabase(request: APIRequestContext): Promise<void> {
const response = await request.post(`${this.apiBaseUrl}/api/test/reset-database`);
if (!response.ok()) {
throw new Error(`Failed to reset database: ${await response.text()}`);
}
}
static async clearTestData(request: APIRequestContext): Promise<void> {
const response = await request.post(`${this.apiBaseUrl}/api/test/clear-test-data`);
if (!response.ok()) {
throw new Error(`Failed to clear test data: ${await response.text()}`);
}
}
static async seedTestData(request: APIRequestContext): Promise<void> {
const response = await request.post(`${this.apiBaseUrl}/api/test/seed-test-data`);
if (!response.ok()) {
throw new Error(`Failed to seed test data: ${await response.text()}`);
}
}
}
+263
View File
@@ -0,0 +1,263 @@
import { Page, expect } from '@playwright/test';
export class TestHelper {
static async waitForPageLoad(page: Page, timeout: number = 30000): Promise<void> {
await page.waitForLoadState('networkidle', { timeout });
await page.waitForLoadState('domcontentloaded', { timeout });
}
static async waitForElementVisible(
page: Page,
selector: string,
timeout: number = 10000
): Promise<void> {
await expect(page.locator(selector)).toBeVisible({ timeout });
}
static async waitForElementHidden(
page: Page,
selector: string,
timeout: number = 10000
): Promise<void> {
await expect(page.locator(selector)).toBeHidden({ timeout });
}
static async waitForTextContent(
page: Page,
selector: string,
text: string,
timeout: number = 10000
): Promise<void> {
await expect(page.locator(selector)).toContainText(text, { timeout });
}
static async clickElement(page: Page, selector: string, timeout: number = 10000): Promise<void> {
await page.click(selector, { timeout });
}
static async fillInput(
page: Page,
selector: string,
value: string,
timeout: number = 10000
): Promise<void> {
await page.fill(selector, value, { timeout });
}
static async selectOption(
page: Page,
selector: string,
value: string,
timeout: number = 10000
): Promise<void> {
await page.selectOption(selector, value, { timeout });
}
static async checkCheckbox(
page: Page,
selector: string,
timeout: number = 10000
): Promise<void> {
await page.check(selector, { timeout });
}
static async uncheckCheckbox(
page: Page,
selector: string,
timeout: number = 10000
): Promise<void> {
await page.uncheck(selector, { timeout });
}
static async uploadFile(
page: Page,
selector: string,
filePath: string,
timeout: number = 10000
): Promise<void> {
await page.setInputFiles(selector, filePath, { timeout });
}
static async takeScreenshot(
page: Page,
filename: string,
fullPage: boolean = false
): Promise<void> {
await page.screenshot({
path: `test-results/screenshots/${filename}`,
fullPage,
});
}
static async waitForUrl(
page: Page,
urlPattern: string | RegExp,
timeout: number = 30000
): Promise<void> {
await page.waitForURL(urlPattern, { timeout });
}
static async reloadPage(page: Page, timeout: number = 30000): Promise<void> {
await page.reload({ waitUntil: 'networkidle', timeout });
}
static async navigateTo(page: Page, url: string, timeout: number = 30000): Promise<void> {
await page.goto(url, { waitUntil: 'networkidle', timeout });
}
static async waitForDialog(page: Page, timeout: number = 10000): Promise<void> {
await page.waitForEvent('dialog', { timeout });
}
static async handleDialog(page: Page, accept: boolean = true): Promise<void> {
page.on('dialog', async (dialog) => {
if (accept) {
await dialog.accept();
} else {
await dialog.dismiss();
}
});
}
static async waitForToast(
page: Page,
message: string,
timeout: number = 5000
): Promise<void> {
await expect(page.locator('.el-message')).toContainText(message, { timeout });
}
static async waitForSuccessMessage(page: Page, timeout: number = 5000): Promise<void> {
await expect(page.locator('.el-message--success')).toBeVisible({ timeout });
}
static async waitForErrorMessage(page: Page, timeout: number = 5000): Promise<void> {
await expect(page.locator('.el-message--error')).toBeVisible({ timeout });
}
static async getElementText(page: Page, selector: string): Promise<string> {
const text = await page.textContent(selector);
return text || '';
}
static async getElementCount(page: Page, selector: string): Promise<number> {
return await page.locator(selector).count();
}
static async isElementVisible(page: Page, selector: string): Promise<boolean> {
return await page.locator(selector).isVisible();
}
static async isElementEnabled(page: Page, selector: string): Promise<boolean> {
return await page.locator(selector).isEnabled();
}
static async scrollToElement(page: Page, selector: string): Promise<void> {
await page.locator(selector).scrollIntoViewIfNeeded();
}
static async hoverElement(page: Page, selector: string): Promise<void> {
await page.hover(selector);
}
static async doubleClickElement(page: Page, selector: string): Promise<void> {
await page.dblclick(selector);
}
static async rightClickElement(page: Page, selector: string): Promise<void> {
await page.click(selector, { button: 'right' });
}
static async waitForApiResponse(
page: Page,
urlPattern: string | RegExp,
timeout: number = 30000
): Promise<void> {
await page.waitForResponse(
(response) => !!response.url().match(urlPattern),
{ timeout }
);
}
static async getApiResponse(
page: Page,
urlPattern: string | RegExp,
timeout: number = 30000
): Promise<any> {
const response = await page.waitForResponse(
(response) => !!response.url().match(urlPattern),
{ timeout }
);
return await response.json();
}
static async mockApiResponse(
page: Page,
urlPattern: string | RegExp,
mockData: any
): Promise<void> {
await page.route(urlPattern, async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(mockData),
});
});
}
static async executeScript(page: Page, script: string): Promise<any> {
return await page.evaluate(script);
}
static async setLocalStorage(page: Page, key: string, value: string): Promise<void> {
await page.evaluate(
({ key, value }) => {
localStorage.setItem(key, value);
},
{ key, value }
);
}
static async getLocalStorage(page: Page, key: string): Promise<string | null> {
return await page.evaluate((key) => localStorage.getItem(key), key);
}
static async clearLocalStorage(page: Page): Promise<void> {
await page.evaluate(() => localStorage.clear());
}
static async setSessionStorage(page: Page, key: string, value: string): Promise<void> {
await page.evaluate(
({ key, value }) => {
sessionStorage.setItem(key, value);
},
{ key, value }
);
}
static async clearSessionStorage(page: Page): Promise<void> {
await page.evaluate(() => sessionStorage.clear());
}
static async clearCookies(page: Page): Promise<void> {
await page.context().clearCookies();
}
static async clearAllStorage(page: Page): Promise<void> {
await this.clearLocalStorage(page);
await this.clearSessionStorage(page);
await this.clearCookies(page);
}
static async getAuthToken(page: Page): Promise<string> {
const token = await this.getLocalStorage(page, 'token');
if (!token) {
const user = await this.getLocalStorage(page, 'user');
if (user) {
const userData = JSON.parse(user);
return userData.token || '';
}
}
return token || '';
}
}