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.
- package/package.json +1 -1
- package/src/adapter/codecept.js +24 -2
- package/src/adapter/cucumber/current.js +2 -0
- package/src/adapter/cypress-plugin/index.js +2 -0
- package/src/adapter/e2e.js +248 -0
- package/src/adapter/jasmine.js +2 -0
- package/src/adapter/jest.js +2 -0
- package/src/adapter/mocha.js +146 -145
- package/src/adapter/nightwatch.js +2 -0
- package/src/adapter/playwright.js +33 -1
- package/src/adapter/vitest.js +6 -1
- package/src/adapter/webdriver.js +46 -2
- package/src/allureReader.js +2 -1
- package/src/bin/cli.js +246 -18
- package/src/client.js +11 -3
- package/src/data-storage.js +4 -3
- package/src/fetch/report.js +86 -0
- package/src/fetch/testruns.js +283 -0
- package/src/pipe/markdown.js +7 -0
- package/src/pipe/testomatio.js +27 -1
- package/src/replay.js +1 -0
- package/src/reporter-functions.js +41 -0
- package/src/uploader.js +81 -1
- package/src/utils/detectRunner.js +151 -0
- package/src/utils/pipe_utils.js +45 -4
- package/src/xmlReader.js +3 -0
- package/types/types.d.ts +1 -0
|
@@ -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
|
+
};
|
package/src/utils/pipe_utils.js
CHANGED
|
@@ -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
|
|
377
|
+
const data = body?.data || [];
|
|
378
|
+
const runs = [data].flat();
|
|
378
379
|
|
|
379
|
-
if (format === 'json') return JSON.stringify(
|
|
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