@testomatio/reporter 2.17.0-beta → 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.
@@ -317,16 +317,31 @@ function stripExampleFromTitle(title) {
317
317
  }
318
318
  let baseTitle = title.slice(0, res.index).trim();
319
319
  if (exampleParsed) {
320
+ const placeholderKeys = [...new Set([...baseTitle.matchAll(PLACEHOLDER_REGEXP)].map(match => match[1]))];
320
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]);
321
325
  if (key === 'current')
322
326
  return formatExample(example);
323
- if (!Object.hasOwn(example ?? {}, key))
324
- return placeholder;
325
- return formatExample(example[key]);
327
+ return placeholder;
326
328
  });
329
+ example = reduceExample(example, placeholderKeys);
327
330
  }
328
331
  return { title: `${baseTitle}${res[2]}`, example };
329
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
+ }
330
345
  function formatExample(example) {
331
346
  if (example !== null && typeof example === 'object')
332
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
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@testomatio/reporter",
3
- "version": "2.17.0-beta",
3
+ "version": "2.17.0-beta.1",
4
4
  "description": "Testomatio Reporter Client",
5
5
  "engines": {
6
6
  "node": ">=18"
@@ -367,15 +367,30 @@ function stripExampleFromTitle(title) {
367
367
 
368
368
  let baseTitle = title.slice(0, res.index).trim();
369
369
  if (exampleParsed) {
370
+ const placeholderKeys = [...new Set([...baseTitle.matchAll(PLACEHOLDER_REGEXP)].map(match => match[1]))];
370
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]);
371
374
  if (key === 'current') return formatExample(example);
372
- if (!Object.hasOwn(example ?? {}, key)) return placeholder;
373
- return formatExample(example[key]);
375
+ return placeholder;
374
376
  });
377
+ example = reduceExample(example, placeholderKeys);
375
378
  }
376
379
  return { title: `${baseTitle}${res[2]}`, example };
377
380
  }
378
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
+
379
394
  function formatExample(example) {
380
395
  if (example !== null && typeof example === 'object') return JSON.stringify(example);
381
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')
@@ -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
  };