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 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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ykq-playwright-ci",
3
- "version": "0.1.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
+ });
@@ -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