testomatio-reporter-cli 2.19.0 → 2.21.0

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.
@@ -0,0 +1,151 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import pc from 'picocolors';
4
+
5
+ const LAUNCHER_PREFIXES = ['npx', 'npm', 'yarn', 'pnpm', 'bun', 'node', 'bunx'];
6
+
7
+ const PLACEHOLDER_PHRASE_RE = /^<?-*(?:run|your|test)\s+(?:[\w-]+\s+)?(?:tests?|command|runner)>?$/i;
8
+
9
+ const RUNNERS = [
10
+ {
11
+ name: 'Playwright',
12
+ configs: ['playwright.config.js', 'playwright.config.ts', 'playwright.config.mjs', 'playwright.config.cjs'],
13
+ dependencies: ['@playwright/test'],
14
+ command: 'npx playwright test',
15
+ },
16
+ {
17
+ name: 'CodeceptJS',
18
+ configs: ['codecept.conf.js', 'codecept.conf.ts'],
19
+ dependencies: ['codeceptjs'],
20
+ command: 'npx codeceptjs run',
21
+ },
22
+ {
23
+ name: 'WebdriverIO',
24
+ configs: ['wdio.conf.js', 'wdio.conf.ts'],
25
+ dependencies: ['@wdio/cli'],
26
+ dependencyPrefix: '@wdio/',
27
+ command: 'npx wdio run ./wdio.conf.js',
28
+ },
29
+ {
30
+ name: 'Cypress',
31
+ configs: ['cypress.config.js', 'cypress.config.ts'],
32
+ dependencies: ['cypress'],
33
+ command: 'npx cypress run',
34
+ },
35
+ {
36
+ name: 'Jest',
37
+ configs: ['jest.config.js', 'jest.config.ts', 'jest.config.mjs', 'jest.config.cjs', 'jest.config.json'],
38
+ dependencies: ['jest'],
39
+ command: 'npx jest',
40
+ },
41
+ {
42
+ name: 'Vitest',
43
+ configs: ['vitest.config.js', 'vitest.config.ts', 'vitest.config.mjs'],
44
+ dependencies: ['vitest'],
45
+ command: 'npx vitest run',
46
+ },
47
+ {
48
+ name: 'Mocha',
49
+ configs: ['.mocharc.js', '.mocharc.cjs', '.mocharc.json', '.mocharc.yml', '.mocharc.yaml'],
50
+ dependencies: ['mocha'],
51
+ command: 'npx mocha',
52
+ },
53
+ {
54
+ name: 'Cucumber',
55
+ configs: ['cucumber.js', 'cucumber.cjs'],
56
+ dependencies: ['@cucumber/cucumber'],
57
+ command: 'npx cucumber-js',
58
+ },
59
+ ];
60
+
61
+ /**
62
+ * @param {string} command
63
+ * @returns {boolean}
64
+ */
65
+ function isPlaceholderCommand(command) {
66
+ const cmd = (command || '').trim();
67
+ if (!cmd) return false;
68
+
69
+ if (cmd.includes('<') || cmd.includes('>')) return true;
70
+
71
+ const firstToken = cmd.split(/\s+/)[0].toLowerCase();
72
+ if (LAUNCHER_PREFIXES.includes(firstToken)) return false;
73
+
74
+ return PLACEHOLDER_PHRASE_RE.test(cmd);
75
+ }
76
+
77
+ /**
78
+ * @param {string} cwd
79
+ * @returns {Record<string, string>}
80
+ */
81
+ function readPackageDependencies(cwd) {
82
+ try {
83
+ const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'));
84
+ return { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
85
+ } catch {
86
+ return {};
87
+ }
88
+ }
89
+
90
+ /**
91
+ * @param {string} [cwd]
92
+ * @returns {{ name: string, command: string } | null}
93
+ */
94
+ function detectRunner(cwd = process.cwd()) {
95
+ const dependencies = readPackageDependencies(cwd);
96
+
97
+ for (const runner of RUNNERS) {
98
+ const config = runner.configs.find(file => fs.existsSync(path.join(cwd, file)));
99
+
100
+ const hasDependency = runner.dependencies?.some(dep => dependencies[dep])
101
+ || (runner.dependencyPrefix && Object.keys(dependencies).some(dep => dep.startsWith(runner.dependencyPrefix)));
102
+
103
+ if (!config && !hasDependency) continue;
104
+
105
+ let command = runner.command;
106
+ if (runner.name === 'WebdriverIO') {
107
+ command = `npx wdio run ./${config || 'wdio.conf.js'}`;
108
+ }
109
+
110
+ return { name: runner.name, command };
111
+ }
112
+
113
+ return null;
114
+ }
115
+
116
+ /**
117
+ * @param {string} [cwd]
118
+ * @returns {string}
119
+ */
120
+ function runnerHint(cwd = process.cwd()) {
121
+ const runner = detectRunner(cwd);
122
+ if (runner) {
123
+ return `💡 Looks like this project uses ${runner.name}. Try: ${pc.green(runner.command)}`;
124
+ }
125
+ return `💡 Replace the placeholder with your test runner command, e.g. ${pc.green('npx playwright test')}`;
126
+ }
127
+
128
+ /**
129
+ * @param {string} command
130
+ * @returns {string[]}
131
+ */
132
+ function buildFilterWarning(command) {
133
+ return [
134
+ '⚠️ The runner exited with a non-zero code while a Testomat.io filter was applied.',
135
+ 'Your command was modified to run only tests matching Testomat IDs:',
136
+ ` ${pc.bold(command)}`,
137
+ 'This usually means no local tests matched the filter:',
138
+ ' - tests are not annotated with the matching @Txxxxxx IDs in their titles',
139
+ ' - the filter selected tests from another repository or branch',
140
+ ' - the test runner does not support the appended --grep / --testNamePattern option',
141
+ '💡 Hint: run the printed command without the appended filter option to see all your tests,',
142
+ 'then annotate them with Testomat IDs.',
143
+ ];
144
+ }
145
+
146
+ export {
147
+ isPlaceholderCommand,
148
+ detectRunner,
149
+ runnerHint,
150
+ buildFilterWarning,
151
+ };
@@ -366,17 +366,18 @@ function formatRunOutput(store, format) {
366
366
 
367
367
  /**
368
368
  * Format the runs fetched via the `fetch` command for machine-readable output.
369
- * `json` prints the full array of run objects; `id` prints one run id per line;
369
+ * `json` prints the full array of run objects (a single object for one run); `id` prints one run id per line;
370
370
  * any other format prints a human-readable multi-line summary per run.
371
371
  *
372
- * @param {{data?: Array}} body - Parsed response body from GET /api/v2/:project/runs.
372
+ * @param {{data?: Array|Object}} body - Parsed response body from GET /api/v2/:project/runs[/:id].
373
373
  * @param {string} [format] - Value of the CLI `--format` option.
374
374
  * @returns {string}
375
375
  */
376
376
  function formatFetchRunsOutput(body, format) {
377
- const runs = body?.data || [];
377
+ const data = body?.data || [];
378
+ const runs = [data].flat();
378
379
 
379
- if (format === 'json') return JSON.stringify(runs, null, 2);
380
+ if (format === 'json') return JSON.stringify(data, null, 2);
380
381
 
381
382
  if (format === 'id') return runs.map(run => run.id).filter(Boolean).join('\n');
382
383
 
@@ -384,6 +385,7 @@ function formatFetchRunsOutput(body, format) {
384
385
  .map(run => {
385
386
  const lines = [`* ID: ${run.id}`];
386
387
  if (run.title) lines.push(` title: ${run.title}`);
388
+ if (run.rungroup_id) lines.push(` rungroup: ${run.rungroup_id}`);
387
389
  if (run.launched_at) lines.push(` started at: ${run.launched_at}`);
388
390
  if (run.finished_at) lines.push(` finished at: ${run.finished_at}`);
389
391
  if (run.passed_count != null) lines.push(` passed: ${run.passed_count} tests`);
@@ -397,6 +399,44 @@ function formatFetchRunsOutput(body, format) {
397
399
  .join('\n');
398
400
  }
399
401
 
402
+ // filterParam keys of the runs page which are API params on their own; the rest go under filter[...]
403
+ const RUNS_PAGE_PARAMS = ['search', 'tql', 'groupId'];
404
+
405
+ /**
406
+ * Parse a Testomat.io app URL of a run (/projects/:project/runs/:id)
407
+ * or of a runs page (/projects/:project/runs?filterParam=...) into API v2 request parts.
408
+ *
409
+ * @param {string} pageUrl
410
+ * @returns {{baseUrl: string, project: string, runId?: string, params: string[][]}|null}
411
+ */
412
+ function parseRunsUrl(pageUrl) {
413
+ let url;
414
+ try {
415
+ url = new URL(pageUrl);
416
+ } catch (e) {
417
+ return null;
418
+ }
419
+
420
+ const match = url.pathname.match(/\/projects\/([^/]+)\/runs(?:\/([^/]+))?\/?$/);
421
+ if (!match) return null;
422
+
423
+ const [, project, runId] = match;
424
+ const params = [];
425
+
426
+ for (const [key, value] of new URLSearchParams(url.searchParams.get('filterParam') || '')) {
427
+ // "groups" only switches the page to the grouped view
428
+ if (!value || key === 'groups') continue;
429
+ if (RUNS_PAGE_PARAMS.includes(key)) {
430
+ params.push([key, value]);
431
+ continue;
432
+ }
433
+ const [name, ...nested] = key.split('[');
434
+ params.push([`filter[${name}]${nested.map(part => `[${part}`).join('')}`, value]);
435
+ }
436
+
437
+ return { baseUrl: url.origin, project, runId, params };
438
+ }
439
+
400
440
  export {
401
441
  updateFilterType,
402
442
  parseFilterParams,
@@ -413,6 +453,7 @@ export {
413
453
  formatFilterListIds,
414
454
  formatRunOutput,
415
455
  formatFetchRunsOutput,
456
+ parseRunsUrl,
416
457
  getObjectSize,
417
458
  splitTestsIntoChunks,
418
459
  };
package/src/xmlReader.js CHANGED
@@ -499,6 +499,9 @@ class XmlReader {
499
499
  this.adapter.formatTest(t);
500
500
 
501
501
  t.title = humanize(t.title);
502
+
503
+ // meta must always include reporter version and adapter used
504
+ t.meta = { ...t.meta, reporterVersion: this.version, adapter: 'xml' };
502
505
  });
503
506
  }
504
507
 
package/types/types.d.ts CHANGED
@@ -374,6 +374,7 @@ declare global {
374
374
  testomatioDataStore?: any;
375
375
  TESTOMATIO_LOGGER_CONSOLE_INTERCEPTED?: boolean;
376
376
  testomatioTestTitle?: string;
377
+ TESTOMATIO_ADAPTER?: string;
377
378
  }
378
379
  }
379
380
  }