17
openhis-ui-vue3/tests/e2e/fixtures/auth.ts
Executable file
17
openhis-ui-vue3/tests/e2e/fixtures/auth.ts
Executable file
@@ -0,0 +1,17 @@
|
||||
import { test as base } from '@playwright/test';
|
||||
import { TEST_USERS } from '../utils/test-data';
|
||||
|
||||
export const test = base.extend({
|
||||
async authenticatedPage({ page }, use) {
|
||||
// 登录
|
||||
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).*/);
|
||||
|
||||
await use(page);
|
||||
},
|
||||
});
|
||||
|
||||
export { expect } from '@playwright/test';
|
||||
26
openhis-ui-vue3/tests/e2e/pages/DoctorStationPage.ts
Executable file
26
openhis-ui-vue3/tests/e2e/pages/DoctorStationPage.ts
Executable file
@@ -0,0 +1,26 @@
|
||||
import { Page, expect } from '@playwright/test';
|
||||
|
||||
export class DoctorStationPage {
|
||||
readonly page: Page;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
}
|
||||
|
||||
async goto() {
|
||||
await this.page.goto('/doctorstation');
|
||||
await this.page.waitForLoadState('networkidle');
|
||||
}
|
||||
|
||||
async expandCategory(index: number = 0) {
|
||||
const item = this.page.locator('.el-collapse-item, .category-item').nth(index);
|
||||
await item.click();
|
||||
await this.page.waitForTimeout(500);
|
||||
}
|
||||
|
||||
async searchPatient(name: string) {
|
||||
await this.page.fill('input[placeholder*="患者"], input[placeholder*="姓名"]', name);
|
||||
await this.page.click('button:has-text("搜索"), button:has-text("查询")');
|
||||
await this.page.waitForLoadState('networkidle');
|
||||
}
|
||||
}
|
||||
46
openhis-ui-vue3/tests/e2e/pages/LoginPage.ts
Executable file
46
openhis-ui-vue3/tests/e2e/pages/LoginPage.ts
Executable file
@@ -0,0 +1,46 @@
|
||||
import { Page, expect } from '@playwright/test';
|
||||
|
||||
export class LoginPage {
|
||||
readonly page: Page;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
}
|
||||
|
||||
async goto() {
|
||||
await this.page.goto('/');
|
||||
await this.page.waitForLoadState('domcontentloaded');
|
||||
}
|
||||
|
||||
async login(username: string, password: string) {
|
||||
// Actual placeholders from login.vue: "账号" and "密码"
|
||||
await this.page.fill('input[placeholder="账号"]', username);
|
||||
await this.page.fill('input[placeholder="密码"]', password);
|
||||
// Check for tenant selection if exists
|
||||
const tenantSelect = this.page.locator('.el-select__wrapper, input[placeholder="请选择医疗机构"]').first();
|
||||
if (await tenantSelect.isVisible().catch(() => false)) {
|
||||
await tenantSelect.click();
|
||||
await this.page.waitForTimeout(500);
|
||||
// Select first option
|
||||
const firstOption = this.page.locator('.el-select-dropdown__item, .el-option').first();
|
||||
if (await firstOption.isVisible().catch(() => false)) {
|
||||
await firstOption.click();
|
||||
await this.page.waitForTimeout(500);
|
||||
}
|
||||
}
|
||||
await this.page.click('button:has-text("登 录")');
|
||||
await this.page.waitForLoadState('networkidle');
|
||||
}
|
||||
|
||||
async expectLoginSuccess() {
|
||||
await expect(this.page).toHaveURL(/.*(dashboard|home|index).*/, { timeout: 15000 });
|
||||
}
|
||||
|
||||
async expectLoginFailed() {
|
||||
await expect(this.page.locator('.el-message--error')).toBeVisible({ timeout: 5000 });
|
||||
}
|
||||
|
||||
async expectOnLoginPage() {
|
||||
await expect(this.page.locator('input[placeholder="账号"]')).toBeVisible();
|
||||
}
|
||||
}
|
||||
34
openhis-ui-vue3/tests/e2e/pages/SurgeryBillingPage.ts
Executable file
34
openhis-ui-vue3/tests/e2e/pages/SurgeryBillingPage.ts
Executable file
@@ -0,0 +1,34 @@
|
||||
import { Page, expect } from '@playwright/test';
|
||||
|
||||
export class SurgeryBillingPage {
|
||||
readonly page: Page;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
}
|
||||
|
||||
async goto() {
|
||||
await this.page.goto('/operatingroom');
|
||||
await this.page.waitForLoadState('networkidle');
|
||||
}
|
||||
|
||||
async rapidClickGenerate(times: number = 5) {
|
||||
const btn = this.page.locator('button:has-text("生成"), button:has-text("新增")');
|
||||
for (let i = 0; i < times; i++) {
|
||||
await btn.click().catch(() => {});
|
||||
}
|
||||
await this.page.waitForLoadState('networkidle');
|
||||
}
|
||||
|
||||
async getDialogCount(): Promise<number> {
|
||||
return await this.page.locator('.el-dialog, .el-message-box').count();
|
||||
}
|
||||
|
||||
async expectNoLocationIdError() {
|
||||
await expect(this.page.locator('text=发放库房为空')).toHaveCount(0, { timeout: 5000 });
|
||||
}
|
||||
|
||||
async expectSaveSuccess() {
|
||||
await expect(this.page.locator('.el-message--success')).toBeVisible({ timeout: 10000 });
|
||||
}
|
||||
}
|
||||
54
openhis-ui-vue3/tests/e2e/specs/concurrency.spec.ts
Executable file
54
openhis-ui-vue3/tests/e2e/specs/concurrency.spec.ts
Executable file
@@ -0,0 +1,54 @@
|
||||
import { test, expect } 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();
|
||||
|
||||
// Login on both pages
|
||||
for (const page of [page1, page2]) {
|
||||
await page.goto(TEST_URLS.login);
|
||||
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|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();
|
||||
});
|
||||
});
|
||||
34
openhis-ui-vue3/tests/e2e/specs/doctor-station.spec.ts
Executable file
34
openhis-ui-vue3/tests/e2e/specs/doctor-station.spec.ts
Executable file
@@ -0,0 +1,34 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { LoginPage } from '../pages/LoginPage';
|
||||
import { TEST_USERS, TEST_URLS } from '../utils/test-data';
|
||||
|
||||
test.describe('🏥 门诊医生站', () => {
|
||||
let loginPage: LoginPage;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
loginPage = new LoginPage(page);
|
||||
await loginPage.goto();
|
||||
await loginPage.login(TEST_USERS.admin.username, TEST_USERS.admin.password);
|
||||
await loginPage.expectLoginSuccess();
|
||||
});
|
||||
|
||||
test('#427 分类手风琴展开/收起 @regression', async ({ page }) => {
|
||||
await page.goto(TEST_URLS.doctorStation);
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const items = page.locator('.el-collapse-item, .category-item');
|
||||
const count = await items.count();
|
||||
|
||||
if (count >= 2) {
|
||||
await items.nth(0).click();
|
||||
await page.waitForTimeout(500);
|
||||
await items.nth(1).click();
|
||||
await page.waitForTimeout(500);
|
||||
}
|
||||
});
|
||||
|
||||
test('TC-DOCTOR-001: 医生站页面加载 @smoke', async ({ page }) => {
|
||||
await page.goto(TEST_URLS.doctorStation);
|
||||
await expect(page).toHaveURL(/.*doctorstation.*/);
|
||||
});
|
||||
});
|
||||
38
openhis-ui-vue3/tests/e2e/specs/login.spec.ts
Executable file
38
openhis-ui-vue3/tests/e2e/specs/login.spec.ts
Executable file
@@ -0,0 +1,38 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { LoginPage } from '../pages/LoginPage';
|
||||
import { TEST_USERS } from '../utils/test-data';
|
||||
|
||||
test.describe('🔐 登录模块', () => {
|
||||
let loginPage: LoginPage;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
loginPage = new LoginPage(page);
|
||||
await loginPage.goto();
|
||||
});
|
||||
|
||||
test('TC-LOGIN-001: 管理员正常登录 @smoke', async ({ page }) => {
|
||||
await loginPage.login(TEST_USERS.admin.username, TEST_USERS.admin.password);
|
||||
await loginPage.expectLoginSuccess();
|
||||
});
|
||||
|
||||
test('TC-LOGIN-002: 错误密码登录 @smoke', async ({ page }) => {
|
||||
await loginPage.login(TEST_USERS.admin.username, 'wrong_password_123');
|
||||
// Check for any error indication (message, toast, or stayed on login page)
|
||||
const hasError = await page.locator('.el-message--error, .el-message-box, text=密码错误, text=用户名或密码错误').isVisible().catch(() => false);
|
||||
const stillOnLogin = page.url().includes('login') || page.url() === 'http://localhost:81/' || page.url() === 'http://localhost:81/index';
|
||||
expect(hasError || stillOnLogin).toBeTruthy();
|
||||
});
|
||||
|
||||
test('TC-LOGIN-003: 空用户名登录', async ({ page }) => {
|
||||
await loginPage.login('', TEST_USERS.admin.password);
|
||||
// Should show validation error or stay on login page
|
||||
const hasError = await page.locator('.el-form-item__error, .el-message--error').isVisible().catch(() => false);
|
||||
const stillOnLogin = page.url().includes('login') || page.url() === 'http://localhost:81/';
|
||||
expect(hasError || stillOnLogin).toBeTruthy();
|
||||
});
|
||||
|
||||
test('TC-LOGIN-004: 密码输入框可见性切换', async ({ page }) => {
|
||||
const passwordInput = page.locator('input[placeholder="密码"]');
|
||||
await expect(passwordInput).toHaveAttribute('type', 'password');
|
||||
});
|
||||
});
|
||||
42
openhis-ui-vue3/tests/e2e/specs/surgery-billing.spec.ts
Executable file
42
openhis-ui-vue3/tests/e2e/specs/surgery-billing.spec.ts
Executable file
@@ -0,0 +1,42 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { LoginPage } from '../pages/LoginPage';
|
||||
import { TEST_USERS, TEST_URLS } from '../utils/test-data';
|
||||
|
||||
test.describe('💊 手术计费模块', () => {
|
||||
let loginPage: LoginPage;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
loginPage = new LoginPage(page);
|
||||
await loginPage.goto();
|
||||
await loginPage.login(TEST_USERS.admin.username, TEST_USERS.admin.password);
|
||||
await loginPage.expectLoginSuccess();
|
||||
});
|
||||
|
||||
test('#437 快速连续点击防重复 @bug437 @smoke', async ({ page }) => {
|
||||
await page.goto(TEST_URLS.surgeryBilling);
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const genBtn = page.locator('button:has-text("生成"), button:has-text("新增")');
|
||||
if (await genBtn.isVisible()) {
|
||||
for (let i = 0; i < 5; i++) {
|
||||
await genBtn.click().catch(() => {});
|
||||
}
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
const count = await page.locator('.el-dialog, .el-message-box').count();
|
||||
expect(count).toBeLessThanOrEqual(1);
|
||||
}
|
||||
});
|
||||
|
||||
test('#443 签发耗材不报库房错误 @bug443 @smoke', async ({ page }) => {
|
||||
await page.goto(TEST_URLS.surgeryBilling);
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const signBtn = page.locator('button:has-text("签发"), button:has-text("提交")');
|
||||
if (await signBtn.isVisible()) {
|
||||
await signBtn.click();
|
||||
await page.waitForTimeout(2000);
|
||||
await expect(page.locator('text=发放库房为空')).toHaveCount(0, { timeout: 5000 });
|
||||
}
|
||||
});
|
||||
});
|
||||
13
openhis-ui-vue3/tests/e2e/utils/test-data.ts
Executable file
13
openhis-ui-vue3/tests/e2e/utils/test-data.ts
Executable file
@@ -0,0 +1,13 @@
|
||||
export const TEST_USERS = {
|
||||
admin: {
|
||||
username: process.env.TEST_USERNAME || 'admin',
|
||||
password: process.env.TEST_PASSWORD || 'admin123',
|
||||
},
|
||||
};
|
||||
|
||||
export const TEST_URLS = {
|
||||
login: '/',
|
||||
dashboard: '/index',
|
||||
doctorStation: '/doctorstation',
|
||||
surgeryBilling: '/operatingroom',
|
||||
};
|
||||
Reference in New Issue
Block a user