ykq-playwright-ci 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,261 +1,111 @@
1
- # Playwright CI 使用说明
1
+ # Playwright CI
2
2
 
3
- 独立 Playwright 自动化测试工程。当前工程用于承接医生端 PC 的 E2E 自动化测试,不把 Playwright 依赖直接放进医生端 Vue2 / Node 14 业务仓。
3
+ 一个本地 Playwright Runner:按项目名拉取对应用例库,然后在测试环境跑自动化用例。
4
4
 
5
- ## 适用范围
6
-
7
- 当前优先覆盖医生端测试环境的基础链路:
8
-
9
- - 登录页是否能正常渲染。
10
- - 未登录访问首页是否按路由守卫跳回登录页。
11
- - 用户协议页是否允许未登录访问。
12
- - 登录表单的基础校验是否符合 `doctor-master` 逻辑。
13
-
14
- 这些用例不需要真实医生账号,也不会真的登录、开方或提交处方。登录后业务链路需要先解决测试账号、验证码或登录态方案。
15
-
16
- ## 环境要求
17
-
18
- 本地使用这个 runner 前,需要准备这些环境:
19
-
20
- | 环境 | 要求 | 说明 |
21
- | --- | --- | --- |
22
- | Node.js | 建议使用 `.nvmrc` 指定的 `22` | 本工具运行在 Node 侧,不受医生端业务仓 Node 14 限制 |
23
- | npm | 随 Node 安装即可 | 推荐用 `npm ci` 或 `npm install` 安装依赖 |
24
- | Git | 需要可执行 `git clone`、`git fetch`、`git pull` | 用于拉取各项目的用例库 |
25
- | Git 权限 | 需要能访问 `git.int.ybm100.com` | 可用本机 Git 凭据,也可临时传 `GIT_USERNAME` / `GIT_PASSWORD` |
26
- | 浏览器 | Playwright Chromium | 首次运行前执行 `npx playwright install chromium` |
27
- | 网络 | 需要访问 Git 仓库和目标测试环境 | 医生端默认访问 `https://ys-test.heyejk.com` |
28
-
29
- 当前基础用例不需要医生测试账号。后续如果跑登录后链路,还需要测试医生账号、验证码绕过方案,或 `playwright/.auth/doctor.json` 登录态文件。
30
-
31
- 不要把账号、密码、token、登录态写进 Git:
5
+ 当前默认项目是医生端 PC:
32
6
 
33
7
  ```text
34
- .env
35
- playwright/.auth/*.json
36
- GIT_PASSWORD
37
- ```
38
-
39
- ## 环境准备
40
-
41
- 进入工程目录:
42
-
43
- ```bash
44
- cd /Users/huangcongqiang/Desktop/products/playwright-ci
8
+ doctor-pc -> https://git.int.ybm100.com/ykq/doctor-pc-web-test-store
45
9
  ```
46
10
 
47
- 安装依赖:
48
-
49
- ```bash
50
- npm install
51
- ```
11
+ ## 先跑起来
52
12
 
53
- 如果使用 `nvm`:
13
+ 需要先准备:
54
14
 
55
- ```bash
56
- nvm use
57
- ```
15
+ - Node.js `22+`
16
+ - npm
17
+ - Git,并且能访问 `git.int.ybm100.com`
18
+ - 能访问目标测试环境,例如 `https://ys-test.heyejk.com`
58
19
 
59
- 安装浏览器:
60
-
61
- ```bash
62
- npx playwright install chromium
63
- ```
64
-
65
- 如果机器已经装过对应版本,Playwright 会复用缓存。
66
-
67
- ## 通过 npm 安装使用
68
-
69
- npm 包名使用 `ykq-playwright-ci`。`playwright-ci` 这个包名已经被 npm 上其它作者占用,所以本工具发布时不能继续使用原仓库名。
70
-
71
- 全局安装:
20
+ 安装工具:
72
21
 
73
22
  ```bash
74
23
  npm install -g ykq-playwright-ci
75
24
  ```
76
25
 
77
- 首次在某个目录使用时,先初始化项目配置:
26
+ 建一个专门放配置和用例缓存的目录:
78
27
 
79
28
  ```bash
80
29
  mkdir playwright-runner
81
30
  cd playwright-runner
82
- playwright-ci init
83
- ```
84
-
85
- 初始化后会在当前目录生成:
86
-
87
- ```text
88
- projects.config.json
89
- ```
90
-
91
- 之后可以直接按项目运行:
92
-
93
- ```bash
94
- playwright-ci list
95
- playwright-ci run doctor-pc
96
- playwright-ci run doctor-pc --headed
97
- playwright-ci run doctor-pc --debug
98
- ```
99
-
100
- 也可以使用项目配置命令:
101
-
102
- ```bash
103
- playwright-ci add
104
- playwright-ci show doctor-pc
105
- playwright-ci update doctor-pc
106
- playwright-ci delete doctor-pc
107
- ```
108
-
109
- 如果不想全局安装,发布后也可以用 `npx`:
110
-
111
- ```bash
112
- npx ykq-playwright-ci init
113
- npx ykq-playwright-ci run doctor-pc
114
- ```
115
-
116
- 从 npm 安装后,Git 用例库仍然拉到当前工作目录的 `.case-repos/`,`projects.config.json` 也保存在当前工作目录,不会写到全局 `node_modules` 目录。
117
-
118
- ## 最常用命令
119
-
120
- 查看已配置的项目:
121
-
122
- ```bash
123
- npm run project:list
124
31
  ```
