Compare commits
13 Commits
290e8f8f15
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
405a9dfb72 | ||
| d1be841688 | |||
|
|
9b8655748e | ||
| 00fd6c8710 | |||
| bbd9d48fa6 | |||
| 8fb1d3e583 | |||
| 34ba7cae6a | |||
| 305ab15436 | |||
| 46a7076460 | |||
| e0e6693897 | |||
|
|
7d1e50d045 | ||
| 25ce12cebf | |||
| 7d55717037 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -63,3 +63,6 @@ public.sql
|
|||||||
发版记录/2025-11-12/发版日志.docx
|
发版记录/2025-11-12/发版日志.docx
|
||||||
.gitignore
|
.gitignore
|
||||||
openhis-server-new/openhis-application/src/main/resources/application-dev.yml
|
openhis-server-new/openhis-application/src/main/resources/application-dev.yml
|
||||||
|
.env.test.local
|
||||||
|
playwright-report/
|
||||||
|
test-results/
|
||||||
|
|||||||
214
docs/specs/playwright-e2e-testing-plan.md
Normal file
214
docs/specs/playwright-e2e-testing-plan.md
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
# HIS项目 Playwright E2E 自动化测试方案 v1.0
|
||||||
|
|
||||||
|
## 一、方案概述
|
||||||
|
|
||||||
|
### 1.1 选型理由
|
||||||
|
- **Playwright** 是微软开源的端到端测试框架,完美适配 Vue 3 + Vite 技术栈
|
||||||
|
- 自动等待机制适合HIS系统复杂交互场景(异步加载、动态渲染)
|
||||||
|
- 支持多浏览器(Chromium/Firefox/WebKit),CI/CD集成成熟
|
||||||
|
- 已有 `@playwright/test ^1.58.2` 依赖 installed
|
||||||
|
|
||||||
|
### 1.2 目标
|
||||||
|
1. 核心业务流程自动化覆盖率达到 80%+
|
||||||
|
2. 已修复Bug 100% 回归测试覆盖
|
||||||
|
3. 每次代码推送自动触发测试,失败阻断发布
|
||||||
|
|
||||||
|
## 二、项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
openhis-ui-vue3/
|
||||||
|
├── tests/
|
||||||
|
│ ├── e2e/
|
||||||
|
│ │ ├── fixtures/ # 测试夹具
|
||||||
|
│ │ │ └── auth.ts # 登录认证fixture
|
||||||
|
│ │ ├── pages/ # 页面对象模型(POM)
|
||||||
|
│ │ │ ├── LoginPage.ts
|
||||||
|
│ │ │ ├── DoctorStationPage.ts
|
||||||
|
│ │ │ └── SurgeryBillingPage.ts
|
||||||
|
│ │ ├── specs/ # 测试用例
|
||||||
|
│ │ │ ├── login.spec.ts
|
||||||
|
│ │ │ ├── doctor-station.spec.ts
|
||||||
|
│ │ │ ├── surgery-billing.spec.ts
|
||||||
|
│ │ │ └── bug-regression.spec.ts # Bug回归测试
|
||||||
|
│ │ └── utils/
|
||||||
|
│ │ └── test-data.ts # 测试数据
|
||||||
|
│ └── playwright.config.ts # Playwright配置
|
||||||
|
├── .env.test # 测试环境变量
|
||||||
|
└── package.json # 已有playwright依赖
|
||||||
|
```
|
||||||
|
|
||||||
|
## 三、环境配置
|
||||||
|
|
||||||
|
### 3.1 环境变量(.env.test)
|
||||||
|
```bash
|
||||||
|
# 测试环境配置
|
||||||
|
VITE_APP_BASE_API=http://192.168.110.253:8080
|
||||||
|
TEST_USERNAME=test_admin
|
||||||
|
TEST_PASSWORD=test123456
|
||||||
|
TEST_BASE_URL=http://localhost:80
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 Playwright配置(playwright.config.ts)
|
||||||
|
```typescript
|
||||||
|
import { defineConfig, devices } from '@playwright/test';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
testDir: './tests/e2e/specs',
|
||||||
|
timeout: 60 * 1000,
|
||||||
|
expect: { timeout: 10000 },
|
||||||
|
fullyParallel: false,
|
||||||
|
forbidOnly: !!process.env.CI,
|
||||||
|
retries: process.env.CI ? 2 : 0,
|
||||||
|
workers: 1,
|
||||||
|
reporter: [['html', { outputFolder: 'playwright-report' }], ['list']],
|
||||||
|
use: {
|
||||||
|
baseURL: process.env.TEST_BASE_URL || 'http://localhost:80',
|
||||||
|
trace: 'on-first-retry',
|
||||||
|
screenshot: 'only-on-failure',
|
||||||
|
video: 'retain-on-failure',
|
||||||
|
},
|
||||||
|
projects: [
|
||||||
|
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## 四、核心测试用例
|
||||||
|
|
||||||
|
### 4.1 登录测试(login.spec.ts)
|
||||||
|
```typescript
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
||||||
|
test('用户登录成功', async ({ page }) => {
|
||||||
|
await page.goto('/');
|
||||||
|
await page.fill('input[placeholder="请输入用户名"]', process.env.TEST_USERNAME || 'admin');
|
||||||
|
await page.fill('input[placeholder="请输入密码"]', process.env.TEST_PASSWORD || '123456');
|
||||||
|
await page.click('button:has-text("登录")');
|
||||||
|
await expect(page).toHaveURL(/.*dashboard.*/);
|
||||||
|
await expect(page.locator('.user-avatar')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('登录失败-错误密码', async ({ page }) => {
|
||||||
|
await page.goto('/');
|
||||||
|
await page.fill('input[placeholder="请输入用户名"]', 'admin');
|
||||||
|
await page.fill('input[placeholder="请输入密码"]', 'wrongpassword');
|
||||||
|
await page.click('button:has-text("登录")');
|
||||||
|
await expect(page.locator('.el-message--error')).toBeVisible();
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 门诊医生站测试(doctor-station.spec.ts)
|
||||||
|
```typescript
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
||||||
|
test.describe('门诊医生站', () => {
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
// 登录
|
||||||
|
await page.goto('/');
|
||||||
|
await page.fill('input[placeholder="请输入用户名"]', process.env.TEST_USERNAME || 'admin');
|
||||||
|
await page.fill('input[placeholder="请输入密码"]', process.env.TEST_PASSWORD || '123456');
|
||||||
|
await page.click('button:has-text("登录")');
|
||||||
|
await page.waitForURL(/.*dashboard.*/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('#427 检查项目分类手风琴展开', async ({ page }) => {
|
||||||
|
await page.goto('/doctorstation');
|
||||||
|
// 点击第一个分类
|
||||||
|
await page.click('.category-item >> nth=0');
|
||||||
|
await expect(page.locator('.category-content >> nth=0')).toBeVisible();
|
||||||
|
// 点击第二个分类,第一个应收起
|
||||||
|
await page.click('.category-item >> nth=1');
|
||||||
|
await expect(page.locator('.category-content >> nth=0')).not.toBeVisible();
|
||||||
|
await expect(page.locator('.category-content >> nth=1')).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 手术计费回归测试(bug-regression.spec.ts)
|
||||||
|
```typescript
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
||||||
|
test.describe('Bug回归测试', () => {
|
||||||
|
test('#437 手术计费防重复提交', async ({ page }) => {
|
||||||
|
// 登录并导航到手术计费
|
||||||
|
await page.goto('/');
|
||||||
|
await page.fill('input[placeholder="请输入用户名"]', process.env.TEST_USERNAME || 'admin');
|
||||||
|
await page.fill('input[placeholder="请输入密码"]', process.env.TEST_PASSWORD || '123456');
|
||||||
|
await page.click('button:has-text("登录")');
|
||||||
|
await page.waitForURL(/.*dashboard.*/);
|
||||||
|
await page.goto('/surgery-billing');
|
||||||
|
|
||||||
|
// 快速连续点击新增按钮(测试防重复锁)
|
||||||
|
const addBtn = page.locator('button:has-text("新增")');
|
||||||
|
await addBtn.click();
|
||||||
|
await addBtn.click(); // 第二次应被阻止
|
||||||
|
await addBtn.click(); // 第三次应被阻止
|
||||||
|
|
||||||
|
// 验证只弹出一个表单
|
||||||
|
await expect(page.locator('.el-dialog')).toHaveCount(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## 五、执行命令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 安装浏览器
|
||||||
|
npx playwright install chromium
|
||||||
|
|
||||||
|
# 运行所有测试
|
||||||
|
npm run test:e2e
|
||||||
|
|
||||||
|
# 运行单个测试文件
|
||||||
|
npx playwright test login.spec.ts
|
||||||
|
|
||||||
|
# 生成HTML报告
|
||||||
|
npx playwright show-report
|
||||||
|
|
||||||
|
# UI模式(调试用)
|
||||||
|
npx playwright test --ui
|
||||||
|
```
|
||||||
|
|
||||||
|
## 六、CI/CD集成
|
||||||
|
|
||||||
|
### 6.1 package.json脚本
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"scripts": {
|
||||||
|
"test:e2e": "playwright test",
|
||||||
|
"test:e2e:ui": "playwright test --ui",
|
||||||
|
"test:e2e:report": "playwright show-report"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 Spug流水线集成
|
||||||
|
```yaml
|
||||||
|
# Spug 构建后阶段添加
|
||||||
|
- name: E2E Testing
|
||||||
|
script: |
|
||||||
|
cd openhis-ui-vue3
|
||||||
|
npx playwright install --with-deps chromium
|
||||||
|
npm run test:e2e -- --reporter=html
|
||||||
|
# 测试失败则阻断发布
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "E2E测试失败,阻断发布!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
## 七、实施计划
|
||||||
|
|
||||||
|
| 阶段 | 时间 | 内容 | 负责人 |
|
||||||
|
|------|------|------|--------|
|
||||||
|
| Phase 1 | 第1周 | 登录+核心页面冒烟测试 | 张飞+赵云 |
|
||||||
|
| Phase 2 | 第2-3周 | 门诊医生站+手术计费全流程 | 张飞 |
|
||||||
|
| Phase 3 | 第4周 | Bug回归测试全覆盖 | 张飞 |
|
||||||
|
| Phase 4 | 第5周 | CI/CD流水线集成 | 赵云+运维 |
|
||||||
|
|
||||||
|
## 八、注意事项
|
||||||
|
|
||||||
|
1. **测试数据隔离**:使用独立的测试数据库,不污染生产数据
|
||||||
|
2. **环境变量**:敏感信息通过 `.env.test` 管理,不提交到git
|
||||||
|
3. **截图留痕**:失败时自动截图,便于排查
|
||||||
|
4. **测试优先**:新功能开发时同步编写测试用例
|
||||||
@@ -11,6 +11,7 @@ import com.openhis.common.constant.CommonConstants;
|
|||||||
import com.openhis.common.enums.*;
|
import com.openhis.common.enums.*;
|
||||||
import com.openhis.common.utils.EnumUtils;
|
import com.openhis.common.utils.EnumUtils;
|
||||||
import com.openhis.common.utils.HisQueryUtils;
|
import com.openhis.common.utils.HisQueryUtils;
|
||||||
|
import com.openhis.web.doctorstation.appservice.IDoctorStationMainAppService;
|
||||||
import com.openhis.web.doctorstation.appservice.ITodayOutpatientService;
|
import com.openhis.web.doctorstation.appservice.ITodayOutpatientService;
|
||||||
import com.openhis.web.doctorstation.dto.TodayOutpatientPatientDto;
|
import com.openhis.web.doctorstation.dto.TodayOutpatientPatientDto;
|
||||||
import com.openhis.web.doctorstation.dto.TodayOutpatientQueryParam;
|
import com.openhis.web.doctorstation.dto.TodayOutpatientQueryParam;
|
||||||
@@ -32,6 +33,9 @@ public class TodayOutpatientServiceImpl implements ITodayOutpatientService {
|
|||||||
@Resource
|
@Resource
|
||||||
private TodayOutpatientMapper todayOutpatientMapper;
|
private TodayOutpatientMapper todayOutpatientMapper;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private IDoctorStationMainAppService doctorStationMainAppService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public TodayOutpatientStatsDto getTodayOutpatientStats(HttpServletRequest request) {
|
public TodayOutpatientStatsDto getTodayOutpatientStats(HttpServletRequest request) {
|
||||||
Long doctorId = SecurityUtils.getLoginUser().getUserId();
|
Long doctorId = SecurityUtils.getLoginUser().getUserId();
|
||||||
@@ -259,22 +263,19 @@ public class TodayOutpatientServiceImpl implements ITodayOutpatientService {
|
|||||||
@Override
|
@Override
|
||||||
public R<?> receivePatient(Long encounterId, HttpServletRequest request) {
|
public R<?> receivePatient(Long encounterId, HttpServletRequest request) {
|
||||||
// 调用现有的接诊逻辑
|
// 调用现有的接诊逻辑
|
||||||
// 这里可以复用 DoctorStationMainAppServiceImpl 中的 receiveEncounter 方法
|
return doctorStationMainAppService.receiveEncounter(encounterId);
|
||||||
// 或者直接调用相应的服务
|
|
||||||
|
|
||||||
return R.ok("接诊成功");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public R<?> completeVisit(Long encounterId, HttpServletRequest request) {
|
public R<?> completeVisit(Long encounterId, HttpServletRequest request) {
|
||||||
// 调用现有的完诊逻辑
|
// 调用现有的完诊逻辑
|
||||||
return R.ok("就诊完成");
|
return doctorStationMainAppService.completeEncounter(encounterId, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public R<?> cancelVisit(Long encounterId, String reason, HttpServletRequest request) {
|
public R<?> cancelVisit(Long encounterId, String reason, HttpServletRequest request) {
|
||||||
// 调用现有的取消就诊逻辑
|
// 调用现有的取消就诊逻辑
|
||||||
return R.ok("就诊取消成功");
|
return doctorStationMainAppService.cancelEncounter(encounterId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -302,4 +303,4 @@ public class TodayOutpatientServiceImpl implements ITodayOutpatientService {
|
|||||||
|
|
||||||
return orderBy;
|
return orderBy;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -350,11 +350,16 @@ public class NurseBillingAppService implements INurseBillingAppService {
|
|||||||
// 1. 筛选临时类型耗材:仅处理临时医嘱(TherapyTimeType.TEMPORARY),且请求ID不为空
|
// 1. 筛选临时类型耗材:仅处理临时医嘱(TherapyTimeType.TEMPORARY),且请求ID不为空
|
||||||
List<AdviceSaveDto> tempDeviceList = deviceAdviceList.stream().collect(Collectors.toList());
|
List<AdviceSaveDto> tempDeviceList = deviceAdviceList.stream().collect(Collectors.toList());
|
||||||
|
|
||||||
// 2. 校验发放库房:必须指定耗材发放库房(locationId),否则抛出业务异常
|
// 2. 颞理发放库房:为locationId为null的项目设置默认值
|
||||||
if (tempDeviceList.stream().anyMatch(t -> t.getLocationId() == null)) {
|
for (AdviceSaveDto advice : tempDeviceList) {
|
||||||
throw new ServiceException("耗材划价失败:发放库房为空,请重新选择");
|
if (advice.getLocationId() == null) {
|
||||||
|
// 设置默认位置为用户组织ID作为fallback
|
||||||
|
LoginUser loginUser = SecurityUtils.getLoginUser();
|
||||||
|
if (loginUser != null && loginUser.getOrgId() != null) {
|
||||||
|
advice.setLocationId(loginUser.getOrgId());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 循环处理每个临时耗材医嘱(逐条生成关联数据)
|
// 3. 循环处理每个临时耗材医嘱(逐条生成关联数据)
|
||||||
for (AdviceSaveDto adviceDto : tempDeviceList) {
|
for (AdviceSaveDto adviceDto : tempDeviceList) {
|
||||||
// 3.1 生成耗材请求记录(WOR_DEVICE_REQUEST):状态设为激活,来源标记为护士划价
|
// 3.1 生成耗材请求记录(WOR_DEVICE_REQUEST):状态设为激活,来源标记为护士划价
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
COALESCE(pi.identifier_no, ap.bus_no, '') AS identifierNo
|
COALESCE(pi.identifier_no, ap.bus_no, '') AS identifierNo
|
||||||
FROM op_schedule os
|
FROM op_schedule os
|
||||||
LEFT JOIN adm_patient ap ON os.patient_id = ap.id
|
LEFT JOIN adm_patient ap ON os.patient_id = ap.id
|
||||||
LEFT JOIN cli_surgery cs ON os.oper_code = cs.surgery_no AND cs.delete_flag = '0'
|
INNER JOIN cli_surgery cs ON os.oper_code = cs.surgery_no AND cs.delete_flag = '0'
|
||||||
LEFT JOIN adm_organization o ON cs.org_id = o.id
|
LEFT JOIN adm_organization o ON cs.org_id = o.id
|
||||||
LEFT JOIN sys_tenant st ON st.id = os.tenant_id
|
LEFT JOIN sys_tenant st ON st.id = os.tenant_id
|
||||||
LEFT JOIN sys_user su ON su.user_id = os.creator_id
|
LEFT JOIN sys_user su ON su.user_id = os.creator_id
|
||||||
@@ -92,7 +92,7 @@
|
|||||||
COALESCE(pi.identifier_no, ap.bus_no, '') AS identifierNo
|
COALESCE(pi.identifier_no, ap.bus_no, '') AS identifierNo
|
||||||
FROM op_schedule os
|
FROM op_schedule os
|
||||||
LEFT JOIN adm_patient ap ON os.patient_id = ap.id
|
LEFT JOIN adm_patient ap ON os.patient_id = ap.id
|
||||||
LEFT JOIN cli_surgery cs ON os.oper_code = cs.surgery_no AND cs.delete_flag = '0'
|
INNER JOIN cli_surgery cs ON os.oper_code = cs.surgery_no AND cs.delete_flag = '0'
|
||||||
LEFT JOIN adm_organization o ON cs.org_id = o.id
|
LEFT JOIN adm_organization o ON cs.org_id = o.id
|
||||||
LEFT JOIN doc_request_form drf ON drf.prescription_no=cs.surgery_no
|
LEFT JOIN doc_request_form drf ON drf.prescription_no=cs.surgery_no
|
||||||
LEFT JOIN (
|
LEFT JOIN (
|
||||||
@@ -153,7 +153,7 @@
|
|||||||
COALESCE(pi.identifier_no, ap.bus_no, '') AS identifierNo
|
COALESCE(pi.identifier_no, ap.bus_no, '') AS identifierNo
|
||||||
FROM op_schedule os
|
FROM op_schedule os
|
||||||
LEFT JOIN adm_patient ap ON os.patient_id = ap.id
|
LEFT JOIN adm_patient ap ON os.patient_id = ap.id
|
||||||
LEFT JOIN cli_surgery cs ON os.oper_code = cs.surgery_no AND cs.delete_flag = '0'
|
INNER JOIN cli_surgery cs ON os.oper_code = cs.surgery_no AND cs.delete_flag = '0'
|
||||||
LEFT JOIN adm_organization o ON cs.org_id = o.id
|
LEFT JOIN adm_organization o ON cs.org_id = o.id
|
||||||
LEFT JOIN sys_tenant st ON st.id = os.tenant_id
|
LEFT JOIN sys_tenant st ON st.id = os.tenant_id
|
||||||
LEFT JOIN sys_user su ON su.user_id = os.creator_id
|
LEFT JOIN sys_user su ON su.user_id = os.creator_id
|
||||||
|
|||||||
@@ -1,12 +1,5 @@
|
|||||||
# 页面标题
|
# Playwright E2E 测试环境变量
|
||||||
VITE_APP_TITLE = 医院信息管理系统
|
# 注意:此文件仅用于本地开发,生产环境使用CI Secret管理
|
||||||
|
TEST_BASE_URL=http://localhost:80
|
||||||
# 测试环境配置
|
TEST_USERNAME=admin
|
||||||
VITE_APP_ENV = 'test'
|
TEST_PASSWORD=changeme_in_local_env
|
||||||
|
|
||||||
# OpenHIS管理系统/测试环境
|
|
||||||
|
|
||||||
VITE_APP_BASE_API = '/test-api'
|
|
||||||
|
|
||||||
# 租户ID配置
|
|
||||||
VITE_APP_TENANT_ID = '1'
|
|
||||||
|
|||||||
5
openhis-ui-vue3/.gitignore
vendored
5
openhis-ui-vue3/.gitignore
vendored
@@ -21,3 +21,8 @@ selenium-debug.log
|
|||||||
|
|
||||||
package-lock.json
|
package-lock.json
|
||||||
yarn.lock
|
yarn.lock
|
||||||
|
|
||||||
|
# Playwright test results
|
||||||
|
test-results/
|
||||||
|
tests/e2e/report/
|
||||||
|
tests/tests/
|
||||||
|
|||||||
@@ -17,7 +17,10 @@
|
|||||||
"test:run": "vitest run",
|
"test:run": "vitest run",
|
||||||
"test:coverage": "vitest run --coverage",
|
"test:coverage": "vitest run --coverage",
|
||||||
"test:ui": "vitest --ui",
|
"test:ui": "vitest --ui",
|
||||||
"lint": "eslint . --ext .js,.vue src/"
|
"lint": "eslint . --ext .js,.vue src/",
|
||||||
|
"test:e2e": "playwright test",
|
||||||
|
"test:e2e:ui": "playwright test --ui",
|
||||||
|
"test:e2e:report": "playwright show-report"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -87,4 +90,4 @@
|
|||||||
"vitest": "^4.0.18",
|
"vitest": "^4.0.18",
|
||||||
"vue-tsc": "^3.1.8"
|
"vue-tsc": "^3.1.8"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
28
openhis-ui-vue3/playwright.config.ts
Normal file
28
openhis-ui-vue3/playwright.config.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { defineConfig, devices } from '@playwright/test';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
testDir: './tests/e2e/specs',
|
||||||
|
fullyParallel: true,
|
||||||
|
timeout: 60_000,
|
||||||
|
expect: { timeout: 10_000 },
|
||||||
|
retries: process.env.CI ? 2 : 1,
|
||||||
|
workers: process.env.CI ? 2 : undefined,
|
||||||
|
reporter: [
|
||||||
|
['html', { outputFolder: 'tests/e2e/report', open: 'never' }],
|
||||||
|
['list'],
|
||||||
|
],
|
||||||
|
use: {
|
||||||
|
baseURL: process.env.TEST_BASE_URL || 'http://localhost:81',
|
||||||
|
screenshot: 'only-on-failure',
|
||||||
|
video: 'retain-on-failure',
|
||||||
|
trace: 'retain-on-failure',
|
||||||
|
viewport: { width: 1920, height: 1080 },
|
||||||
|
locale: 'zh-CN',
|
||||||
|
timezoneId: 'Asia/Shanghai',
|
||||||
|
actionTimeout: 15_000,
|
||||||
|
navigationTimeout: 30_000,
|
||||||
|
},
|
||||||
|
projects: [
|
||||||
|
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
|
||||||
|
],
|
||||||
|
});
|
||||||
@@ -100,7 +100,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
© 2025 {{ currentTenantName || settings.systemName }}信息管理系统 | 版本 v2.5.1
|
© 2025 {{ currentTenantName || settings.systemName }}信息管理系统 | 版本 {{ loginVersion }}
|
||||||
<!-- 公司版权信息(新增) -->
|
<!-- 公司版权信息(新增) -->
|
||||||
<div class="company-copyright">
|
<div class="company-copyright">
|
||||||
技术支持:上海经创贺联信息技术有限公司
|
技术支持:上海经创贺联信息技术有限公司
|
||||||
@@ -141,6 +141,7 @@ const route = useRoute();
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { proxy } = getCurrentInstance();
|
const { proxy } = getCurrentInstance();
|
||||||
const env = import.meta.env.MODE;
|
const env = import.meta.env.MODE;
|
||||||
|
const loginVersion = import.meta.env.VITE_APP_BUILD_VERSION;
|
||||||
|
|
||||||
const loginForm = ref({
|
const loginForm = ref({
|
||||||
username: '',
|
username: '',
|
||||||
|
|||||||
@@ -1941,6 +1941,7 @@ function submitForm() {
|
|||||||
// 新增手术安排
|
// 新增手术安排
|
||||||
addSurgerySchedule(submitData).then((res) => {
|
addSurgerySchedule(submitData).then((res) => {
|
||||||
proxy.$modal.msgSuccess('新增成功')
|
proxy.$modal.msgSuccess('新增成功')
|
||||||
|
queryParams.pageNo = 1
|
||||||
open.value = false
|
open.value = false
|
||||||
getPageList()
|
getPageList()
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
|
|||||||
17
openhis-ui-vue3/tests/e2e/fixtures/auth.ts
Normal file
17
openhis-ui-vue3/tests/e2e/fixtures/auth.ts
Normal 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
Normal file
26
openhis-ui-vue3/tests/e2e/pages/DoctorStationPage.ts
Normal 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
Normal file
46
openhis-ui-vue3/tests/e2e/pages/LoginPage.ts
Normal 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
Normal file
34
openhis-ui-vue3/tests/e2e/pages/SurgeryBillingPage.ts
Normal 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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
53
openhis-ui-vue3/tests/e2e/specs/bug-regression.spec.ts
Normal file
53
openhis-ui-vue3/tests/e2e/specs/bug-regression.spec.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
import { LoginPage } from '../pages/LoginPage';
|
||||||
|
import { TEST_USERS, TEST_URLS } from '../utils/test-data';
|
||||||
|
|
||||||
|
test.describe('🐛 Bug回归测试', () => {
|
||||||
|
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 @regression', async ({ page }) => {
|
||||||
|
await page.goto(TEST_URLS.surgeryBilling);
|
||||||
|
await page.waitForLoadState('networkidle');
|
||||||
|
|
||||||
|
const addBtn = page.locator('button:has-text("新增"), button:has-text("生成")');
|
||||||
|
if (await addBtn.isVisible()) {
|
||||||
|
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('#443 手术计费签发耗材 @bug443 @regression', 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);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
54
openhis-ui-vue3/tests/e2e/specs/concurrency.spec.ts
Normal file
54
openhis-ui-vue3/tests/e2e/specs/concurrency.spec.ts
Normal 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
Normal file
34
openhis-ui-vue3/tests/e2e/specs/doctor-station.spec.ts
Normal 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
Normal file
38
openhis-ui-vue3/tests/e2e/specs/login.spec.ts
Normal 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
Normal file
42
openhis-ui-vue3/tests/e2e/specs/surgery-billing.spec.ts
Normal 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
Normal file
13
openhis-ui-vue3/tests/e2e/utils/test-data.ts
Normal 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',
|
||||||
|
};
|
||||||
28
openhis-ui-vue3/tests/playwright.config.ts
Normal file
28
openhis-ui-vue3/tests/playwright.config.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { defineConfig, devices } from '@playwright/test';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
testDir: './e2e/specs',
|
||||||
|
fullyParallel: true,
|
||||||
|
timeout: 60_000,
|
||||||
|
expect: { timeout: 10_000 },
|
||||||
|
retries: process.env.CI ? 2 : 1,
|
||||||
|
workers: process.env.CI ? 2 : undefined,
|
||||||
|
reporter: [
|
||||||
|
['html', { outputFolder: 'tests/e2e/report', open: 'never' }],
|
||||||
|
['list'],
|
||||||
|
],
|
||||||
|
use: {
|
||||||
|
baseURL: process.env.TEST_BASE_URL || 'http://localhost:81',
|
||||||
|
screenshot: 'only-on-failure',
|
||||||
|
video: 'retain-on-failure',
|
||||||
|
trace: 'retain-on-failure',
|
||||||
|
viewport: { width: 1920, height: 1080 },
|
||||||
|
locale: 'zh-CN',
|
||||||
|
timezoneId: 'Asia/Shanghai',
|
||||||
|
actionTimeout: 15_000,
|
||||||
|
navigationTimeout: 30_000,
|
||||||
|
},
|
||||||
|
projects: [
|
||||||
|
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
|
||||||
|
],
|
||||||
|
});
|
||||||
@@ -11,11 +11,11 @@ import createVitePlugins from './vite/plugins';
|
|||||||
export default defineConfig(({ mode, command }) => {
|
export default defineConfig(({ mode, command }) => {
|
||||||
const env = loadEnv(mode, process.cwd());
|
const env = loadEnv(mode, process.cwd());
|
||||||
const { VITE_APP_ENV } = env;
|
const { VITE_APP_ENV } = env;
|
||||||
|
const buildVersion = process.env.VITE_APP_VERSION || env.VITE_APP_VERSION || Date.now().toString();
|
||||||
return {
|
return {
|
||||||
// define: {
|
define: {
|
||||||
// // enable hydration mismatch details in production build
|
'import.meta.env.VITE_APP_BUILD_VERSION': JSON.stringify(buildVersion),
|
||||||
// __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: 'true'
|
},
|
||||||
// },
|
|
||||||
// 部署生产环境和开发环境下的URL。
|
// 部署生产环境和开发环境下的URL。
|
||||||
// 默认情况下,vite 会假设你的应用是被部署在一个域名的根路径上
|
// 默认情况下,vite 会假设你的应用是被部署在一个域名的根路径上
|
||||||
// 例如 https://www.openHIS.vip/。如果应用被部署在一个子路径上,你就需要用这个选项指定这个子路径。例如,如果你的应用被部署在 https://www.openhis.vip/admin/,则设置 baseUrl 为 /admin/。
|
// 例如 https://www.openHIS.vip/。如果应用被部署在一个子路径上,你就需要用这个选项指定这个子路径。例如,如果你的应用被部署在 https://www.openhis.vip/admin/,则设置 baseUrl 为 /admin/。
|
||||||
|
|||||||
Reference in New Issue
Block a user