@testomatio/reporter 2.6.0-beta.1.allure → 2.6.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.
Files changed (57) hide show
  1. package/README.md +9 -11
  2. package/lib/adapter/playwright.d.ts +2 -0
  3. package/lib/adapter/playwright.js +29 -5
  4. package/lib/adapter/utils/playwright.d.ts +25 -0
  5. package/lib/adapter/utils/playwright.js +123 -0
  6. package/lib/adapter/vitest.js +2 -1
  7. package/lib/bin/cli.js +36 -36
  8. package/lib/data-storage.d.ts +1 -1
  9. package/lib/data-storage.js +1 -0
  10. package/lib/junit-adapter/index.js +0 -4
  11. package/lib/pipe/coverage.js +63 -5
  12. package/lib/pipe/debug.js +1 -2
  13. package/lib/pipe/github.js +15 -0
  14. package/lib/pipe/html.d.ts +2 -3
  15. package/lib/pipe/html.js +745 -37
  16. package/lib/pipe/testomatio.js +83 -36
  17. package/lib/reporter-functions.d.ts +36 -11
  18. package/lib/reporter-functions.js +72 -22
  19. package/lib/reporter.d.ts +90 -38
  20. package/lib/services/artifacts.d.ts +1 -1
  21. package/lib/services/key-values.d.ts +1 -1
  22. package/lib/services/links.d.ts +5 -3
  23. package/lib/services/links.js +1 -1
  24. package/lib/services/logger.d.ts +1 -1
  25. package/lib/template/testomatio-old.hbs +1421 -0
  26. package/lib/template/testomatio.hbs +3200 -1157
  27. package/lib/utils/log-formatter.d.ts +1 -2
  28. package/lib/utils/log-formatter.js +8 -4
  29. package/lib/utils/utils.js +0 -9
  30. package/package.json +2 -2
  31. package/src/adapter/playwright.js +32 -6
  32. package/src/adapter/utils/playwright.js +121 -0
  33. package/src/adapter/vitest.js +2 -1
  34. package/src/bin/cli.js +39 -47
  35. package/src/data-storage.js +1 -0
  36. package/src/junit-adapter/index.js +0 -4
  37. package/src/pipe/coverage.js +90 -32
  38. package/src/pipe/debug.js +1 -2
  39. package/src/pipe/github.js +14 -0
  40. package/src/pipe/html.js +844 -38
  41. package/src/pipe/testomatio.js +98 -53
  42. package/src/reporter-functions.js +73 -25
  43. package/src/services/links.js +1 -1
  44. package/src/template/testomatio-old.hbs +1421 -0
  45. package/src/template/testomatio.hbs +3200 -1157
  46. package/src/utils/log-formatter.js +9 -4
  47. package/src/utils/utils.js +0 -5
  48. package/types/types.d.ts +30 -6
  49. package/lib/allureReader.d.ts +0 -65
  50. package/lib/allureReader.js +0 -448
  51. package/lib/junit-adapter/kotlin.d.ts +0 -5
  52. package/lib/junit-adapter/kotlin.js +0 -46
  53. package/lib/services/labels.d.ts +0 -0
  54. package/lib/services/labels.js +0 -0
  55. package/src/allureReader.js +0 -523
  56. package/src/junit-adapter/kotlin.js +0 -48
  57. package/src/services/labels.js +0 -1
@@ -25,5 +25,4 @@ export function formatError(error: Error & {
25
25
  actual?: any;
26
26
  expected?: any;
27
27
  }, message?: string): string;