125
32
 
126
- 交互式新增项目配置:
33
+ 初始化配置:
127
34
 
128
35
  ```bash
129
- npm run project:add
36
+ playwright-ci init
130
37
  ```
131
38
 
132
- 查看某个项目详情:
39
+ 安装浏览器:
133
40
 
134
41
  ```bash
135
- npm run project:show -- doctor-pc
42
+ playwright-ci install chromium
136
43
  ```
137
44
 
138
- 交互式更新项目配置:
45
+ 运行医生端用例:
139
46
 
140
47
  ```bash
141
- npm run project:update -- doctor-pc
48
+ playwright-ci run doctor-pc
142
49
  ```
143
50
 
144
- 删除项目配置:
51
+ 如果 Git 需要账号密码:
145
52
 
146
53
  ```bash
147
- npm run project:delete -- doctor-pc
54
+ GIT_USERNAME=你的用户名 GIT_PASSWORD='你的密码或访问令牌' playwright-ci run doctor-pc
148
55
  ```
149
56
 
150
- 后台无头运行测试环境基础用例:
151
-
152
- ```bash
153
- npm run doctor:test
154
- ```
57
+ 不要把账号密码写进 `projects.config.json`。
155
58
 
156
- 打开浏览器窗口,看 Playwright 实际操作:
59
+ ## 常用命令
157
60
 
158
- ```bash
159
- npm run doctor:test:headed
160
- ```
61
+ | 命令 | 用途 |
62
+ | --- | --- |
63
+ | `playwright-ci init` | 在当前目录生成 `projects.config.json` |
64
+ | `playwright-ci list` | 查看已有项目 |
65
+ | `playwright-ci run doctor-pc` | 后台运行医生端用例 |
66
+ | `playwright-ci run doctor-pc --headed` | 打开浏览器窗口运行 |
67
+ | `playwright-ci run doctor-pc --debug` | 调试模式运行 |
68
+ | `playwright-ci run doctor-pc --ui` | 打开 Playwright UI |
69
+ | `playwright-ci add` | 交互式新增项目 |
70
+ | `playwright-ci show doctor-pc` | 查看项目配置 |
71
+ | `playwright-ci update doctor-pc` | 修改项目配置 |
72
+ | `playwright-ci delete doctor-pc` | 删除项目配置 |
73
+ | `playwright-ci install chromium` | 安装 Chromium 浏览器 |
161
74
 
162
- 调试模式,适合逐步看定位、点击和断言:
75
+ 也可以不全局安装,直接用 `npx`:
163
76
 
164
77
  ```bash
165
- npm run doctor:test:debug
78
+ npx ykq-playwright-ci init
79
+ npx ykq-playwright-ci run doctor-pc
166
80
  ```
167
81
 
168
- 打开 Playwright UI,适合筛选、重跑、查看每一步:
169
-
170
- ```bash
171
- npm run doctor:test:ui
172
- ```
82
+ ## 它实际做了什么
173
83
 
174
- 查看最近一次测试报告:
84
+ 执行:
175
85
 
176
86
  ```bash
177
- npm run report
178
- ```
179
-
180
- ## 命令说明
181
-
182
- | 命令 | 用途 | 是否打开浏览器窗口 |
183
- | --- | --- | --- |
184
- | `npm run project:list` | 查看当前 runner 已配置的项目 | 否 |
185
- | `npm run project:show -- doctor-pc` | 查看单个项目完整配置 | 否 |
186
- | `npm run project:add` | 交互式新增项目配置 | 否 |
187
- | `npm run project:update -- doctor-pc` | 交互式更新项目配置 | 否 |
188
- | `npm run project:delete -- doctor-pc` | 删除项目配置,需要输入 `delete` 确认 | 否 |
189
- | `npm run project:test -- doctor-pc` | 按项目名运行用例 | 否 |
190
- | `npm run doctor:test` | 跑医生端测试环境基础用例,适合日常验证和 CI | 否 |
191
- | `npm run doctor:test:headed` | 跑同一批用例,同时显示浏览器窗口 | 是 |
192
- | `npm run doctor:test:debug` | 调试用例,可暂停、逐步执行 | 是 |
193
- | `npm run doctor:test:ui` | 打开 Playwright UI 选择和重跑用例 | 是 |
194
- | `npm run doctor:test:local` | 不拉远程仓库,直接跑当前 runner 内置医生端用例 | 否 |
195
- | `npm run patient:test` | 跑患者端用例库;需要先配置 repo | 否 |
196
- | `npm run admin:test` | 跑后台用例库;需要先配置 repo | 否 |
197
- | `npm run report` | 打开 HTML 测试报告 | 是 |
198
- | `npm run typecheck` | TypeScript 类型检查 | 否 |
199
- | `npm run lint` | ESLint 检查 | 否 |
200
-
201
- ## 多项目用例库
202
-
203
- 本工程现在是一个本地 Playwright Runner。Runner 负责:
204
-
205
- - 管理 Playwright 版本、浏览器、报告和通用命令。
206
- - 按项目名读取配置。
207
- - 如果项目配置了 Git 用例库,运行前自动 clone 或 `git pull --ff-only`。
208
- - 根据项目配置设置测试环境变量。
209
- - 调用 Playwright 执行对应用例目录。
210
-
211
- 医生端用例库:
212
-
213
- ```text
214
- https://git.int.ybm100.com/ykq/doctor-pc-web-test-store
215
- ```
216
-
217
- 项目配置文件:
218
-
219
- ```text
220
- projects.config.json
221
- ```
222
-
223
- 当前结构:
224
-
225
- ```json
226
- {
227
- "caseCacheDir": ".case-repos",
228
- "projects": {
229
- "doctor-pc": {
230
- "repo": "https://git.int.ybm100.com/ykq/doctor-pc-web-test-store",
231
- "branch": "master",
232
- "caseDir": ".case-repos/doctor-pc",
233
- "env": {
234
- "DOCTOR_BASE_URL": "https://ys-test.heyejk.com",
235
- "DOCTOR_PUBLIC_PATH": "/physician-admin/",
236
- "DOCTOR_LOGIN_PATH": "/physician-admin/login"
237
- },
238
- "playwrightArgs": ["--project=chromium", "--no-deps"]
239
- }
240
- }
241
- }
87
+ playwright-ci run doctor-pc
242
88
  ```
