ykq-playwright-ci 0.1.1 → 0.1.3
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
|
@@ -186,10 +186,13 @@ projects.config.json
|
|
|
186
186
|
失败时会生成:
|
|
187
187
|
|
|
188
188
|
```text
|
|
189
|
+
test-results/summary.md
|
|
189
190
|
test-results/
|
|
190
191
|
playwright-report/
|
|
191
192
|
```
|
|
192
193
|
|
|
194
|
+
其中 `test-results/summary.md` 是 Markdown 摘要,适合直接发给别人看。
|
|
195
|
+
|
|
193
196
|
在源码仓库里可以打开报告:
|
|
194
197
|
|
|
195
198
|
```bash
|
|
@@ -268,6 +271,7 @@ npm run typecheck
|
|
|
268
271
|
| `PW-TEST-002` | 未登录访问 `/home` 是否跳回登录页 |
|
|
269
272
|
| `PW-TEST-003` | `/serviceAgreement` 是否允许未登录访问 |
|
|
270
273
|
| `PW-TEST-004` | 登录表单基础校验 |
|
|
274
|
+
| `PW-TEST-005` | 使用测试账号登录成功 |
|
|
271
275
|
|
|
272
276
|
## 安全注意
|
|
273
277
|
|
|
@@ -1,11 +1,25 @@
|
|
|
1
|
+
/* global process */
|
|
2
|
+
|
|
1
3
|
import { test, expect } from '../../../src/fixtures/base.fixture';
|
|
2
4
|
import { annotateCase } from '../../../src/utils/evidence';
|
|
3
5
|
import { appEnv, doctorURL, hasDoctorBaseURL } from '../../../src/utils/env';
|
|
4
6
|
|
|
7
|
+
const doctorUsername = process.env.DOCTOR_USERNAME?.trim() || '';
|
|
8
|
+
const doctorLoginCode = (
|
|
9
|
+
process.env.DOCTOR_LOGIN_CODE ||
|
|
10
|
+
process.env.DOCTOR_OTP ||
|
|
11
|
+
process.env.DOCTOR_PASSWORD ||
|
|
12
|
+
''
|
|
13
|
+
).trim();
|
|
14
|
+
|
|
5
15
|
async function gotoDoctor(path: string, page: import('@playwright/test').Page): Promise<void> {
|
|
6
16
|
await page.goto(doctorURL(path), { waitUntil: 'domcontentloaded' });
|
|
7
17
|
}
|
|
8
18
|
|
|
19
|
+
function hasDoctorLoginCredentials(): boolean {
|
|
20
|
+
return Boolean(doctorUsername && doctorLoginCode);
|
|
21
|
+
}
|
|
22
|
+
|
|
9
23
|
test.describe('doctor-master test environment basics', () => {
|
|
10
24
|
test.beforeEach(async () => {
|
|
11
25
|
test.skip(!hasDoctorBaseURL(), 'DOCTOR_BASE_URL is required for doctor-master test environment tests.');
|
|
@@ -86,4 +100,40 @@ test.describe('doctor-master test environment basics', () => {
|
|
|
86
100
|
await page.locator('.submit-btn', { hasText: '登录' }).click();
|
|
87
101
|
await expect(page.getByText('请阅读并勾选《荷叶健康用户服务协议》')).toBeVisible();
|
|
88
102
|
});
|
|
103
|
+
|
|
104
|
+
test('PW-TEST-005 @smoke doctor can login with configured test account', async ({
|
|
105
|
+
page
|
|
106
|
+
}, testInfo) => {
|
|
107
|
+
test.skip(
|
|
108
|
+
!hasDoctorLoginCredentials(),
|
|
109
|
+
'DOCTOR_USERNAME and DOCTOR_LOGIN_CODE/DOCTOR_OTP/DOCTOR_PASSWORD are required.'
|
|
110
|
+
);
|
|
111
|
+
annotateCase(testInfo, {
|
|
112
|
+
caseId: 'PW-TEST-005',
|
|
113
|
+
evidenceId: 'TEST-doctor-login-success',
|
|
114
|
+
priority: 'Smoke'
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
await gotoDoctor(appEnv.doctorLoginPath, page);
|
|
118
|
+
|
|
119
|
+
const loginResponsePromise = page.waitForResponse(
|
|
120
|
+
(response) =>
|
|
121
|
+
response.url().includes('/physician/account/loginPhysician') &&
|
|
122
|
+
response.request().method() === 'POST'
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
await page.getByPlaceholder('手机号').fill(doctorUsername);
|
|
126
|
+
await page.getByPlaceholder('图形验证码').fill('playwright');
|
|
127
|
+
await page.getByPlaceholder('验证码', { exact: true }).fill(doctorLoginCode);
|
|
128
|
+
await page.locator('.el-checkbox', { hasText: '我已阅读并同意' }).click();
|
|
129
|
+
await expect(page.locator('.submit-btn', { hasText: '登录' })).not.toHaveClass(/gruyBtn/);
|
|
130
|
+
await page.locator('.submit-btn', { hasText: '登录' }).click();
|
|
131
|
+
|
|
132
|
+
const loginResponse = await loginResponsePromise;
|
|
133
|
+
const loginBody = await loginResponse.json();
|
|
134
|
+
expect(loginResponse.ok()).toBeTruthy();
|
|
135
|
+
expect(loginBody?.data?.accountId, loginBody?.msg || '登录接口未返回 accountId').toBeTruthy();
|
|
136
|
+
|
|
137
|
+
await expect(page).toHaveURL(/\/physician-admin\/home(?:\?|#|$)/, { timeout: 15_000 });
|
|
138
|
+
});
|
|
89
139
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ykq-playwright-ci",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
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
|
+
});
|
package/playwright.config.ts
CHANGED
|
@@ -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: ['
|
|
7
|
-
outputDir:
|
|
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:
|
|
19
|
-
['junit', { outputFile: '
|
|
20
|
-
['json', { outputFile: '
|
|
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,
|
package/scripts/run-project.mjs
CHANGED
|
@@ -2,22 +2,41 @@
|
|
|
2
2
|
|
|
3
3
|
import { spawn, spawnSync } from 'node:child_process';
|
|
4
4
|
import fs from 'node:fs';
|
|
5
|
-
import { createRequire } from 'node:module';
|
|
6
5
|
import os from 'node:os';
|
|
7
6
|
import path from 'node:path';
|
|
8
7
|
import { fileURLToPath } from 'node:url';
|
|
9
8
|
|
|
10
9
|
const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
11
|
-
const require = createRequire(import.meta.url);
|
|
12
10
|
const configPath = process.env.PROJECTS_CONFIG_PATH
|
|
13
11
|
? path.resolve(process.env.PROJECTS_CONFIG_PATH)
|
|
14
12
|
: path.join(rootDir, 'projects.config.json');
|
|
15
13
|
const workspaceDir = process.env.PROJECTS_CONFIG_PATH ? path.dirname(configPath) : rootDir;
|
|
16
|
-
const playwrightConfigPath = path.join(rootDir, 'playwright.config.
|
|
17
|
-
const
|
|
14
|
+
const playwrightConfigPath = path.join(rootDir, 'playwright.config.mjs');
|
|
15
|
+
const packageNodeModulesDir = resolveNodeModulesDir(rootDir);
|
|
16
|
+
const playwrightCliPath = path.join(packageNodeModulesDir, '@playwright', 'test', 'cli.js');
|
|
18
17
|
const knownModeArgs = new Set(['--headed', '--debug', '--ui']);
|
|
18
|
+
const resultsDir = process.env.PLAYWRIGHT_CI_RESULTS_DIR || path.join(workspaceDir, 'test-results');
|
|
19
|
+
const reportDir = process.env.PLAYWRIGHT_CI_REPORT_DIR || path.join(workspaceDir, 'playwright-report');
|
|
19
20
|
let gitAskpassPath = '';
|
|
20
21
|
|
|
22
|
+
function resolveNodeModulesDir(startDir) {
|
|
23
|
+
let currentDir = startDir;
|
|
24
|
+
|
|
25
|
+
while (true) {
|
|
26
|
+
const nodeModulesDir = path.join(currentDir, 'node_modules');
|
|
27
|
+
const playwrightCli = path.join(nodeModulesDir, '@playwright', 'test', 'cli.js');
|
|
28
|
+
if (fs.existsSync(playwrightCli)) {
|
|
29
|
+
return nodeModulesDir;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const parentDir = path.dirname(currentDir);
|
|
33
|
+
if (parentDir === currentDir) {
|
|
34
|
+
throw new Error(`Cannot find @playwright/test from ${startDir}`);
|
|
35
|
+
}
|
|
36
|
+
currentDir = parentDir;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
21
40
|
function readConfig() {
|
|
22
41
|
return JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
23
42
|
}
|
|
@@ -201,6 +220,125 @@ function buildPlaywrightArgs(caseDir, project, passthroughArgs) {
|
|
|
201
220
|
return args;
|
|
202
221
|
}
|
|
203
222
|
|
|
223
|
+
function createPlaywrightEnv(projectEnv) {
|
|
224
|
+
const nodePath = [packageNodeModulesDir, process.env.NODE_PATH].filter(Boolean).join(path.delimiter);
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
...projectEnv,
|
|
228
|
+
...process.env,
|
|
229
|
+
NODE_PATH: nodePath,
|
|
230
|
+
PLAYWRIGHT_CI_RESULTS_DIR: resultsDir,
|
|
231
|
+
PLAYWRIGHT_CI_REPORT_DIR: reportDir,
|
|
232
|
+
PLAYWRIGHT_CI_TEST_DIR: workspaceDir
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function formatDuration(durationMs = 0) {
|
|
237
|
+
if (durationMs < 1000) {
|
|
238
|
+
return `${Math.round(durationMs)}ms`;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return `${(durationMs / 1000).toFixed(1)}s`;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function escapeMarkdown(value = '') {
|
|
245
|
+
return String(value).replace(/\|/g, '\\|').replace(/\r?\n/g, ' ');
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function collectSpecs(suites = [], rows = []) {
|
|
249
|
+
for (const suite of suites) {
|
|
250
|
+
for (const spec of suite.specs || []) {
|
|
251
|
+
for (const test of spec.tests || []) {
|
|
252
|
+
const result = test.results?.[test.results.length - 1] || {};
|
|
253
|
+
const annotations = [...(test.annotations || []), ...(result.annotations || [])];
|
|
254
|
+
const caseId = annotations.find((item) => item.type === 'case-id')?.description || '-';
|
|
255
|
+
const priority = annotations.find((item) => item.type === 'priority')?.description || '-';
|
|
256
|
+
const error = result.errors?.[0]?.message || '';
|
|
257
|
+
|
|
258
|
+
rows.push({
|
|
259
|
+
status: result.status || test.status || (spec.ok ? 'passed' : 'failed'),
|
|
260
|
+
caseId,
|
|
261
|
+
priority,
|
|
262
|
+
title: spec.title,
|
|
263
|
+
projectName: test.projectName || '-',
|
|
264
|
+
duration: result.duration || 0,
|
|
265
|
+
file: spec.file || suite.file || '-',
|
|
266
|
+
line: spec.line || 0,
|
|
267
|
+
error
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
collectSpecs(suite.suites || [], rows);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return rows;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function statusText(status) {
|
|
279
|
+
if (status === 'passed') return '通过';
|
|
280
|
+
if (status === 'skipped') return '跳过';
|
|
281
|
+
if (status === 'timedOut') return '超时';
|
|
282
|
+
if (status === 'failed' || status === 'unexpected') return '失败';
|
|
283
|
+
return status || '-';
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function writeMarkdownSummary(projectName, caseDir, exitCode) {
|
|
287
|
+
const jsonPath = path.join(resultsDir, 'results.json');
|
|
288
|
+
if (!fs.existsSync(jsonPath)) {
|
|
289
|
+
console.warn(`未找到 Playwright JSON 结果,已跳过 Markdown 摘要:${jsonPath}`);
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const result = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
|
|
294
|
+
const stats = result.stats || {};
|
|
295
|
+
const rows = collectSpecs(result.suites || []);
|
|
296
|
+
const summaryPath = path.join(resultsDir, 'summary.md');
|
|
297
|
+
const failedRows = rows.filter((row) => !['passed', 'skipped'].includes(row.status));
|
|
298
|
+
const status = exitCode === 0 && failedRows.length === 0 ? '通过' : '失败';
|
|
299
|
+
const reportPath = path.relative(resultsDir, path.join(reportDir, 'index.html')) || 'index.html';
|
|
300
|
+
|
|
301
|
+
const lines = [
|
|
302
|
+
'# Playwright 测试结果',
|
|
303
|
+
'',
|
|
304
|
+
`- 项目:${projectName}`,
|
|
305
|
+
`- 状态:${status}`,
|
|
306
|
+
`- 开始时间:${stats.startTime || '-'}`,
|
|
307
|
+
`- 总耗时:${formatDuration(stats.duration || 0)}`,
|
|
308
|
+
`- 用例数:${rows.length}`,
|
|
309
|
+
`- 通过:${stats.expected ?? 0}`,
|
|
310
|
+
`- 失败:${stats.unexpected ?? 0}`,
|
|
311
|
+
`- 跳过:${stats.skipped ?? 0}`,
|
|
312
|
+
`- 不稳定:${stats.flaky ?? 0}`,
|
|
313
|
+
`- 用例目录:${path.relative(workspaceDir, caseDir) || caseDir}`,
|
|
314
|
+
`- HTML 报告:${reportPath}`,
|
|
315
|
+
'',
|
|
316
|
+
'## 用例明细',
|
|
317
|
+
'',
|
|
318
|
+
'| 状态 | 用例 ID | 优先级 | 用例 | 浏览器 | 耗时 | 文件 |',
|
|
319
|
+
'| --- | --- | --- | --- | --- | --- | --- |'
|
|
320
|
+
];
|
|
321
|
+
|
|
322
|
+
for (const row of rows) {
|
|
323
|
+
const file = row.line ? `${row.file}:${row.line}` : row.file;
|
|
324
|
+
lines.push(
|
|
325
|
+
`| ${statusText(row.status)} | ${escapeMarkdown(row.caseId)} | ${escapeMarkdown(row.priority)} | ${escapeMarkdown(row.title)} | ${escapeMarkdown(row.projectName)} | ${formatDuration(row.duration)} | ${escapeMarkdown(file)} |`
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (failedRows.length) {
|
|
330
|
+
lines.push('', '## 失败信息', '');
|
|
331
|
+
for (const row of failedRows) {
|
|
332
|
+
lines.push(`### ${row.caseId} ${row.title}`, '');
|
|
333
|
+
lines.push(row.error ? `\`\`\`text\n${row.error.trim()}\n\`\`\`` : '无错误详情。', '');
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
fs.mkdirSync(resultsDir, { recursive: true });
|
|
338
|
+
fs.writeFileSync(summaryPath, `${lines.join('\n')}\n`);
|
|
339
|
+
console.log(`Markdown 测试摘要已生成:${summaryPath}`);
|
|
340
|
+
}
|
|
341
|
+
|
|
204
342
|
function main() {
|
|
205
343
|
const config = readConfig();
|
|
206
344
|
const { projectName, passthroughArgs, skipPull } = parseArgs(process.argv.slice(2));
|
|
@@ -225,10 +363,7 @@ function main() {
|
|
|
225
363
|
[playwrightCliPath, ...buildPlaywrightArgs(caseDir, project, passthroughArgs)],
|
|
226
364
|
{
|
|
227
365
|
cwd: workspaceDir,
|
|
228
|
-
env: {
|
|
229
|
-
...(project.env || {}),
|
|
230
|
-
...process.env
|
|
231
|
-
},
|
|
366
|
+
env: createPlaywrightEnv(project.env || {}),
|
|
232
367
|
stdio: 'inherit'
|
|
233
368
|
}
|
|
234
369
|
);
|
|
@@ -239,7 +374,13 @@ function main() {
|
|
|
239
374
|
return;
|
|
240
375
|
}
|
|
241
376
|
|
|
242
|
-
|
|
377
|
+
const exitCode = code ?? 1;
|
|
378
|
+
try {
|
|
379
|
+
writeMarkdownSummary(projectName, caseDir, exitCode);
|
|
380
|
+
} catch (error) {
|
|
381
|
+
console.warn(`Markdown 测试摘要生成失败:${error instanceof Error ? error.message : error}`);
|
|
382
|
+
}
|
|
383
|
+
process.exit(exitCode);
|
|
243
384
|
});
|
|
244
385
|
}
|
|
245
386
|
|