@testomatio/reporter 2.16.0 → 2.17.0-beta.1
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/lib/adapter/codecept.js +24 -2
- package/lib/bin/cli.js +42 -0
- package/lib/utils/pipe_utils.d.ts +12 -0
- package/lib/utils/pipe_utils.js +43 -0
- package/package.json +1 -1
- package/src/adapter/codecept.js +22 -2
- package/src/bin/cli.js +54 -1
- package/src/utils/pipe_utils.js +34 -0
package/lib/adapter/codecept.js
CHANGED
|
@@ -36,6 +36,7 @@ const HOOK_EXECUTION_ORDER = {
|
|
|
36
36
|
data_storage_js_1.dataStorage.isFileStorage = false;
|
|
37
37
|
// CodeceptJS appends the serialized data row of a data-driven test to its title
|
|
38
38
|
const DATA_REGEXP = / \| (\{.*\}|\[.*\]|null|"(?:\\.|[^"\\])*")((?:\s+@[a-zA-Z0-9-_]+)*)$/;
|
|
39
|
+
const PLACEHOLDER_REGEXP = /\$\{([\w_]+)\}/g;
|
|
39
40
|
if (MAJOR_VERSION < 3) {
|
|
40
41
|
console.log('🔴 This reporter works with CodeceptJS 3+, please update your tests');
|
|
41
42
|
}
|
|
@@ -315,11 +316,32 @@ function stripExampleFromTitle(title) {
|
|
|
315
316
|
}
|
|
316
317
|
}
|
|
317
318
|
let baseTitle = title.slice(0, res.index).trim();
|
|
318
|
-
if (exampleParsed
|
|
319
|
-
|
|
319
|
+
if (exampleParsed) {
|
|
320
|
+
const placeholderKeys = [...new Set([...baseTitle.matchAll(PLACEHOLDER_REGEXP)].map(match => match[1]))];
|
|
321
|
+
baseTitle = baseTitle.replace(PLACEHOLDER_REGEXP, (placeholder, key) => {
|
|
322
|
+
// a placeholder matching a key of an object row resolves to that key's value
|
|
323
|
+
if (Object.hasOwn(example ?? {}, key))
|
|
324
|
+
return formatExample(example[key]);
|
|
325
|
+
if (key === 'current')
|
|
326
|
+
return formatExample(example);
|
|
327
|
+
return placeholder;
|
|
328
|
+
});
|
|
329
|
+
example = reduceExample(example, placeholderKeys);
|
|
320
330
|
}
|
|
321
331
|
return { title: `${baseTitle}${res[2]}`, example };
|
|
322
332
|
}
|
|
333
|
+
// When the title uses only some keys of an object row, report just those values,
|
|
334
|
+
// the way Playwright shows only the parameters referenced in the title
|
|
335
|
+
function reduceExample(example, placeholderKeys) {
|
|
336
|
+
if (example === null || typeof example !== 'object' || Array.isArray(example))
|
|
337
|
+
return example;
|
|
338
|
+
const usedKeys = placeholderKeys.filter(key => Object.hasOwn(example, key));
|
|
339
|
+
if (!usedKeys.length)
|
|
340
|
+
return example;
|
|
341
|
+
if (usedKeys.length === 1)
|
|
342
|
+
return example[usedKeys[0]];
|
|
343
|
+
return Object.fromEntries(usedKeys.map(key => [key, example[key]]));
|
|
344
|
+
}
|
|
323
345
|
function formatExample(example) {
|
|
324
346
|
if (example !== null && typeof example === 'object')
|
|
325
347
|
return JSON.stringify(example);
|
package/lib/bin/cli.js
CHANGED
|
@@ -26,6 +26,8 @@ const path_1 = __importDefault(require("path"));
|
|
|
26
26
|
const debug = (0, debug_1.default)('@testomatio/reporter:cli');
|
|
27
27
|
const version = (0, utils_js_1.getPackageVersion)();
|
|
28
28
|
const program = new commander_1.Command();
|
|
29
|
+
const FETCH_RUNS_DEFAULT_LIMIT = 30;
|
|
30
|
+
const FETCH_RUNS_MAX_LIMIT = 100;
|
|
29
31
|
program
|
|
30
32
|
.version(version)
|
|
31
33
|
.option('--env-file <envfile>', 'Load environment variables from env file')
|
|
@@ -116,6 +118,46 @@ program
|
|
|
116
118
|
process.exit(0);
|
|
117
119
|
});
|
|
118
120
|
});
|
|
121
|
+
program
|
|
122
|
+
.command('fetch')
|
|
123
|
+
.description('Fetch runs from Testomat.io API v2')
|
|
124
|
+
.option('--project <slug>', 'Project slug (or set TESTOMATIO_PROJECT)')
|
|
125
|
+
.option('--title <text>', 'Filter by run title')
|
|
126
|
+
.option('--tql <query>', 'Filter using Testomat Query Language')
|
|
127
|
+
.option('--rungroup <uid>', 'Filter by rungroup id')
|
|
128
|
+
.option('--limit <number>', `Max number of runs to return (max ${FETCH_RUNS_MAX_LIMIT})`, String(FETCH_RUNS_DEFAULT_LIMIT))
|
|
129
|
+
.option('--latest', 'Only fetch the most recent run (shorthand for --limit 1)')
|
|
130
|
+
.option('--format <format>', 'Machine-readable output: run ids, one per line (--format id) or run details (--format json)')
|
|
131
|
+
.action(async (opts) => {
|
|
132
|
+
const apiKey = process.env['INPUT_TESTOMATIO-KEY'] || config_js_1.config.TESTOMATIO;
|
|
133
|
+
if (!apiKey) {
|
|
134
|
+
log_js_1.log.error(picocolors_1.default.red('⚠️ TESTOMATIO API key required'));
|
|
135
|
+
process.exit(1);
|
|
136
|
+
}
|
|
137
|
+
const project = opts.project || process.env.TESTOMATIO_PROJECT;
|
|
138
|
+
if (!project) {
|
|
139
|
+
log_js_1.log.error(picocolors_1.default.red('⚠️ --project (or TESTOMATIO_PROJECT) is required'));
|
|
140
|
+
process.exit(1);
|
|
141
|
+
}
|
|
142
|
+
const baseUrl = process.env.TESTOMATIO_URL || 'https://app.testomat.io';
|
|
143
|
+
const url = new URL(`/api/v2/${project}/runs`, baseUrl);
|
|
144
|
+
if (opts.title)
|
|
145
|
+
url.searchParams.set('search', opts.title);
|
|
146
|
+
if (opts.tql)
|
|
147
|
+
url.searchParams.set('tql', opts.tql);
|
|
148
|
+
if (opts.rungroup)
|
|
149
|
+
url.searchParams.set('groupId', opts.rungroup);
|
|
150
|
+
const requestedLimit = opts.latest ? 1 : parseInt(opts.limit, 10) || FETCH_RUNS_DEFAULT_LIMIT;
|
|
151
|
+
const limit = Math.min(Math.max(requestedLimit, 1), FETCH_RUNS_MAX_LIMIT);
|
|
152
|
+
url.searchParams.set('per_page', String(limit));
|
|
153
|
+
const response = await fetch(url, { headers: { Authorization: `Bearer ${apiKey}` } });
|
|
154
|
+
const body = await response.json();
|
|
155
|
+
if (!response.ok) {
|
|
156
|
+
log_js_1.log.error(picocolors_1.default.red(`Failed to fetch runs: ${response.status} ${body.error || response.statusText}`));
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|
|
159
|
+
console.log((0, pipe_utils_js_1.formatFetchRunsOutput)(body, opts.format));
|
|
160
|
+
});
|
|
119
161
|
program
|
|
120
162
|
.command('run')
|
|
121
163
|
.alias('test')
|
|
@@ -121,6 +121,18 @@ export function formatRunOutput(store: {
|
|
|
121
121
|
runUrl?: string;
|
|
122
122
|
runPublicUrl?: string;
|
|
123
123
|
}, format?: string): string;
|
|
124
|
+
/**
|
|
125
|
+
* Format the runs fetched via the `fetch` command for machine-readable output.
|
|
126
|
+
* `json` prints the full array of run objects; `id` prints one run id per line;
|
|
127
|
+
* any other format prints a human-readable multi-line summary per run.
|
|
128
|
+
*
|
|
129
|
+
* @param {{data?: Array}} body - Parsed response body from GET /api/v2/:project/runs.
|
|
130
|
+
* @param {string} [format] - Value of the CLI `--format` option.
|
|
131
|
+
* @returns {string}
|
|
132
|
+
*/
|
|
133
|
+
export function formatFetchRunsOutput(body: {
|
|
134
|
+
data?: any[];
|
|
135
|
+
}, format?: string): string;
|
|
124
136
|
/**
|
|
125
137
|
* Calculate the approximate size of data in bytes (JSON stringified, UTF-8 encoded length).
|
|
126
138
|
* @param {Object} data - Data to measure
|
package/lib/utils/pipe_utils.js
CHANGED
|
@@ -16,6 +16,7 @@ exports.plannedTestsLabel = plannedTestsLabel;
|
|
|
16
16
|
exports.parsePipeOptions = parsePipeOptions;
|
|
17
17
|
exports.formatFilterListIds = formatFilterListIds;
|
|
18
18
|
exports.formatRunOutput = formatRunOutput;
|
|
19
|
+
exports.formatFetchRunsOutput = formatFetchRunsOutput;
|
|
19
20
|
exports.getObjectSize = getObjectSize;
|
|
20
21
|
exports.splitTestsIntoChunks = splitTestsIntoChunks;
|
|
21
22
|
const humanize_duration_1 = __importDefault(require("humanize-duration"));
|
|
@@ -324,6 +325,46 @@ function formatRunOutput(store, format) {
|
|
|
324
325
|
output.runPublicUrl = store.runPublicUrl;
|
|
325
326
|
return JSON.stringify(output);
|
|
326
327
|
}
|
|
328
|
+
/**
|
|
329
|
+
* Format the runs fetched via the `fetch` command for machine-readable output.
|
|
330
|
+
* `json` prints the full array of run objects; `id` prints one run id per line;
|
|
331
|
+
* any other format prints a human-readable multi-line summary per run.
|
|
332
|
+
*
|
|
333
|
+
* @param {{data?: Array}} body - Parsed response body from GET /api/v2/:project/runs.
|
|
334
|
+
* @param {string} [format] - Value of the CLI `--format` option.
|
|
335
|
+
* @returns {string}
|
|
336
|
+
*/
|
|
337
|
+
function formatFetchRunsOutput(body, format) {
|
|
338
|
+
const runs = body?.data || [];
|
|
339
|
+
if (format === 'json')
|
|
340
|
+
return JSON.stringify(runs, null, 2);
|
|
341
|
+
if (format === 'id')
|
|
342
|
+
return runs.map(run => run.id).filter(Boolean).join('\n');
|
|
343
|
+
return runs
|
|
344
|
+
.map(run => {
|
|
345
|
+
const lines = [`* ID: ${run.id}`];
|
|
346
|
+
if (run.title)
|
|
347
|
+
lines.push(` title: ${run.title}`);
|
|
348
|
+
if (run.launched_at)
|
|
349
|
+
lines.push(` started at: ${run.launched_at}`);
|
|
350
|
+
if (run.finished_at)
|
|
351
|
+
lines.push(` finished at: ${run.finished_at}`);
|
|
352
|
+
if (run.passed_count != null)
|
|
353
|
+
lines.push(` passed: ${run.passed_count} tests`);
|
|
354
|
+
if (run.failed_count != null)
|
|
355
|
+
lines.push(` failed: ${run.failed_count} tests`);
|
|
356
|
+
if (run.skipped_count != null)
|
|
357
|
+
lines.push(` skipped: ${run.skipped_count} tests`);
|
|
358
|
+
if (run.tests_count != null)
|
|
359
|
+
lines.push(` tests_count: ${run.tests_count} tests`);
|
|
360
|
+
if (run.env)
|
|
361
|
+
lines.push(` env: ${run.env}`);
|
|
362
|
+
if (run.ci_build_url)
|
|
363
|
+
lines.push(` ci build url: ${run.ci_build_url}`);
|
|
364
|
+
return lines.join('\n');
|
|
365
|
+
})
|
|
366
|
+
.join('\n');
|
|
367
|
+
}
|
|
327
368
|
|
|
328
369
|
module.exports.updateFilterType = updateFilterType;
|
|
329
370
|
|
|
@@ -351,6 +392,8 @@ module.exports.formatFilterListIds = formatFilterListIds;
|
|
|
351
392
|
|
|
352
393
|
module.exports.formatRunOutput = formatRunOutput;
|
|
353
394
|
|
|
395
|
+
module.exports.formatFetchRunsOutput = formatFetchRunsOutput;
|
|
396
|
+
|
|
354
397
|
module.exports.getObjectSize = getObjectSize;
|
|
355
398
|
|
|
356
399
|
module.exports.splitTestsIntoChunks = splitTestsIntoChunks;
|
package/package.json
CHANGED
package/src/adapter/codecept.js
CHANGED
|
@@ -36,6 +36,7 @@ dataStorage.isFileStorage = false;
|
|
|
36
36
|
|
|
37
37
|
// CodeceptJS appends the serialized data row of a data-driven test to its title
|
|
38
38
|
const DATA_REGEXP = / \| (\{.*\}|\[.*\]|null|"(?:\\.|[^"\\])*")((?:\s+@[a-zA-Z0-9-_]+)*)$/;
|
|
39
|
+
const PLACEHOLDER_REGEXP = /\$\{([\w_]+)\}/g;
|
|
39
40
|
|
|
40
41
|
if (MAJOR_VERSION < 3) {
|
|
41
42
|
console.log('🔴 This reporter works with CodeceptJS 3+, please update your tests');
|
|
@@ -365,12 +366,31 @@ function stripExampleFromTitle(title) {
|
|
|
365
366
|
}
|
|
366
367
|
|
|
367
368
|
let baseTitle = title.slice(0, res.index).trim();
|
|
368
|
-
if (exampleParsed
|
|
369
|
-
|
|
369
|
+
if (exampleParsed) {
|
|
370
|
+
const placeholderKeys = [...new Set([...baseTitle.matchAll(PLACEHOLDER_REGEXP)].map(match => match[1]))];
|
|
371
|
+
baseTitle = baseTitle.replace(PLACEHOLDER_REGEXP, (placeholder, key) => {
|
|
372
|
+
// a placeholder matching a key of an object row resolves to that key's value
|
|
373
|
+
if (Object.hasOwn(example ?? {}, key)) return formatExample(example[key]);
|
|
374
|
+
if (key === 'current') return formatExample(example);
|
|
375
|
+
return placeholder;
|
|
376
|
+
});
|
|
377
|
+
example = reduceExample(example, placeholderKeys);
|
|
370
378
|
}
|
|
371
379
|
return { title: `${baseTitle}${res[2]}`, example };
|
|
372
380
|
}
|
|
373
381
|
|
|
382
|
+
// When the title uses only some keys of an object row, report just those values,
|
|
383
|
+
// the way Playwright shows only the parameters referenced in the title
|
|
384
|
+
function reduceExample(example, placeholderKeys) {
|
|
385
|
+
if (example === null || typeof example !== 'object' || Array.isArray(example)) return example;
|
|
386
|
+
|
|
387
|
+
const usedKeys = placeholderKeys.filter(key => Object.hasOwn(example, key));
|
|
388
|
+
if (!usedKeys.length) return example;
|
|
389
|
+
if (usedKeys.length === 1) return example[usedKeys[0]];
|
|
390
|
+
|
|
391
|
+
return Object.fromEntries(usedKeys.map(key => [key, example[key]]));
|
|
392
|
+
}
|
|
393
|
+
|
|
374
394
|
function formatExample(example) {
|
|
375
395
|
if (example !== null && typeof example === 'object') return JSON.stringify(example);
|
|
376
396
|
return String(example);
|
package/src/bin/cli.js
CHANGED
|
@@ -16,7 +16,7 @@ import { filesize as prettyBytes } from 'filesize';
|
|
|
16
16
|
import dotenv from 'dotenv';
|
|
17
17
|
import Replay from '../replay.js';
|
|
18
18
|
import { log } from '../utils/log.js';
|
|
19
|
-
import { formatFilterListIds, formatRunOutput } from '../utils/pipe_utils.js';
|
|
19
|
+
import { formatFilterListIds, formatRunOutput, formatFetchRunsOutput } from '../utils/pipe_utils.js';
|
|
20
20
|
import fs from 'fs';
|
|
21
21
|
import path from 'path';
|
|
22
22
|
|
|
@@ -24,6 +24,9 @@ const debug = createDebugMessages('@testomatio/reporter:cli');
|
|
|
24
24
|
const version = getPackageVersion();
|
|
25
25
|
const program = new Command();
|
|
26
26
|
|
|
27
|
+
const FETCH_RUNS_DEFAULT_LIMIT = 30;
|
|
28
|
+
const FETCH_RUNS_MAX_LIMIT = 100;
|
|
29
|
+
|
|
27
30
|
program
|
|
28
31
|
.version(version)
|
|
29
32
|
.option('--env-file <envfile>', 'Load environment variables from env file')
|
|
@@ -124,6 +127,56 @@ program
|
|
|
124
127
|
});
|
|
125
128
|
});
|
|
126
129
|
|
|
130
|
+
program
|
|
131
|
+
.command('fetch')
|
|
132
|
+
.description('Fetch runs from Testomat.io API v2')
|
|
133
|
+
.option('--project <slug>', 'Project slug (or set TESTOMATIO_PROJECT)')
|
|
134
|
+
.option('--title <text>', 'Filter by run title')
|
|
135
|
+
.option('--tql <query>', 'Filter using Testomat Query Language')
|
|
136
|
+
.option('--rungroup <uid>', 'Filter by rungroup id')
|
|
137
|
+
.option(
|
|
138
|
+
'--limit <number>',
|
|
139
|
+
`Max number of runs to return (max ${FETCH_RUNS_MAX_LIMIT})`,
|
|
140
|
+
String(FETCH_RUNS_DEFAULT_LIMIT),
|
|
141
|
+
)
|
|
142
|
+
.option('--latest', 'Only fetch the most recent run (shorthand for --limit 1)')
|
|
143
|
+
.option(
|
|
144
|
+
'--format <format>',
|
|
145
|
+
'Machine-readable output: run ids, one per line (--format id) or run details (--format json)',
|
|
146
|
+
)
|
|
147
|
+
.action(async opts => {
|
|
148
|
+
const apiKey = process.env['INPUT_TESTOMATIO-KEY'] || config.TESTOMATIO;
|
|
149
|
+
if (!apiKey) {
|
|
150
|
+
log.error(pc.red('⚠️ TESTOMATIO API key required'));
|
|
151
|
+
process.exit(1);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const project = opts.project || process.env.TESTOMATIO_PROJECT;
|
|
155
|
+
if (!project) {
|
|
156
|
+
log.error(pc.red('⚠️ --project (or TESTOMATIO_PROJECT) is required'));
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const baseUrl = process.env.TESTOMATIO_URL || 'https://app.testomat.io';
|
|
161
|
+
const url = new URL(`/api/v2/${project}/runs`, baseUrl);
|
|
162
|
+
if (opts.title) url.searchParams.set('search', opts.title);
|
|
163
|
+
if (opts.tql) url.searchParams.set('tql', opts.tql);
|
|
164
|
+
if (opts.rungroup) url.searchParams.set('groupId', opts.rungroup);
|
|
165
|
+
const requestedLimit = opts.latest ? 1 : parseInt(opts.limit, 10) || FETCH_RUNS_DEFAULT_LIMIT;
|
|
166
|
+
const limit = Math.min(Math.max(requestedLimit, 1), FETCH_RUNS_MAX_LIMIT);
|
|
167
|
+
url.searchParams.set('per_page', String(limit));
|
|
168
|
+
|
|
169
|
+
const response = await fetch(url, { headers: { Authorization: `Bearer ${apiKey}` } });
|
|
170
|
+
const body = await response.json();
|
|
171
|
+
|
|
172
|
+
if (!response.ok) {
|
|
173
|
+
log.error(pc.red(`Failed to fetch runs: ${response.status} ${body.error || response.statusText}`));
|
|
174
|
+
process.exit(1);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
console.log(formatFetchRunsOutput(body, opts.format));
|
|
178
|
+
});
|
|
179
|
+
|
|
127
180
|
program
|
|
128
181
|
.command('run')
|
|
129
182
|
.alias('test')
|
package/src/utils/pipe_utils.js
CHANGED
|
@@ -325,6 +325,39 @@ function formatRunOutput(store, format) {
|
|
|
325
325
|
return JSON.stringify(output);
|
|
326
326
|
}
|
|
327
327
|
|
|
328
|
+
/**
|
|
329
|
+
* Format the runs fetched via the `fetch` command for machine-readable output.
|
|
330
|
+
* `json` prints the full array of run objects; `id` prints one run id per line;
|
|
331
|
+
* any other format prints a human-readable multi-line summary per run.
|
|
332
|
+
*
|
|
333
|
+
* @param {{data?: Array}} body - Parsed response body from GET /api/v2/:project/runs.
|
|
334
|
+
* @param {string} [format] - Value of the CLI `--format` option.
|
|
335
|
+
* @returns {string}
|
|
336
|
+
*/
|
|
337
|
+
function formatFetchRunsOutput(body, format) {
|
|
338
|
+
const runs = body?.data || [];
|
|
339
|
+
|
|
340
|
+
if (format === 'json') return JSON.stringify(runs, null, 2);
|
|
341
|
+
|
|
342
|
+
if (format === 'id') return runs.map(run => run.id).filter(Boolean).join('\n');
|
|
343
|
+
|
|
344
|
+
return runs
|
|
345
|
+
.map(run => {
|
|
346
|
+
const lines = [`* ID: ${run.id}`];
|
|
347
|
+
if (run.title) lines.push(` title: ${run.title}`);
|
|
348
|
+
if (run.launched_at) lines.push(` started at: ${run.launched_at}`);
|
|
349
|
+
if (run.finished_at) lines.push(` finished at: ${run.finished_at}`);
|
|
350
|
+
if (run.passed_count != null) lines.push(` passed: ${run.passed_count} tests`);
|
|
351
|
+
if (run.failed_count != null) lines.push(` failed: ${run.failed_count} tests`);
|
|
352
|
+
if (run.skipped_count != null) lines.push(` skipped: ${run.skipped_count} tests`);
|
|
353
|
+
if (run.tests_count != null) lines.push(` tests_count: ${run.tests_count} tests`);
|
|
354
|
+
if (run.env) lines.push(` env: ${run.env}`);
|
|
355
|
+
if (run.ci_build_url) lines.push(` ci build url: ${run.ci_build_url}`);
|
|
356
|
+
return lines.join('\n');
|
|
357
|
+
})
|
|
358
|
+
.join('\n');
|
|
359
|
+
}
|
|
360
|
+
|
|
328
361
|
export {
|
|
329
362
|
updateFilterType,
|
|
330
363
|
parseFilterParams,
|
|
@@ -339,6 +372,7 @@ export {
|
|
|
339
372
|
parsePipeOptions,
|
|
340
373
|
formatFilterListIds,
|
|
341
374
|
formatRunOutput,
|
|
375
|
+
formatFetchRunsOutput,
|
|
342
376
|
getObjectSize,
|
|
343
377
|
splitTestsIntoChunks,
|
|
344
378
|
};
|