243
89
 
244
- `doctor-pc` 已配置为远程用例库。首次运行会拉取到:
90
+ 流程是:
245
91
 
246
92
  ```text
247
- .case-repos/doctor-pc
93
+ 读取当前目录的 projects.config.json
94
+ 拉取 doctor-pc 对应的 Git 用例库
95
+ 缓存到 .case-repos/doctor-pc
96
+ 设置 DOCTOR_BASE_URL 等环境变量
97
+ 调用 Playwright 跑测试
248
98
  ```
249
99
 
250
- 如果 Git 需要登录,可以用本机 Git 凭据管理,也可以临时通过环境变量传入:
100
+ 默认是无头浏览器,所以不会弹窗口。要看真实页面操作,用:
251
101
 
252
102
  ```bash
253
- GIT_USERNAME=你的用户名 GIT_PASSWORD='你的密码或访问令牌' npm run doctor:test
103
+ playwright-ci run doctor-pc --headed
254
104
  ```
255
105
 
256
- 不要把账号密码写进 `projects.config.json`、README、脚本或 Git remote URL。
106
+ ## 默认医生端配置
257
107
 
258
- 远程用例库配置示例:
108
+ `playwright-ci init` 生成的 `doctor-pc` 配置大致是:
259
109
 
260
110
  ```json
261
111
  {
@@ -271,283 +121,167 @@ GIT_USERNAME=你的用户名 GIT_PASSWORD='你的密码或访问令牌' npm run
271
121
  }
272
122
  ```
273
123
 
274
- 运行时逻辑:
124
+ 当前基础用例会访问:
275
125
 
276
126
  ```text
277
- npm run doctor:test
278
- -> 读取 projects.config.json 的 doctor-pc
279
- -> 如果 repo 不为空,clone / pull 到 .case-repos/doctor-pc
280
- -> 设置 DOCTOR_BASE_URL 等环境变量
281
- -> 执行 Playwright
127
+ https://ys-test.heyejk.com/physician-admin/login
128
+ https://ys-test.heyejk.com/physician-admin/home
129
+ https://ys-test.heyejk.com/physician-admin/serviceAgreement
282
130
  ```
283
131
 
284
- `.case-repos/` 是本地缓存目录,已加入 `.gitignore`,不要提交。
132
+ 当前基础用例不需要真实医生账号,不会真的登录、开方或提交处方。
133
+
134
+ ## 项目配置怎么改
285
135
 
286
- 如果本地用例库有未提交改动,runner 会停止自动拉取,避免覆盖你调试中的用例。确认不需要这些改动后,再自己处理对应用例库的 Git 状态。
136
+ 查看:
287
137
 
288
- 通用运行方式:
138
+ ```bash
139
+ playwright-ci show doctor-pc
140
+ ```
141
+
142
+ 交互式修改:
289
143
 
290
144
  ```bash
291
- npm run project:test -- doctor-pc
292
- npm run project:test -- doctor-pc --headed
293
- npm run project:test -- doctor-pc --debug
145
+ playwright-ci update doctor-pc
294
146
  ```
295
147
 
296
- 项目配置增删改查:
148
+ 新增一个项目:
297
149
 
298
150
  ```bash
299
- npm run project:list
300
- npm run project:show -- doctor-pc
301
- npm run project:add
302
- npm run project:update -- doctor-pc
303
- npm run project:delete -- doctor-pc
151
+ playwright-ci add
304
152
  ```
305
153
 
306
- 新增和更新会依次询问:
154
+ 新增或修改时会问这些内容:
307
155
 
308
156
  - 项目名
309
157
  - 用例库 Git 地址
310
158
  - 分支
311
- - 本地用例缓存目录
159
+ - 本地缓存目录
312
160
  - 项目说明
313
161
  - Playwright 参数
314
162
  - 环境变量
315
163
 
