ykq-playwright-ci 0.1.1 → 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 +3 -0
- package/package.json +2 -1
- package/playwright.config.mjs +62 -0
- package/playwright.config.ts +11 -6
- package/scripts/run-project.mjs +120 -3
package/README.md
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ykq-playwright-ci",
|
|
3
|
-
"version": "0.1.
|
|
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
|
+
});
|
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
|
@@ -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.
|
|
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
|
-
|
|
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
|
|