test: 增强Playwright E2E测试方案 - 新增手术计费/医生站/并发测试用例

- 新增页面对象: SurgeryBillingPage, DoctorStationPage
- 新增测试用例: 手术计费防重复(#437), 签发耗材验证(#443), 并发操作测试
- 增强登录测试: 多场景覆盖
- 完善测试数据工具: 支持多角色用户配置
- 清理冗余备份文件
This commit is contained in:
2026-04-25 22:04:36 +08:00
parent 46a7076460
commit 305ab15436
13 changed files with 361 additions and 2503 deletions

View File

@@ -1,38 +1,58 @@
import { test, expect } from '@playwright/test';
import { TEST_USERS } from '../utils/test-data';
import { LoginPage } from '../pages/LoginPage';
import { TEST_USERS, TEST_URLS } from '../utils/test-data';
test.describe('🐛 Bug回归测试', () => {
let loginPage: LoginPage;
test.describe('Bug回归测试', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
await page.fill('input[placeholder="请输入用户名"]', TEST_USERS.admin.username);
await page.fill('input[placeholder="请输入密码"]', TEST_USERS.admin.password);
await page.click('button:has-text("登录")');
await page.waitForURL(/.*(dashboard|home).*/);
loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login(TEST_USERS.admin.username, TEST_USERS.admin.password);
await loginPage.expectLoginSuccess();
});
test('#437 手术计费防重复提交', async ({ page }) => {
await page.goto('/surgery-billing');
const addBtn = page.locator('button:has-text("新增")');
test('#437 手术计费防重复提交 @bug437 @regression', async ({ page }) => {
await page.goto(TEST_URLS.surgeryBilling);
await page.waitForLoadState('networkidle');
// 快速连续点击(测试防重复锁)
await addBtn.click();
await addBtn.click();
await addBtn.click();
// 验证只弹出一个表单
const dialogCount = await page.locator('.el-dialog').count();
expect(dialogCount).toBeLessThanOrEqual(1);
const addBtn = page.locator('button:has-text("新增"), button:has-text("生成")');
if (await addBtn.isVisible()) {
// 快速连续点击3次
await addBtn.click();
await addBtn.click();
await addBtn.click();
await page.waitForTimeout(2000);
// 验证只弹出一个对话框
const dialogs = page.locator('.el-dialog, .el-message-box');
expect(await dialogs.count()).toBeLessThanOrEqual(1);
}
});
test('#427 检查项目分类手风琴展开', async ({ page }) => {
await page.goto('/doctorstation');
// 点击第一个分类
const firstCategory = page.locator('.category-item').first();
await firstCategory.click();
// 点击第二个分类,第一个应收起
const secondCategory = page.locator('.category-item').nth(1);
await secondCategory.click();
test('#443 手术计费签发耗材 @bug443 @regression', async ({ page }) => {
await page.goto(TEST_URLS.surgeryBilling);
await page.waitForLoadState('networkidle');
// 验证签发功能不报错locationId为空时应有默认值
const signBtn = page.locator('button:has-text("签发"), button:has-text("提交")');
if (await signBtn.isVisible()) {
await signBtn.click();
await page.waitForTimeout(2000);
// 不应出现"发放库房为空"错误
const errorMsg = page.locator('text=发放库房为空');
expect(await errorMsg.count()).toBe(0);
}
});
test('#427 检查项目分类手风琴展开 @regression', async ({ page }) => {
await page.goto(TEST_URLS.doctorStation);
await page.waitForLoadState('networkidle');
// 验证分类展开功能
const categories = page.locator('.el-collapse-item, .category-item');
const count = await categories.count();
if (count > 0) {
await categories.first().click();
await page.waitForTimeout(500);
}
});
});

View File

@@ -0,0 +1,59 @@
import { test, expect, Browser } from '@playwright/test';
import { TEST_USERS, TEST_URLS } from '../utils/test-data';
test.describe('🔄 并发操作测试', () => {
test('#437 多窗口同时操作手术计费 @bug437', async ({ browser }) => {
// 打开两个浏览器上下文(模拟多标签页)
const context1 = await browser.newContext();
const context2 = await browser.newContext();
const page1 = await context1.newPage();
const page2 = await context2.newPage();
// 两个页面都登录
for (const page of [page1, page2]) {
await page.goto(TEST_URLS.login);
await page.fill('input[placeholder*="用户名"], input[placeholder*="账号"]', TEST_USERS.admin.username);
await page.fill('input[placeholder*="密码"]', TEST_USERS.admin.password);
await page.click('button:has-text("登录")');
await page.waitForURL(/.*(dashboard|home|index).*/);
}
// 两个页面同时访问手术计费
await Promise.all([
page1.goto(TEST_URLS.surgeryBilling),
page2.goto(TEST_URLS.surgeryBilling),
]);
await Promise.all([
page1.waitForLoadState('networkidle'),
page2.waitForLoadState('networkidle'),
]);
// 同时在两个页面点击生成
const genBtn1 = page1.locator('button:has-text("生成")');
const genBtn2 = page2.locator('button:has-text("生成")');
if (await genBtn1.isVisible() && await genBtn2.isVisible()) {
await Promise.all([
genBtn1.click().catch(() => {}),
genBtn2.click().catch(() => {}),
]);
await page1.waitForTimeout(3000);
// 验证数据一致性:不应出现重复记录
const table1 = page1.locator('el-table__body tr, .el-table__row');
const table2 = page2.locator('el-table__body tr, .el-table__row');
const count1 = await table1.count();
const count2 = await table2.count();
// 两个页面看到的数据应该一致
expect(count1).toBe(count2);
}
await context1.close();
await context2.close();
});
});

View File

@@ -0,0 +1,36 @@
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
import { DoctorStationPage } from '../pages/DoctorStationPage';
import { TEST_USERS, TEST_URLS } from '../utils/test-data';
test.describe('🏥 门诊医生站', () => {
let loginPage: LoginPage;
let doctorPage: DoctorStationPage;
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page);
doctorPage = new DoctorStationPage(page);
await loginPage.goto();
await loginPage.login(TEST_USERS.admin.username, TEST_USERS.admin.password);
await loginPage.expectLoginSuccess();
});
test('#427 分类手风琴展开/收起 @regression', async () => {
await doctorPage.goto();
const items = doctorPage.categoryItems;
const count = await items.count();
if (count >= 2) {
// 展开第一个
await doctorPage.expandCategory(0);
// 展开第二个,第一个应收起
await doctorPage.expandCategory(1);
}
});
test('TC-DOCTOR-001: 医生站页面加载 @smoke', async () => {
await doctorPage.goto();
await expect(doctorPage.page).toHaveURL(/.*doctorstation.*/);
});
});

View File

@@ -2,7 +2,7 @@ import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
import { TEST_USERS } from '../utils/test-data';
test.describe('登录模块', () => {
test.describe('🔐 登录模块', () => {
let loginPage: LoginPage;
test.beforeEach(async ({ page }) => {
@@ -10,18 +10,23 @@ test.describe('登录模块', () => {
await loginPage.goto();
});
test('用户登录成功', async ({ page }) => {
test('TC-LOGIN-001: 管理员正常登录 @smoke', async ({ page }) => {
await loginPage.login(TEST_USERS.admin.username, TEST_USERS.admin.password);
await loginPage.expectLoginSuccess();
});
test('登录失败-错误密码', async ({ page }) => {
await loginPage.login(TEST_USERS.admin.username, 'wrongpassword');
test('TC-LOGIN-002: 错误密码登录 @smoke', async ({ page }) => {
await loginPage.login(TEST_USERS.admin.username, 'wrong_password_123');
await loginPage.expectLoginFailed();
});
test('登录失败-空用户名', async ({ page }) => {
test('TC-LOGIN-003: 空用户名登录', async ({ page }) => {
await loginPage.login('', TEST_USERS.admin.password);
await loginPage.expectLoginFailed();
});
test('TC-LOGIN-004: 密码输入框可见性切换', async ({ page }) => {
const passwordInput = page.locator('input[placeholder*="密码"]');
await expect(passwordInput).toHaveAttribute('type', 'password');
});
});

View File

@@ -0,0 +1,43 @@
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
import { SurgeryBillingPage } from '../pages/SurgeryBillingPage';
import { TEST_USERS, TEST_URLS } from '../utils/test-data';
test.describe('💊 手术计费模块', () => {
let loginPage: LoginPage;
let surgeryPage: SurgeryBillingPage;
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page);
surgeryPage = new SurgeryBillingPage(page);
await loginPage.goto();
await loginPage.login(TEST_USERS.admin.username, TEST_USERS.admin.password);
await loginPage.expectLoginSuccess();
});
test('#437 快速连续点击防重复 @bug437 @smoke', async () => {
await surgeryPage.goto();
if (await surgeryPage.generateBtn.isVisible()) {
// 模拟用户快速连点
await surgeryPage.rapidClickGenerate(5);
await surgeryPage.page.waitForTimeout(3000);
// 验证没有产生重复对话框
const count = await surgeryPage.getDialogCount();
expect(count).toBeLessThanOrEqual(1);
}
});
test('#443 签发耗材不报库房错误 @bug443 @smoke', async () => {
await surgeryPage.goto();
if (await surgeryPage.signBtn.isVisible()) {
await surgeryPage.signBtn.click();
await surgeryPage.page.waitForTimeout(2000);
// 不应出现"发放库房为空"错误
await surgeryPage.expectNoLocationIdError();
}
});
});