316
- 写入后会更新 `projects.config.json`。账号密码不要写进项目配置,运行时通过本机 Git 凭据或临时环境变量传入:
317
-
318
- ```bash
319
- GIT_USERNAME=你的用户名 GIT_PASSWORD='你的密码或访问令牌' npm run project:test -- 项目名
320
- ```
321
-
322
- 如果只想跑 runner 内置的临时本地用例,不拉远程用例库:
323
-
324
- ```bash
325
- npm run doctor:test:local
326
- ```
327
-
328
- ## 默认测试环境
329
-
330
- 医生端测试环境命令默认使用:
164
+ 配置文件就是当前目录的:
331
165
 
332
166
  ```text
333
- DOCTOR_BASE_URL=https://ys-test.heyejk.com
334
- DOCTOR_PUBLIC_PATH=/physician-admin/
335
- DOCTOR_LOGIN_PATH=/physician-admin/login
336
- ```
337
-
338
- 也就是说,基础用例会访问:
339
-
340
- ```text
341
- https://ys-test.heyejk.com/physician-admin/login
342
- https://ys-test.heyejk.com/physician-admin/home
343
- https://ys-test.heyejk.com/physician-admin/serviceAgreement
344
- ```
345
-
346
- 如果要临时切换环境,可以在命令前覆盖变量:
347
-
348
- ```bash
349
- DOCTOR_BASE_URL=https://your-test-host npm run doctor:test
350
- ```
351
-
352
- 如果路径也不同,可以一起覆盖:
353
-
354
- ```bash
355
- DOCTOR_BASE_URL=https://your-test-host \
356
- DOCTOR_PUBLIC_PATH=/physician-admin/ \
357
- DOCTOR_LOGIN_PATH=/physician-admin/login \
358
- npm run doctor:test
167
+ projects.config.json
359
168
  ```
360
169
 
361
- ## 当前测试用例
362
-
363
- 文件位置:
170
+ 用例库缓存目录是:
364
171
 
365
172
  ```text
366
- cases/doctor-pc/test-env/doctor-master-basic.spec.ts
173
+ .case-repos/
367
174
  ```
368
175
 
369
- 当前包含 4 条用例:
176
+ `.case-repos/` 只是本地缓存,不需要提交。
370
177
 
371
- | 用例 ID | 验证内容 | 方式 |
372
- | --- | --- | --- |
373
- | `PW-TEST-001` | 登录页控件渲染:手机号、图形验证码、短信验证码、协议、登录按钮 | 直接访问登录路由 |
374
- | `PW-TEST-002` | 未登录访问 `/home` 会被路由守卫跳到 `/login` | 直接访问受保护路由 |
375
- | `PW-TEST-003` | `/serviceAgreement` 在白名单内,未登录可访问 | 直接访问白名单路由 |
376
- | `PW-TEST-004` | 登录表单校验:必填、手机号格式、未勾协议提示 | 真实输入和点击 |
178
+ ## 看结果
377
179
 
378
- 前 3 条主要验证路由和页面状态;第 4 条包含真实输入和点击。
379
-
380
- ## 如何看结果
381
-
382
- 命令行里看到类似结果表示通过:
180
+ 命令行看到类似结果表示通过:
383
181
 
384
182
  ```text
385
183
  4 passed
386
184
  ```
387
185
 
388
- 失败时,Playwright 会在这些目录生成证据:
186
+ 失败时会生成:
389
187
 
390
188
  ```text
189
+ test-results/summary.md
391
190
  test-results/
392
191
  playwright-report/
393
192
  ```
394
193
 
395
- 常用查看方式:
194
+ 其中 `test-results/summary.md` 是 Markdown 摘要,适合直接发给别人看。
195
+
196
+ 在源码仓库里可以打开报告:
396
197
 
397
198
  ```bash
398
199
  npm run report
399
200
  ```
400
201
 
401
- 失败报告里通常包含:
402
-
403
- - 失败截图
404
- - 操作视频
405
- - trace 回放
406
- - 报错堆栈
407
- - 失败前后的页面状态
408
-
409
- trace 可以帮助判断到底是页面没出来、定位器没找到、跳转不符合预期,还是网络/环境问题。
410
-
411
- ## 第一次运行常见问题
412
-
413
- ### 缺少 Chromium
414
-
415
- 报错类似:
416
-
417
- ```text
418
- Executable doesn't exist
419
- Please run the following command to download new browsers:
420
- npx playwright install
421
- ```
422
-
423
- 处理:
202
+ 通过 npm 安装使用时,可以在运行目录打开报告:
424
203
 
425
204
  ```bash
426
- npx playwright install chromium
205
+ npx playwright show-report playwright-report
427
206
  ```
428
207
 
429
- ### 测试环境访问慢
430
-
431
- 如果测试偶发超时,先用可视化模式看页面是否真的打开:
208
+ ## 常见问题
432
209
 
433
- ```bash
434
- npm run doctor:test:headed
435
- ```
210
+ ### 提示缺少浏览器
436
211
 
437
- 再打开报告看失败证据:
212
+ 执行:
438
213
 
439
214
  ```bash
440
- npm run report
215
+ playwright-ci install chromium
441
216
  ```
442
217
 
443
- ### 断言找到了多个元素
218
+ ### 拉取用例库失败
444
219
 
445
- Playwright 默认严格模式下,一个定位器只能匹配一个元素。比如 `验证码` 可能同时匹配 `图形验证码` 和 `验证码`。这种情况需要把定位器写得更精确,例如:
220
+ 先确认当前机器能访问内部 Git。需要账号密码时这样跑:
446
221
 