28
- export const stripColors: typeof stripVTControlCharacters;
29
- import { stripVTControlCharacters } from 'util';
28
+ export function stripColors(str: string): string;
@@ -114,10 +114,14 @@ function formatError(error, message) {
114
114
  * @returns {boolean}
115
115
  */
116
116
  function isNotInternalFrame(frame) {
117
- return (frame.getFileName() &&
118
- frame.getFileName().includes(path_1.sep) &&
119
- !frame.getFileName().includes('node_modules') &&
120
- !frame.getFileName().includes('internal'));
117
+ const fileName = frame.getFileName();
118
+ if (!fileName)
119
+ return false;
120
+ const isFileUrl = fileName.startsWith('file://');
121
+ const hasPathSeparator = fileName.includes(path_1.sep) || fileName.includes('/') || isFileUrl;
122
+ return (hasPathSeparator &&
123
+ !fileName.includes('node_modules') &&
124
+ !fileName.includes('internal'));
121
125
  }
122
126
 
123
127
  module.exports.formatLogs = formatLogs;
@@ -311,15 +311,6 @@ const fetchSourceCode = (contents, opts = {}) => {
311
311
  if (lineIndex === -1)
312
312
  lineIndex = lines.findIndex(l => l.includes(`${title}(`));
313
313
  }
314
- else if (opts.lang === 'kotlin') {
315
- lineIndex = lines.findIndex(l => l.includes(`fun test${title}`));
316
- if (lineIndex === -1)
317
- lineIndex = lines.findIndex(l => l.includes(`@DisplayName("${title}`));
318
- if (lineIndex === -1)
319
- lineIndex = lines.findIndex(l => l.includes(`fun ${title}`));
320
- if (lineIndex === -1)
321
- lineIndex = lines.findIndex(l => l.includes(`${title}(`));
322
- }
323
314
  else if (opts.lang === 'csharp') {
324
315
  // Find the method declaration line
325
316
  let methodLineIndex = lines.findIndex(l => l.includes(`public void ${title}(`));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@testomatio/reporter",
3
- "version": "2.6.0-beta.1.allure",
3
+ "version": "2.6.1",
4
4
  "description": "Testomatio Reporter Client",
5
5
  "engines": {
6
6
  "node": ">=18"
@@ -24,7 +24,7 @@
24
24
  "csv-writer": "^1.6.0",
25
25
  "debug": "4.3.4",
26
26
  "dotenv": "^16.0.1",
27
- "fast-xml-parser": "^4.4.1",
27
+ "fast-xml-parser": "^5.3.4",
28
28
  "file-url": "3.0.0",
29
29
  "filesize": "^10.1.6",
30
30
  "gaxios": ">=6.0 || >=7.0.0-rc.4 || <8",
@@ -1,4 +1,3 @@
1
- import pc from 'picocolors';
2
1
  import crypto from 'crypto';
3
2
  import os from 'os';
4
3
  import path from 'path';
@@ -10,6 +9,8 @@ import { getTestomatIdFromTestTitle, fileSystem } from '../utils/utils.js';
10
9
  import { services } from '../services/index.js';
11
10
  import { dataStorage } from '../data-storage.js';
12
11
  import { extensionMap } from '../utils/constants.js';
12
+ import pc from 'picocolors';
13
+ import { fetchLinksFromLogs } from './utils/playwright.js';
13
14
 
14
15
  const reportTestPromises = [];
15
16
 
@@ -41,6 +42,16 @@ class PlaywrightReporter {
41
42
 
42
43
  const { title } = test;
43
44
  const { error, duration } = result;
45
+ const pwAttachments = (result.attachments || []).filter(a => a.body || a.path);
46
+
47
+ const files = pwAttachments
48
+ .map(att => ({
49
+ path: this.#getArtifactPath(att),
50
+ title: att.name || title,
51
+ type: att.contentType,
52
+ }))
53
+ .filter(f => f.path);
54
+
44
55
  const suite_title = test.parent ? test.parent?.title : path.basename(test?.location?.file);
45
56
 
46
57
  const steps = [];
@@ -55,13 +66,26 @@ class PlaywrightReporter {
55
66
  const tags = extractTags(test);
56
67
 
57
68
  const fullTestTitle = getTestContextName(test);
69
+
58
70
  let logs = '';
59
- if (result.stderr.length || result.stdout.length) {
60
- logs = `\n\n${pc.bold('Logs:')}\n${pc.red(result.stderr.join(''))}\n${result.stdout.join('')}`;
71
+ // get links along with filtered logs (liks related logs removed)
72
+ const { stdout: filteredStdout, links, meta } = fetchLinksFromLogs(result.stdout);
73
+ if (filteredStdout?.length || result.stderr?.length) {
74
+ logs = `\n\n${pc.bold('Logs:')}\n${pc.red(result.stderr.join(''))}\n${filteredStdout.join('')}`;
61
75
  }
76
+
77
+ /*
78
+ All services fucntions work different for Playwright.
79
+ We don't have access to test title (as result, to test id) when calling this functions inside a test.
80
+ Thus, when user calls services functions inside a test, we just log this data to console.
81
+ Playwright intercepts the console.log on it's end and we just get this data from it.
82
+ Thus, we have a tiny drawback: all data from services functions inside a test will be logged to console.
83
+ And this requires a condition to be added for each service function – if its Playwright, then log to console.
84
+
85
+ "get" method of services will not return data for Playwright, we should parse stdout.
86
+ */
62
87
  const manuallyAttachedArtifacts = services.artifacts.get(fullTestTitle);
63
88
  const testMeta = services.keyValues.get(fullTestTitle);
64
- const links = services.links.get(fullTestTitle);
65
89
  const rid = test.id || test.testId || uuidv4();
66
90
 
67
91
  /**
@@ -102,12 +126,14 @@ class PlaywrightReporter {
102
126
  logs,
103
127
  links,
104
128
  manuallyAttachedArtifacts,
129
+ files: files.length ? files : undefined,
105
130
  meta: {
106
131
  browser: project.browser,
107
132
  isMobile: project.isMobile,
108
133
  project: project.name,
109
134
  projectDependencies: project.dependencies?.length ? project.dependencies : null,
110
135
  ...testMeta,
136
+ ...meta,
111
137
  ...project.metadata, // metadata has any type (in playwright), but we will stringify it in client.js
112
138
  ...test.annotations?.reduce((acc, annotation) => {
113
139
  acc[annotation.type] = annotation.description;
@@ -120,7 +146,7 @@ class PlaywrightReporter {
120
146
  this.uploads.push({
121
147
  rid: `${rid}-${project.name}`,
122
148
  title: test.title,
123
- files: result.attachments.filter(a => a.body || a.path),
149
+ files: pwAttachments,
124
150
  file: test.location?.file,
125
151
  });
126
152
  // remove empty uploads
@@ -289,4 +315,4 @@ function getTestContextName(test) {
289
315
  }
290
316
 
291
317
  export default PlaywrightReporter;
292
- export { extractTags };
318
+ export { extractTags, fetchLinksFromLogs };
@@ -0,0 +1,121 @@
1
+ import createDebugMessages from 'debug';
2
+ const debug = createDebugMessages('@testomatio/reporter:adapter-playwright-utils');
3
+
4
+ export const playwrightLogsMarkers = {
5
+ label: '[TESTOMATIO-LABEL]',
6
+ meta: '[TESTOMATIO-META]',
7
+ linkTest: '[TESTOMATIO-LINK-TEST]',
8
+ linkJira: '[TESTOMATIO-LINK-JIRA]',
9
+ };
10
+
11
+ /**
12
+ * Fetches links from stdout. Returns links and filtered stdout (without data containing markers)
13
+ *
14
+ * @param {(string | Buffer)[]} stdout
15
+ * @returns {{
16
+ * links: { [key: 'test' | 'jira' | 'label']: string }[],
17
+ * meta: { [key: string]: any },
18
+ * stdout: (string | Buffer)[]
19
+ * }}
20
+ */
21
+ export function fetchLinksFromLogs(stdout) {
22
+ const links = [];
23
+ const meta = {};
24
+
25
+ const markers = [
26
+ { key: playwrightLogsMarkers.linkTest, type: 'test' },
27
+ { key: playwrightLogsMarkers.linkJira, type: 'jira' },
28
+ { key: playwrightLogsMarkers.label, type: 'label' },
29
+ { key: playwrightLogsMarkers.meta, type: 'meta' },
30
+ ];
31
+
32
+ const filteredStdout = [];
33
+
34
+ stdout.forEach(entry => {
35
+ if (typeof entry !== 'string') {
36
+ filteredStdout.push(entry);
37
+ return;
38
+ }
39
+
40
+ // check if entry contains any of markers
41
+ if (!markers.some(m => entry.includes(m.key))) {
42
+ filteredStdout.push(entry);
43
+ return;
44
+ }
45
+
46
+ const newEntryLines = [];
47
+ entry.split('\n').forEach(line => {
48
+ line = line.trim();
49
+ let hasMarker = false;
50
+ for (const marker of markers) {
51
+ // links (test/jira/label) stored as array of objects
52
+ if (line.includes(marker.key)) {
53
+ hasMarker = true;
54
+ try {
55
+ const rawData = line.split(marker.key)[1]?.trim();
56
+ if (!rawData) continue;
57
+
58
+ let data;
59
+ try {
60
+ data = JSON.parse(rawData);
61
+ } catch (e) {
62
+ // Try to extract JSON from the beginning of the string (to handle trailing text)
63
+ const jsonMatch = rawData.match(/^\s*(\[.*?\]|\{.*?\})/);
64
+ if (jsonMatch) {
65
+ try {
66
+ data = JSON.parse(jsonMatch[1]);
67
+ } catch (jsonError) {
68
+ // If JSON extraction fails, skip this entry
69
+ debug('Error parsing links from string:', line, '\n', jsonError);
70
+ continue;
71
+ }
72
+ } else {
73
+ // No JSON found, skip this entry
74
+ debug('No valid JSON found in:', line);
75
+ continue;
76
+ }
77
+ }
78
+
79
+ if (marker.type === 'meta') {
80
+ // meta stored as an object, thus make it similar to links
81
+ Object.assign(meta, data);
82
+ } else {
83
+ const ids = Array.isArray(data) ? data : [data];
84
+ links.push(
85
+ ...ids
86
+ // filter non-truthy ids
87
+ .filter(id => !!id)
88
+ .map(id => {
89
+ // If id is already an object with the marker type key, return it as is
90
+ if (typeof id === 'object' && id !== null && marker.type in id) {
91
+ return id;
92
+ }
93
+ // Otherwise, wrap it with the marker type key
94
+ return {
95
+ // marker type is either 'test' or 'jira' or 'label'
96
+ [marker.type]: id,
97
+ };
98
+ }),
99
+ );
100
+ }
101
+ } catch (e) {
102
+ debug('Error parsing links from string:', line, '\n', e);
103
+ }
104
+ }
105
+ }
106
+ if (!hasMarker && line) {
107
+ newEntryLines.push(line);
108
+ }
109
+ });
110
+
111
+ if (newEntryLines.length) {
112
+ filteredStdout.push(newEntryLines.join('\n'));
113
+ }
114
+ });
115
+
116
+ return {
117
+ stdout: filteredStdout,
118
+ links,
119
+ meta,
120
+ };
121
+ }
@@ -101,7 +101,7 @@ class VitestReporter {
101
101
  *
102
102
  * @param {VitestTest} test
103
103
  *
104
- * @returns {TestData & {status: string}}
104
+ * @returns {TestData & {status: 'passed' | 'failed' | 'skipped'}}
105
105
  */
106
106
  #getDataFromTest(test) {
107
107
  return {
@@ -109,6 +109,7 @@ class VitestReporter {
109
109
  file: test.file.name,
110
110
  logs: test.logs ? transformLogsToString(test.logs) : '',
111
111
  meta: test.meta,
112
+ // @ts-ignore - STATUS values are string literals but type system sees them as string
112
113
  status: getTestStatus(test),
113
114
  suite_title: test.suite.name || test.file?.name,
114
115
  test_id: getTestomatIdFromTestTitle(test.name),
package/src/bin/cli.js CHANGED
@@ -6,7 +6,6 @@ import { glob } from 'glob';
6
6
  import createDebugMessages from 'debug';
7
7
  import TestomatClient from '../client.js';
8
8
  import XmlReader from '../xmlReader.js';
9
- import AllureReader from '../allureReader.js';
10
9
  import { APP_PREFIX, STATUS } from '../constants.js';
11
10
  import { cleanLatestRunId, getPackageVersion, applyFilter } from '../utils/utils.js';
12
11
  import { config } from '../config.js';
@@ -16,7 +15,7 @@ import { filesize as prettyBytes } from 'filesize';
16
15
  import dotenv from 'dotenv';
17
16
  import Replay from '../replay.js';
18
17
 
19
- const debug = createDebugMessages('@testomatio/reporter:xml-cli');
18
+ const debug = createDebugMessages('@testomatio/reporter:cli');
20
19
  const version = getPackageVersion();
21
20
  console.log(pc.cyan(pc.bold(` 🤩 Testomat.io Reporter v${version}`)));
22
21
  const program = new Command();
@@ -80,22 +79,17 @@ program
80
79
  .command('run')
81
80
  .alias('test')
82
81
  .description('Run tests with the specified command')
83
- .argument('<command>', 'Test runner command')
82
+ .argument('[command]', 'Test runner command')
84
83
  .option('--filter <filter>', 'Additional execution filter')
85
84
  .option('--filter-list <filter>', 'Get a list of all tests by filter before running')
86
85
  .option('--kind <type>', 'Specify run type: automated, manual, or mixed')
87
86
  .action(async (command, opts) => {
88
87
  const apiKey = process.env['INPUT_TESTOMATIO-KEY'] || config.TESTOMATIO;
89
88
  const title = process.env.TESTOMATIO_TITLE;
90
-
91
- if (!command || !command.split) {
92
- console.log(APP_PREFIX, `No command provided. Use -c option to launch a test runner.`);
93
- return process.exit(255);
94
- }
95
-
96
89
  const client = new TestomatClient({ apiKey, title });
97
90
 
98
91
  if (opts.filter || opts.filterList) {
92
+ console.log(APP_PREFIX,'Filtering tests...');
99
93
  // Example of use: npx @testomatio/reporter run "npx jest" --filter "testomatio:tag-name=frontend"
100
94
  // Example of use: npx @testomatio/reporter run "npx jest" --filter "coverage:file=coverage.yml"
101
95
  // Example of use: npx @testomatio/reporter run "npx jest" --filter-list "coverage:file=coverage.yml"
@@ -103,6 +97,9 @@ program
103
97
  const pipeOptions = optsArray.join(':');
104
98
 
105
99
  const prepareRunParams = { pipe, pipeOptions };
100
+ if (opts.filterList) {
101
+ client.pipeStore.filterList = true;
102
+ }
106
103
 
107
104
  try {
108
105
  const tests = await client.prepareRun(prepareRunParams);
@@ -117,19 +114,48 @@ program
117
114
 
118
115
  debug(`Execution pattern: "${pattern}"`);
119
116
 
120
- if (opts.filterList) {
117
+ if(opts.filterList) {
121
118
  console.log(APP_PREFIX, pc.blue(`Matched test/suite IDs: ${tests.join(', ')}`));
122
- console.log(APP_PREFIX, pc.green(`Full Running Command: ${filteredCommand}`));
119
+ if (command) console.log(APP_PREFIX, pc.green(`Full Running Command: ${filteredCommand}`));
123
120
  return;
124
121
  }
125
122
 
126
- command = filteredCommand;
127
- } catch (err) {
123
+ if (command && command.split) {
124
+ command = filteredCommand;
125
+ }
126
+ }
127
+ catch (err) {
128
128
  console.log(APP_PREFIX, err.message || err);
129
129
  return;
130
130
  }
131
131
  }
132
132
 
133
+ // just create a run (wich tests which match filters) without executing tests
134
+ if (!command || !command.split) {
135
+ const createRunParams = {};
136
+ if (title) {
137
+ createRunParams.title = title;
138
+ }
139
+ if (opts.kind) {
140
+ createRunParams.kind = opts.kind;
141
+ }
142
+
143
+ if (apiKey) {
144
+ await client.createRun(createRunParams);
145
+ const runId = process.env.TESTOMATIO_RUN || process.env.runId;
146
+ if (client.pipeStore.runUrl) console.log(APP_PREFIX, `📊 Report URL: ${pc.magenta(client.pipeStore.runUrl)}`);
147
+
148
+ if (opts.kind !== 'manual') {
149
+ console.log(APP_PREFIX, `No command passed, so you need to run tests yourself:`);
150
+ console.log(APP_PREFIX, `TESTOMATIO_RUN=${runId} <command>`);
151
+ }
152
+ } else {
153
+ console.log(APP_PREFIX, '⚠️ No API key provided. Cannot create run without TESTOMATIO key.');
154
+ process.exit(1);
155
+ }
156
+ return process.exit(0);
157
+ }
158
+
133
159
  console.log(APP_PREFIX, `🚀 Running`, pc.green(command));
134
160
 
135
161
  const runTests = async () => {
@@ -237,40 +263,6 @@ program
237
263
  if (timeoutTimer) clearTimeout(timeoutTimer);
238
264
  });
239
265
 
240
- program
241
- .command('allure')
242
- .description('Parse Allure result files and upload to Testomat.io')
243
- .argument('<pattern>', 'Allure result directory pattern')
244
- .option('-d, --dir <dir>', 'Project directory')
245
- .option('--timelimit <time>', 'default time limit in seconds to kill a stuck process')
246
- .option('--with-package', 'Keep full package path in file names (default: strip package prefix)')
247
- .action(async (pattern, opts) => {
248
- const runReader = new AllureReader({ withPackage: opts.withPackage });
249
-
250
- let timeoutTimer;
251
- if (opts.timelimit) {
252
- timeoutTimer = setTimeout(
253
- () => {
254
- console.log(
255
- `⚠️ Reached timeout of ${opts.timelimit}s. Exiting... (Exit code is 0 to not fail the pipeline)`,
256
- );
257
- process.exit(0);
258
- },
259
- parseInt(opts.timelimit, 10) * 1000,
260
- );
261
- }
262
-
263
- try {
264
- await runReader.parse(pattern);
265
- await runReader.createRun();
266
- await runReader.uploadData();
267
- } catch (err) {
268
- console.log(APP_PREFIX, 'Error uploading Allure results:', err);
269
- }
270
-
271
- if (timeoutTimer) clearTimeout(timeoutTimer);
272
- });
273
-
274
266
  program
275
267
  .command('upload-artifacts')
276
268
  .description('Upload artifacts to Testomat.io')
@@ -139,6 +139,7 @@ class DataStorage {
139
139
  const testDataAsText = fs.readFileSync(filepath, 'utf-8');
140
140
  if (testDataAsText) debug('<=', dataType, 'file', context, testDataAsText);
141
141
  const testDataArr = testDataAsText?.split(os.EOL) || [];
142
+ debug('<=', dataType, 'file', context, testDataArr);
142
143
  return testDataArr;
143
144
  }
144
145
  // debug(`No ${this.dataType} data for ${context} in <file> storage`);
@@ -4,7 +4,6 @@ import JavaAdapter from './java.js';
4
4
  import PythonAdapter from './python.js';
5
5
  import RubyAdapter from './ruby.js';
6
6
  import CSharpAdapter from './csharp.js';
7
- import KotlinAdapter from './kotlin.js';
8
7
 
9
8
  function AdapterFactory(lang, opts) {
10
9
  if (lang === 'java') {
@@ -22,9 +21,6 @@ function AdapterFactory(lang, opts) {
22
21
  if (lang === 'c#' || lang === 'csharp') {
23
22
  return new CSharpAdapter(opts);
24
23
  }
25
- if (lang === 'kotlin') {
26
- return new KotlinAdapter(opts);
27
- }
28
24
 
29
25
  return new Adapter(opts);
30
26
  }