feat(e2e): 优化测试配置并增强富文本编辑器测试

refactor(cases): 更新案例数据结构字段
test(admin): 增加富文本编辑器多种格式测试
fix(contact-form): 修复表单提交测试并移除skip标记
perf(smart-wait): 改进页面就绪等待逻辑
ci(playwright): 调整firefox配置并优化全局setup
This commit is contained in:
张翔
2026-03-26 18:06:34 +08:00
parent 14448af731
commit 027ee2137e
10 changed files with 5367 additions and 4886 deletions
+37 -20
View File
@@ -1,32 +1,49 @@
import { chromium, FullConfig } from '@playwright/test';
import { chromium, firefox, webkit, FullConfig } from '@playwright/test';
import { getEnvironment } from './src/config/environments';
const env = getEnvironment();
async function globalSetup(_config: FullConfig) {
const browser = await chromium.launch();
const page = await browser.newPage();
async function globalSetup(config: FullConfig) {
const browserName = config.projects?.[0]?.use?.browserName || 'chromium';
let browser;
try {
await page.goto(`${env.baseURL}/admin/login`, { waitUntil: 'commit', timeout: 120000 });
await page.waitForSelector('#email', { timeout: 30000 });
await page.locator('#email').fill('admin@novalon.cn');
await page.locator('#password').fill('admin123456');
await page.locator('button[type="submit"]').click();
switch (browserName) {
case 'firefox':
browser = await firefox.launch();
break;
case 'webkit':
browser = await webkit.launch();
break;
default:
browser = await chromium.launch();
}
const page = await browser.newPage();
try {
await page.waitForURL(/\/admin(?!\/login)/, { timeout: 30000 });
await page.context().storageState({ path: '.auth/admin.json' });
await page.goto(`${env.baseURL}/admin/login`, { waitUntil: 'commit', timeout: 120000 });
await page.waitForSelector('#email', { timeout: 30000 });
await page.locator('#email').fill('admin@novalon.cn');
await page.locator('#password').fill('admin123456');
await page.locator('button[type="submit"]').click();
try {
await page.waitForURL(/\/admin(?!\/login)/, { timeout: 30000 });
await page.context().storageState({ path: '.auth/admin.json' });
} catch {
console.warn('登录失败,跳过需要认证的测试');
}
} catch {
console.warn('登录失败,跳过需要认证的测试');
console.warn('Admin登录页面不可用,跳过需要认证的测试');
} finally {
await browser.close();
}
} catch {
console.warn('Admin登录页面不可用,跳过需要认证的测试');
} finally {
await browser.close();
} catch (error) {
console.warn(`浏览器启动失败 (${browserName}),跳过需要认证的测试:`, error.message);
}
}
+1
View File
@@ -58,6 +58,7 @@ export default defineConfig({
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
globalSetup: undefined,
},
{
name: 'webkit',
+1 -1
View File
@@ -18,7 +18,7 @@ export const environments: Record<string, EnvironmentConfig> = {
apiURL: 'http://localhost:3000/api',
timeout: 120000,
retries: 0,
headless: false,
headless: true,
slowMo: 100,
screenshot: 'only-on-failure',
video: 'retain-on-failure',
+255 -22
View File
@@ -1,9 +1,12 @@
import { test, expect } from '../../fixtures/admin.fixture';
test.describe('富文本编辑器E2E测试', () => {
test('应该能够输入文本内容', async ({ page }) => {
test.beforeEach(async ({ page }) => {
await page.goto('/admin/content/new');
await page.locator('select[name="type"]').selectOption('news');
});
test('应该能够输入文本内容', async ({ page }) => {
await page.locator('input[name="title"]').fill('富文本测试');
await page.locator('input[name="slug"]').fill('rich-text-test');
@@ -17,28 +20,194 @@ test.describe('富文本编辑器E2E测试', () => {
await expect(page.locator('text=保存成功')).toBeVisible({ timeout: 5000 });
});
test('应该能够使用格式化工具', async ({ page }) => {
await page.goto('/admin/content/new');
await page.locator('select[name="type"]').selectOption('news');
await page.locator('input[name="title"]').fill('格式化测试');
await page.locator('input[name="slug"]').fill('formatting-test');
test('应该能够使用粗体格式', async ({ page }) => {
await page.locator('input[name="title"]').fill('粗体测试');
await page.locator('input[name="slug"]').fill('bold-test');
const editor = page.locator('.ProseMirror');
await editor.waitFor({ state: 'visible', timeout: 10000 });
await editor.click();
await editor.fill('普通文本');
await editor.fill('粗体文本');
await page.keyboard.selectText('普通文本');
await page.keyboard.selectText('粗体文本');
await page.getByRole('button', { name: '粗体' }).click();
await page.getByRole('button', { name: /保存/i }).click();
const boldButton = page.getByRole('button', { name: '粗体' });
await expect(boldButton).toHaveClass(/bg-gray-200/);
await page.getByRole('button', { name: /保存/i }).click();
await expect(page.locator('text=保存成功')).toBeVisible({ timeout: 5000 });
});
test('应该能够使用斜体格式', async ({ page }) => {
await page.locator('input[name="title"]').fill('斜体测试');
await page.locator('input[name="slug"]').fill('italic-test');
const editor = page.locator('.ProseMirror');
await editor.waitFor({ state: 'visible', timeout: 10000 });
await editor.click();
await editor.fill('斜体文本');
await page.keyboard.selectText('斜体文本');
await page.getByRole('button', { name: '斜体' }).click();
const italicButton = page.getByRole('button', { name: '斜体' });
await expect(italicButton).toHaveClass(/bg-gray-200/);
await page.getByRole('button', { name: /保存/i }).click();
await expect(page.locator('text=保存成功')).toBeVisible({ timeout: 5000 });
});
test('应该能够使用删除线格式', async ({ page }) => {
await page.locator('input[name="title"]').fill('删除线测试');
await page.locator('input[name="slug"]').fill('strikethrough-test');
const editor = page.locator('.ProseMirror');
await editor.waitFor({ state: 'visible', timeout: 10000 });
await editor.click();
await editor.fill('删除线文本');
await page.keyboard.selectText('删除线文本');
await page.getByRole('button', { name: '删除线' }).click();
const strikeButton = page.getByRole('button', { name: '删除线' });
await expect(strikeButton).toHaveClass(/bg-gray-200/);
await page.getByRole('button', { name: /保存/i }).click();
await expect(page.locator('text=保存成功')).toBeVisible({ timeout: 5000 });
});
test('应该能够使用代码格式', async ({ page }) => {
await page.locator('input[name="title"]').fill('代码测试');
await page.locator('input[name="slug"]').fill('code-test');
const editor = page.locator('.ProseMirror');
await editor.waitFor({ state: 'visible', timeout: 10000 });
await editor.click();
await editor.fill('console.log("test")');
await page.keyboard.selectText('console.log("test")');
await page.getByRole('button', { name: '代码' }).click();
const codeButton = page.getByRole('button', { name: '代码' });
await expect(codeButton).toHaveClass(/bg-gray-200/);
await page.getByRole('button', { name: /保存/i }).click();
await expect(page.locator('text=保存成功')).toBeVisible({ timeout: 5000 });
});
test('应该能够使用标题格式', async ({ page }) => {
await page.locator('input[name="title"]').fill('标题测试');
await page.locator('input[name="slug"]').fill('heading-test');
const editor = page.locator('.ProseMirror');
await editor.waitFor({ state: 'visible', timeout: 10000 });
await editor.click();
await editor.fill('标题1');
await page.keyboard.selectText('标题1');
await page.getByRole('button', { name: '标题 1' }).click();
const h1Button = page.getByRole('button', { name: '标题 1' });
await expect(h1Button).toHaveClass(/bg-gray-200/);
await page.getByRole('button', { name: /保存/i }).click();
await expect(page.locator('text=保存成功')).toBeVisible({ timeout: 5000 });
});
test('应该能够使用标题2格式', async ({ page }) => {
await page.locator('input[name="title"]').fill('标题2测试');
await page.locator('input[name="slug"]').fill('heading2-test');
const editor = page.locator('.ProseMirror');
await editor.waitFor({ state: 'visible', timeout: 10000 });
await editor.click();
await editor.fill('标题2');
await page.keyboard.selectText('标题2');
await page.getByRole('button', { name: '标题 2' }).click();
const h2Button = page.getByRole('button', { name: '标题 2' });
await expect(h2Button).toHaveClass(/bg-gray-200/);
await page.getByRole('button', { name: /保存/i }).click();
await expect(page.locator('text=保存成功')).toBeVisible({ timeout: 5000 });
});
test('应该能够使用标题3格式', async ({ page }) => {
await page.locator('input[name="title"]').fill('标题3测试');
await page.locator('input[name="slug"]').fill('heading3-test');
const editor = page.locator('.ProseMirror');
await editor.waitFor({ state: 'visible', timeout: 10000 });
await editor.click();
await editor.fill('标题3');
await page.keyboard.selectText('标题3');
await page.getByRole('button', { name: '标题 3' }).click();
const h3Button = page.getByRole('button', { name: '标题 3' });
await expect(h3Button).toHaveClass(/bg-gray-200/);
await page.getByRole('button', { name: /保存/i }).click();
await expect(page.locator('text=保存成功')).toBeVisible({ timeout: 5000 });
});
test('应该能够使用无序列表', async ({ page }) => {
await page.locator('input[name="title"]').fill('无序列表测试');
await page.locator('input[name="slug"]').fill('bullet-list-test');
const editor = page.locator('.ProseMirror');
await editor.waitFor({ state: 'visible', timeout: 10000 });
await editor.click();
await editor.fill('列表项1');
await page.getByRole('button', { name: '无序列表' }).click();
const listButton = page.getByRole('button', { name: '无序列表' });
await expect(listButton).toHaveClass(/bg-gray-200/);
await page.getByRole('button', { name: /保存/i }).click();
await expect(page.locator('text=保存成功')).toBeVisible({ timeout: 5000 });
});
test('应该能够使用有序列表', async ({ page }) => {
await page.locator('input[name="title"]').fill('有序列表测试');
await page.locator('input[name="slug"]').fill('ordered-list-test');
const editor = page.locator('.ProseMirror');
await editor.waitFor({ state: 'visible', timeout: 10000 });
await editor.click();
await editor.fill('列表项1');
await page.getByRole('button', { name: '有序列表' }).click();
const listButton = page.getByRole('button', { name: '有序列表' });
await expect(listButton).toHaveClass(/bg-gray-200/);
await page.getByRole('button', { name: /保存/i }).click();
await expect(page.locator('text=保存成功')).toBeVisible({ timeout: 5000 });
});
test('应该能够使用引用格式', async ({ page }) => {
await page.locator('input[name="title"]').fill('引用测试');
await page.locator('input[name="slug"]').fill('quote-test');
const editor = page.locator('.ProseMirror');
await editor.waitFor({ state: 'visible', timeout: 10000 });
await editor.click();
await editor.fill('这是一段引用文字');
await page.getByRole('button', { name: '引用' }).click();
const quoteButton = page.getByRole('button', { name: '引用' });
await expect(quoteButton).toHaveClass(/bg-gray-200/);
await page.getByRole('button', { name: /保存/i }).click();
await expect(page.locator('text=保存成功')).toBeVisible({ timeout: 5000 });
});
test('应该能够添加链接', async ({ page }) => {
await page.goto('/admin/content/new');
await page.locator('select[name="type"]').selectOption('news');
await page.locator('input[name="title"]').fill('链接测试');
await page.locator('input[name="slug"]').fill('link-test');
@@ -61,24 +230,88 @@ test.describe('富文本编辑器E2E测试', () => {
await expect(page.locator('text=保存成功')).toBeVisible({ timeout: 5000 });
});
test('应该能够添加列表', async ({ page }) => {
await page.goto('/admin/content/new');
await page.locator('select[name="type"]').selectOption('news');
await page.locator('input[name="title"]').fill('列表测试');
await page.locator('input[name="slug"]').fill('list-test');
test('应该能够撤销操作', async ({ page }) => {
await page.locator('input[name="title"]').fill('撤销测试');
await page.locator('input[name="slug"]').fill('undo-test');
const editor = page.locator('.ProseMirror');
await editor.waitFor({ state: 'visible', timeout: 10000 });
await editor.click();
await editor.fill('原始文本');
await editor.fill('列表项1');
await page.keyboard.press('Enter');
await editor.type('列表项2');
await page.keyboard.press('Enter');
await editor.type('列表项3');
await page.keyboard.selectText('原始文本');
await page.getByRole('button', { name: '粗体' }).click();
const undoButton = page.getByRole('button', { name: '撤销' });
await expect(undoButton).toBeEnabled();
await undoButton.click();
const boldButton = page.getByRole('button', { name: '粗体' });
await expect(boldButton).not.toHaveClass(/bg-gray-200/);
});
test('应该能够重做操作', async ({ page }) => {
await page.locator('input[name="title"]').fill('重做测试');
await page.locator('input[name="slug"]').fill('redo-test');
const editor = page.locator('.ProseMirror');
await editor.waitFor({ state: 'visible', timeout: 10000 });
await editor.click();
await editor.fill('原始文本');
await page.keyboard.selectText('原始文本');
await page.getByRole('button', { name: '粗体' }).click();
const undoButton = page.getByRole('button', { name: '撤销' });
await undoButton.click();
const redoButton = page.getByRole('button', { name: '重做' });
await expect(redoButton).toBeEnabled();
await redoButton.click();
const boldButton = page.getByRole('button', { name: '粗体' });
await expect(boldButton).toHaveClass(/bg-gray-200/);
});
test('应该能够组合多种格式', async ({ page }) => {
await page.locator('input[name="title"]').fill('组合格式测试');
await page.locator('input[name="slug"]').fill('combined-format-test');
const editor = page.locator('.ProseMirror');
await editor.waitFor({ state: 'visible', timeout: 10000 });
await editor.click();
await editor.fill('粗体斜体文本');
await page.keyboard.selectText('粗体斜体文本');
await page.getByRole('button', { name: '粗体' }).click();
await page.getByRole('button', { name: '斜体' }).click();
const boldButton = page.getByRole('button', { name: '粗体' });
const italicButton = page.getByRole('button', { name: '斜体' });
await expect(boldButton).toHaveClass(/bg-gray-200/);
await expect(italicButton).toHaveClass(/bg-gray-200/);
await page.getByRole('button', { name: /保存/i }).click();
await expect(page.locator('text=保存成功')).toBeVisible({ timeout: 5000 });
});
test('应该能够切换格式状态', async ({ page }) => {
await page.locator('input[name="title"]').fill('切换格式测试');
await page.locator('input[name="slug"]').fill('toggle-format-test');
const editor = page.locator('.ProseMirror');
await editor.waitFor({ state: 'visible', timeout: 10000 });
await editor.click();
await editor.fill('切换文本');
await page.keyboard.selectText('切换文本');
await page.getByRole('button', { name: '粗体' }).click();
const boldButton = page.getByRole('button', { name: '粗体' });
await expect(boldButton).toHaveClass(/bg-gray-200/);
await page.getByRole('button', { name: '粗体' }).click();
await expect(boldButton).not.toHaveClass(/bg-gray-200/);
});
});
@@ -6,19 +6,16 @@ test.describe('联系表单回归测试 @regression', () => {
await contactPage.waitForPageLoad();
});
test.skip('应该能够提交完整的表单', async ({ contactPage, testDataGenerator }) => {
test('应该能够提交完整的表单', async ({ contactPage, testDataGenerator }) => {
const formData = testDataGenerator.generateContactFormData();
await contactPage.fillAndSubmitForm(formData);
await contactPage.page.waitForTimeout(3000);
const isFormVisible = await contactPage.isFormVisible();
console.log('Form visible after submission:', isFormVisible);
await contactPage.page.waitForTimeout(5000);
const isSuccessVisible = await contactPage.isSuccessMessageVisible();
console.log('Success message visible:', isSuccessVisible);
expect(isSuccessVisible || !isFormVisible).toBe(true);
expect(isSuccessVisible).toBe(true);
});
test.skip('应该验证必填字段', async ({ contactPage }) => {
@@ -122,35 +119,35 @@ test.describe('联系表单回归测试 @regression', () => {
expect(isLoading).toBe(true);
});
test.skip('应该显示成功消息', async ({ contactPage, testDataGenerator }) => {
test('应该显示成功消息', async ({ contactPage, testDataGenerator }) => {
const formData = testDataGenerator.generateContactFormData();
await contactPage.fillAndSubmitForm(formData);
await contactPage.waitForFormSubmission();
await contactPage.page.waitForTimeout(5000);
const isSuccessVisible = await contactPage.isSuccessMessageVisible();
expect(isSuccessVisible).toBe(true);
});
test.skip('应该显示正确的成功消息文本', async ({ contactPage, testDataGenerator }) => {
test('应该显示正确的成功消息文本', async ({ contactPage, testDataGenerator }) => {
const formData = testDataGenerator.generateContactFormData();
await contactPage.fillAndSubmitForm(formData);
await contactPage.waitForFormSubmission();
await contactPage.page.waitForTimeout(5000);
const messageText = await contactPage.getSuccessMessageText();
expect(messageText).toContain('消息已发送');
expect(messageText).toContain('成功');
});
test.skip('应该能够重新提交表单', async ({ contactPage, testDataGenerator }) => {
test('应该能够重新提交表单', async ({ contactPage, testDataGenerator }) => {
const formData1 = testDataGenerator.generateContactFormData();
await contactPage.fillAndSubmitForm(formData1);
await contactPage.waitForFormSubmission();
await contactPage.page.waitForTimeout(5000);
await contactPage.page.reload();
await contactPage.waitForPageLoad();
const formData2 = testDataGenerator.generateContactFormData();
await contactPage.fillAndSubmitForm(formData2);
await contactPage.waitForFormSubmission();
const isSubmitted = await contactPage.isFormSubmitted();
expect(isSubmitted).toBe(true);
await contactPage.page.waitForTimeout(5000);
const isSuccessVisible = await contactPage.isSuccessMessageVisible();
expect(isSuccessVisible).toBe(true);
});
test('应该能够输入空格', async ({ contactPage, testDataGenerator }) => {
@@ -198,10 +195,10 @@ test.describe('联系表单回归测试 @regression', () => {
expect(isVisible).toBe(true);
});
test.skip('应该能够截取成功消息截图', async ({ contactPage, testDataGenerator }) => {
test('应该能够截取成功消息截图', async ({ contactPage, testDataGenerator }) => {
const formData = testDataGenerator.generateContactFormData();
await contactPage.fillAndSubmitForm(formData);
await contactPage.waitForFormSubmission();
await contactPage.page.waitForTimeout(5000);
const isSuccessVisible = await contactPage.isSuccessMessageVisible();
expect(isSuccessVisible).toBe(true);
});
+7 -11
View File
@@ -38,18 +38,14 @@ test.describe('管理后台冒烟测试', () => {
await loginPage.goto();
await loginPage.login('admin@novalon.cn', 'admin123456');
try {
await expect(async () => {
await page.waitForURL(/\/admin(?!\/login)/);
}).toPass({ timeout: 15000 });
} catch (error) {
test.skip(true, '登录功能不稳定,跳过此测试');
}
await page.waitForURL(/\/admin(?!\/login)/, { timeout: 20000 });
await page.waitForLoadState('networkidle', { timeout: 10000 });
await expect(dashboardPage.contentMenuItem).toBeVisible();
await expect(dashboardPage.settingsMenuItem).toBeVisible();
await expect(dashboardPage.usersMenuItem).toBeVisible();
await expect(dashboardPage.logsMenuItem).toBeVisible();
await expect(dashboardPage.sidebar).toBeVisible({ timeout: 10000 });
await expect(dashboardPage.contentMenuItem).toBeVisible({ timeout: 5000 });
await expect(dashboardPage.settingsMenuItem).toBeVisible({ timeout: 5000 });
await expect(dashboardPage.usersMenuItem).toBeVisible({ timeout: 5000 });
await expect(dashboardPage.logsMenuItem).toBeVisible({ timeout: 5000 });
});
});
+13 -5
View File
@@ -86,20 +86,28 @@ export class SmartWait {
throw new Error(`文本内容未在 ${timeout}ms 内出现: ${expectedText}`);
}
async waitForPageReady(timeout: number = 15000) {
async waitForPageReady(timeout: number = 30000) {
const startTime = Date.now();
try {
await this.page.waitForLoadState('domcontentloaded', { timeout });
await this.waitForNetworkIdle(3000);
try {
await this.waitForNetworkIdle(2000);
} catch {
console.log('网络空闲等待失败,继续页面加载检查');
}
const body = this.page.locator('body');
await this.waitForElement(body, { timeout: 5000, state: 'visible' });
try {
const body = this.page.locator('body');
await this.waitForElement(body, { timeout: 5000, state: 'visible' });
} catch {
console.log('Body元素等待超时,尝试继续');
}
return true;
} catch (error) {
console.log(`页面未在 ${timeout}ms 内就绪`);
console.log(`页面未在 ${timeout}ms 内就绪: ${error.message}`);
throw error;
}
}