447
- ```ts
448
- page.getByPlaceholder('验证码', { exact: true })
222
+ ```bash
223
+ GIT_USERNAME=你的用户名 GIT_PASSWORD='你的密码或访问令牌' playwright-ci run doctor-pc
449
224
  ```
450
225
 
451
- ## 如何新增用例
452
-
453
- 医生端测试环境基础用例优先放在:
226
+ ### 当前目录缺少 projects.config.json
454
227
 
455
- ```text
456
- cases/doctor-pc/test-env/
457
- ```
458
-
459
- 建议命名:
228
+ 先执行:
460
229
 
461
- ```text
462
- 模块-场景-预期.spec.ts
230
+ ```bash
231
+ playwright-ci init
463
232
  ```
464
233
 
465
- 示例:
234
+ ### 本地用例库有未提交改动
466
235
 
467
- ```text
468
- login-form-validation.spec.ts
469
- home-route-guard.spec.ts
470
- ```
471
-
472
- 每条用例建议补充 `annotateCase`,方便报告里追踪用例 ID:
236
+ Runner 会停止自动 `pull`,避免覆盖你正在调试的用例。处理 `.case-repos/项目名` 里的 Git 状态后再运行。
473
237
 
474
- ```ts
475
- annotateCase(testInfo, {
476
- caseId: 'PW-TEST-005',
477
- evidenceId: 'TEST-some-business-evidence',
478
- priority: 'Smoke'
479
- });
480
- ```
238
+ ## 在源码仓库里开发
481
239
 
482
- 写用例时优先使用接近用户行为的定位方式:
240
+ 如果你是维护这个 runner 本身:
483
241
 
484
- ```ts
485
- page.getByRole('button', { name: '登录' })
486
- page.getByPlaceholder('手机号')
487
- page.getByText('荷叶医生')
242
+ ```bash
243
+ git clone https://git.int.ybm100.com/ykq/playwright-ci
244
+ cd playwright-ci
245
+ npm install
246
+ npx playwright install chromium
488
247
  ```
489
248
 
490
- 不要优先依赖样式类名、DOM 层级或 `nth-child`。如果页面没有稳定语义定位,再考虑让业务仓补 `data-testid`。
491
-
492
- ## 登录后用例怎么做
493
-
494
- 当前基础用例不需要登录。后续要测登录后页面,比如首页、问诊列表、处方 tab、开方、撤回处方,需要先满足至少一种方案:
495
-
496
- - 后端提供测试登录接口。
497
- - 测试环境提供固定验证码或验证码绕过能力。
498
- - 手工生成 `playwright/.auth/doctor.json` 登录态。
499
-
500
- 手工登录态方式参考:
249
+ 运行内置医生端命令:
501
250
 
502
251
  ```bash
503
- npx playwright codegen --save-storage=playwright/.auth/doctor.json https://ys-test.heyejk.com/physician-admin/login
252
+ npm run doctor:test
253
+ npm run doctor:test:headed
254
+ npm run doctor:test:debug
504
255
  ```
505
256
 
506
- 在打开的浏览器里完成登录后关闭窗口,后续已登录用例可复用该 storageState。
507
-
508
- 注意:`playwright/.auth/*.json` 包含登录态,已被 `.gitignore` 排除,不要提交。
509
-
510
- ## CI 或服务器运行
511
-
512
- 服务器不需要显示器,可以直接跑无头模式:
257
+ 检查代码:
513
258
 
514
259
  ```bash
515
- npm ci
516
- npx playwright install --with-deps chromium
517
- npm run doctor:test
260
+ npm run lint
261
+ npm run typecheck
518
262
  ```
519
263
 
520
- CI 建议归档:
264
+ ## 当前基础用例
521
265
 
522
- ```text
523
- playwright-report/
524
- test-results/
525
- ```
266
+ 医生端测试环境基础用例包括:
526
267
 
527
- 不要归档或提交:
268
+ | 用例 | 验证内容 |
269
+ | --- | --- |
270
+ | `PW-TEST-001` | 登录页控件是否渲染 |
271
+ | `PW-TEST-002` | 未登录访问 `/home` 是否跳回登录页 |
272
+ | `PW-TEST-003` | `/serviceAgreement` 是否允许未登录访问 |
273
+ | `PW-TEST-004` | 登录表单基础校验 |
528
274
 
529
- ```text
530
- playwright/.auth/*.json
531
- .env
532
- ```
275
+ ## 安全注意
533
276
 
534
- ## 项目结构
277
+ 不要提交这些内容:
535
278
 
536
279
  ```text
537
- cases/doctor-pc/ # 医生端业务用例
538
- src/fixtures/ # Playwright fixtures 和登录态工具
539
- src/pages/doctor-pc/ # Page Object
540
- src/flows/doctor-pc/ # 跨页面业务流程
541
- src/api/ # API 和测试数据客户端
542
- src/mocks/ # route / websocket mock
543
- docs/ # 运行、登录、测试数据、选择器和 CI 说明
544
- scripts/ # 命令封装脚本
280
+ .env
281
+ playwright/.auth/*.json
282
+ 账号密码
283
+ 访问令牌
284
+ npm token
545
285
  ```
546
286
 
547
- ## 维护原则
548
-
549
- - 先用少量稳定 Smoke 验证页面和路由,再扩展登录后 P0 链路。
550
- - 用例失败必须能留下截图、视频或 trace。
551
- - 不在测试工程里硬编码真实账号密码。
552
- - 不把 Playwright 依赖塞回医生端业务仓。
553
- - 涉及处方、问诊、药品、自动关诊等业务链路时,要先确认测试数据和登录态方案。
287
+ 登录后业务用例需要额外准备测试账号、验证码绕过方案,或 `playwright/.auth/doctor.json` 登录态文件。
@@ -3,14 +3,18 @@
3
3
 
4
4
  import { spawn } from 'node:child_process';
5
5
  import fs from 'node:fs';
6
+ import { createRequire } from 'node:module';
6
7
  import path from 'node:path';
7
8
  import { fileURLToPath } from 'node:url';
8
9
 
9
10
  const packageRootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
11
+ const require = createRequire(import.meta.url);
10
12
  const defaultConfigPath = path.join(packageRootDir, 'projects.config.json');
11
13
  const userConfigPath = process.env.PROJECTS_CONFIG_PATH
12
14
  ? path.resolve(process.env.PROJECTS_CONFIG_PATH)
13
15
  : path.resolve(process.cwd(), 'projects.config.json');
16
+ const packageJson = JSON.parse(fs.readFileSync(path.join(packageRootDir, 'package.json'), 'utf8'));
17
+ const playwrightCliPath = path.join(path.dirname(require.resolve('@playwright/test/package.json')), 'cli.js');
14
18
 
15
19
  const configActions = new Set(['list', 'show', 'add', 'update', 'delete', 'remove']);
16
20
 
@@ -22,11 +26,13 @@ function printHelp() {
22
26
  playwright-ci add [project]
23
27
  playwright-ci update <project>
24
28
  playwright-ci delete <project>
29
+ playwright-ci install [browser]
25
30
  playwright-ci run <project> [playwright args]
26
31
  playwright-ci <project> [playwright args]
27
32
 
28
33
  Examples:
29
34
  playwright-ci init
35
+ playwright-ci install chromium
30
36
  playwright-ci run doctor-pc --headed
31
37
  GIT_USERNAME=user GIT_PASSWORD=token playwright-ci run doctor-pc`);
32
38
  }
@@ -69,6 +75,23 @@ function runNodeScript(scriptName, args) {
69
75
  });
70
76
  }
71
77
 
78
+ function runPlaywright(args) {
79
+ const child = spawn(process.execPath, [playwrightCliPath, ...args], {
80
+ cwd: process.cwd(),
81
+ env: process.env,
82
+ stdio: 'inherit'
83
+ });
84
+
85
+ child.on('exit', (code, signal) => {
86
+ if (signal) {
87
+ process.kill(process.pid, signal);
88
+ return;
89
+ }
90
+
91
+ process.exit(code ?? 1);
92
+ });
93
+ }
94
+
72
95
  function main() {
73
96
  const [command = 'help', ...args] = process.argv.slice(2);
74
97
 
@@ -77,11 +100,21 @@ function main() {
77
100
  return;
78
101
  }
79
102
 
103
+ if (command === 'version' || command === '--version' || command === '-v') {
104
+ console.log(packageJson.version);
105
+ return;
106
+ }
107
+
80
108
  if (command === 'init') {
81
109
  initConfig();
82
110
  return;
83
111
  }
84
112
 
113
+ if (command === 'install') {
114
+ runPlaywright(['install', ...(args.length ? args : ['chromium'])]);
115
+ return;
116
+ }
117
+
85
118
  if (command === 'run' || command === 'project:test') {
86
119
  ensureConfigExists();
87
120
  runNodeScript('run-project.mjs', args);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ykq-playwright-ci",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Company-level Playwright E2E test project for doctor PC workflows.",
5
5
  "bin": {
6
6
  "playwright-ci": "bin/playwright-ci.mjs"
@@ -16,6 +16,7 @@
16
16
  ".nvmrc",
17
17
  "eslint.config.mjs",
18
18
  "package.json",
19
+ "playwright.config.mjs",
19
20
  "playwright.config.ts",
20
21
  "projects.config.json",
21
22
  "README.md",
@@ -0,0 +1,62 @@
1
+ /* global process */
2
+
3
+ import { defineConfig, devices } from '@playwright/test';
4
+ import dotenv from 'dotenv';
5
+ import path from 'node:path';
6
+
7
+ dotenv.config();
8
+
9
+ const resultsDir = process.env.PLAYWRIGHT_CI_RESULTS_DIR || 'test-results';
10
+ const reportDir = process.env.PLAYWRIGHT_CI_REPORT_DIR || 'playwright-report';
11
+ const testDir = process.env.PLAYWRIGHT_CI_TEST_DIR || '.';
12
+
13
+ function optional(name) {
14
+ const value = process.env[name]?.trim();
15
+ return value ? value : undefined;
16
+ }
17
+
18
+ function trimTrailingSlash(value) {
19
+ return value?.replace(/\/+$/, '');
20
+ }
21
+
22
+ export default defineConfig({
23
+ testDir,
24
+ testMatch: ['**/*.spec.ts', '**/*.setup.ts'],
25
+ outputDir: resultsDir,
26
+ timeout: 60_000,
27
+ expect: {
28
+ timeout: 10_000
29
+ },
30
+ fullyParallel: false,
31
+ forbidOnly: Boolean(process.env.CI),
32
+ retries: process.env.CI ? 2 : 0,
33
+ workers: process.env.CI ? 2 : undefined,
34
+ reporter: [
35
+ ['list'],
36
+ ['html', { outputFolder: reportDir, open: 'never' }],
37
+ ['junit', { outputFile: path.join(resultsDir, 'junit.xml') }],
38
+ ['json', { outputFile: path.join(resultsDir, 'results.json') }]
39
+ ],
40
+ use: {
41
+ baseURL: trimTrailingSlash(optional('DOCTOR_BASE_URL')),
42
+ actionTimeout: 15_000,
43
+ navigationTimeout: 30_000,
44
+ trace: 'retain-on-failure',
45
+ screenshot: 'only-on-failure',
46
+ video: 'retain-on-failure'
47
+ },
48
+ projects: [
49
+ {
50
+ name: 'setup',
51
+ testMatch: /.*\.setup\.ts/
52
+ },
53
+ {
54
+ name: 'chromium',
55
+ dependencies: ['setup'],
56
+ testIgnore: /.*\.setup\.ts/,
57
+ use: {
58
+ ...devices['Desktop Chrome']
59
+ }
60
+ }
61
+ ]
62
+ });
@@ -1,10 +1,15 @@
1
1
  import { defineConfig, devices } from '@playwright/test';
2
+ import path from 'node:path';
2
3
  import { appEnv } from './src/utils/env';
3
4
 
5
+ const resultsDir = process.env.PLAYWRIGHT_CI_RESULTS_DIR || 'test-results';
6
+ const reportDir = process.env.PLAYWRIGHT_CI_REPORT_DIR || 'playwright-report';
7
+ const testDir = process.env.PLAYWRIGHT_CI_TEST_DIR || '.';
8
+
4
9
  export default defineConfig({
5
- testDir: '.',
6
- testMatch: ['cases/**/*.spec.ts', 'cases/**/*.setup.ts'],
7
- outputDir: 'test-results',
10
+ testDir,
11
+ testMatch: ['**/*.spec.ts', '**/*.setup.ts'],
12
+ outputDir: resultsDir,
8
13
  timeout: 60_000,
9
14
  expect: {
10
15
  timeout: 10_000
@@ -15,9 +20,9 @@ export default defineConfig({
15
20
  workers: process.env.CI ? 2 : undefined,
16
21
  reporter: [
17
22
  ['list'],
18
- ['html', { outputFolder: 'playwright-report', open: 'never' }],
19
- ['junit', { outputFile: 'test-results/junit.xml' }],
20
- ['json', { outputFile: 'test-results/results.json' }]
23
+ ['html', { outputFolder: reportDir, open: 'never' }],
24
+ ['junit', { outputFile: path.join(resultsDir, 'junit.xml') }],
25
+ ['json', { outputFile: path.join(resultsDir, 'results.json') }]
21
26
  ],
22
27
  use: {
23
28
  baseURL: appEnv.doctorBaseURL,
@@ -13,9 +13,11 @@ const configPath = process.env.PROJECTS_CONFIG_PATH
13
13
  ? path.resolve(process.env.PROJECTS_CONFIG_PATH)
14
14
  : path.join(rootDir, 'projects.config.json');
15
15
  const workspaceDir = process.env.PROJECTS_CONFIG_PATH ? path.dirname(configPath) : rootDir;
16
- const playwrightConfigPath = path.join(rootDir, 'playwright.config.ts');
16
+ const playwrightConfigPath = path.join(rootDir, 'playwright.config.mjs');
17
17
  const playwrightCliPath = path.join(path.dirname(require.resolve('@playwright/test/package.json')), 'cli.js');
18
18
  const knownModeArgs = new Set(['--headed', '--debug', '--ui']);
19
+ const resultsDir = process.env.PLAYWRIGHT_CI_RESULTS_DIR || path.join(workspaceDir, 'test-results');
20
+ const reportDir = process.env.PLAYWRIGHT_CI_REPORT_DIR || path.join(workspaceDir, 'playwright-report');
19
21
  let gitAskpassPath = '';
20
22
 
21
23
  function readConfig() {
@@ -201,6 +203,112 @@ function buildPlaywrightArgs(caseDir, project, passthroughArgs) {
201
203
  return args;
202
204
  }
203
205
 
206
+ function formatDuration(durationMs = 0) {
207
+ if (durationMs < 1000) {
208
+ return `${Math.round(durationMs)}ms`;
209
+ }
210
+
211
+ return `${(durationMs / 1000).toFixed(1)}s`;
212
+ }
213
+
214
+ function escapeMarkdown(value = '') {
215
+ return String(value).replace(/\|/g, '\\|').replace(/\r?\n/g, ' ');
216
+ }
217
+
218
+ function collectSpecs(suites = [], rows = []) {
219
+ for (const suite of suites) {
220
+ for (const spec of suite.specs || []) {
221
+ for (const test of spec.tests || []) {
222
+ const result = test.results?.[test.results.length - 1] || {};
223
+ const annotations = [...(test.annotations || []), ...(result.annotations || [])];
224
+ const caseId = annotations.find((item) => item.type === 'case-id')?.description || '-';
225
+ const priority = annotations.find((item) => item.type === 'priority')?.description || '-';
226
+ const error = result.errors?.[0]?.message || '';
227
+
228
+ rows.push({
229
+ status: result.status || test.status || (spec.ok ? 'passed' : 'failed'),
230
+ caseId,
231
+ priority,
232
+ title: spec.title,
233
+ projectName: test.projectName || '-',
234
+ duration: result.duration || 0,
235
+ file: spec.file || suite.file || '-',
236
+ line: spec.line || 0,
237
+ error
238
+ });
239
+ }
240
+ }
241
+
242
+ collectSpecs(suite.suites || [], rows);
243
+ }
244
+
245
+ return rows;
246
+ }
247
+
248
+ function statusText(status) {
249
+ if (status === 'passed') return '通过';
250
+ if (status === 'skipped') return '跳过';
251
+ if (status === 'timedOut') return '超时';
252
+ if (status === 'failed' || status === 'unexpected') return '失败';
253
+ return status || '-';
254
+ }
255
+
256
+ function writeMarkdownSummary(projectName, caseDir, exitCode) {
257
+ const jsonPath = path.join(resultsDir, 'results.json');
258
+ if (!fs.existsSync(jsonPath)) {
259
+ console.warn(`未找到 Playwright JSON 结果,已跳过 Markdown 摘要:${jsonPath}`);
260
+ return;
261
+ }
262
+
263
+ const result = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
264
+ const stats = result.stats || {};
265
+ const rows = collectSpecs(result.suites || []);
266
+ const summaryPath = path.join(resultsDir, 'summary.md');
267
+ const failedRows = rows.filter((row) => !['passed', 'skipped'].includes(row.status));
268
+ const status = exitCode === 0 && failedRows.length === 0 ? '通过' : '失败';
269
+ const reportPath = path.relative(resultsDir, path.join(reportDir, 'index.html')) || 'index.html';
270
+
271
+ const lines = [
272
+ '# Playwright 测试结果',
273
+ '',
274
+ `- 项目:${projectName}`,
275
+ `- 状态:${status}`,
276
+ `- 开始时间:${stats.startTime || '-'}`,
277
+ `- 总耗时:${formatDuration(stats.duration || 0)}`,
278
+ `- 用例数:${rows.length}`,
279
+ `- 通过:${stats.expected ?? 0}`,
280
+ `- 失败:${stats.unexpected ?? 0}`,
281
+ `- 跳过:${stats.skipped ?? 0}`,
282
+ `- 不稳定:${stats.flaky ?? 0}`,
283
+ `- 用例目录:${path.relative(workspaceDir, caseDir) || caseDir}`,
284
+ `- HTML 报告:${reportPath}`,
285
+ '',
286
+ '## 用例明细',
287
+ '',
288
+ '| 状态 | 用例 ID | 优先级 | 用例 | 浏览器 | 耗时 | 文件 |',
289
+ '| --- | --- | --- | --- | --- | --- | --- |'
290
+ ];
291
+
292
+ for (const row of rows) {
293
+ const file = row.line ? `${row.file}:${row.line}` : row.file;
294
+ lines.push(
295
+ `| ${statusText(row.status)} | ${escapeMarkdown(row.caseId)} | ${escapeMarkdown(row.priority)} | ${escapeMarkdown(row.title)} | ${escapeMarkdown(row.projectName)} | ${formatDuration(row.duration)} | ${escapeMarkdown(file)} |`
296
+ );
297
+ }
298
+
299
+ if (failedRows.length) {
300
+ lines.push('', '## 失败信息', '');
301
+ for (const row of failedRows) {
302
+ lines.push(`### ${row.caseId} ${row.title}`, '');
303
+ lines.push(row.error ? `\`\`\`text\n${row.error.trim()}\n\`\`\`` : '无错误详情。', '');
304
+ }
305
+ }
306
+
307
+ fs.mkdirSync(resultsDir, { recursive: true });
308
+ fs.writeFileSync(summaryPath, `${lines.join('\n')}\n`);
309
+ console.log(`Markdown 测试摘要已生成:${summaryPath}`);
310
+ }
311
+
204
312
  function main() {
205
313
  const config = readConfig();
206
314
  const { projectName, passthroughArgs, skipPull } = parseArgs(process.argv.slice(2));
@@ -227,7 +335,10 @@ function main() {
227
335
  cwd: workspaceDir,
228
336
  env: {
229
337
  ...(project.env || {}),
230
- ...process.env
338
+ ...process.env,
339
+ PLAYWRIGHT_CI_RESULTS_DIR: resultsDir,
340
+ PLAYWRIGHT_CI_REPORT_DIR: reportDir,
341
+ PLAYWRIGHT_CI_TEST_DIR: workspaceDir
231
342
  },
232
343
  stdio: 'inherit'
233
344
  }
@@ -239,7 +350,13 @@ function main() {
239
350
  return;
240
351
  }
241
352
 
242
- process.exit(code ?? 1);
353
+ const exitCode = code ?? 1;
354
+ try {
355
+ writeMarkdownSummary(projectName, caseDir, exitCode);
356
+ } catch (error) {
357
+ console.warn(`Markdown 测试摘要生成失败:${error instanceof Error ? error.message : error}`);
358
+ }
359
+ process.exit(exitCode);
243
360
  });
244
361
  }
245
362