@testomatio/reporter 2.1.0-beta-nightwatch → 2.1.0-beta.2-codeceptjs

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 (80) hide show
  1. package/README.md +1 -0
  2. package/lib/adapter/codecept.js +288 -202
  3. package/lib/adapter/cypress-plugin/index.js +0 -2
  4. package/lib/adapter/mocha.js +0 -1
  5. package/lib/adapter/nightwatch.js +5 -5
  6. package/lib/adapter/playwright.js +11 -3
  7. package/lib/adapter/webdriver.d.ts +1 -1
  8. package/lib/adapter/webdriver.js +18 -8
  9. package/lib/bin/cli.js +73 -8
  10. package/lib/bin/reportXml.js +4 -2
  11. package/lib/bin/startTest.js +3 -2
  12. package/lib/bin/uploadArtifacts.js +5 -4
  13. package/lib/client.js +31 -10
  14. package/lib/data-storage.d.ts +5 -5
  15. package/lib/data-storage.js +23 -13
  16. package/lib/junit-adapter/csharp.d.ts +1 -0
  17. package/lib/junit-adapter/csharp.js +11 -1
  18. package/lib/pipe/bitbucket.d.ts +2 -0
  19. package/lib/pipe/bitbucket.js +38 -26
  20. package/lib/pipe/debug.js +27 -6
  21. package/lib/pipe/github.d.ts +2 -2
  22. package/lib/pipe/github.js +35 -3
  23. package/lib/pipe/gitlab.d.ts +2 -0
  24. package/lib/pipe/gitlab.js +27 -9
  25. package/lib/pipe/html.js +0 -3
  26. package/lib/pipe/index.js +17 -7
  27. package/lib/pipe/testomatio.d.ts +3 -2
  28. package/lib/pipe/testomatio.js +85 -75
  29. package/lib/replay.d.ts +31 -0
  30. package/lib/replay.js +259 -0
  31. package/lib/reporter-functions.d.ts +7 -0
  32. package/lib/reporter-functions.js +36 -0
  33. package/lib/reporter.d.ts +15 -12
  34. package/lib/reporter.js +4 -1
  35. package/lib/services/artifacts.d.ts +1 -1
  36. package/lib/services/index.d.ts +2 -0
  37. package/lib/services/index.js +2 -0
  38. package/lib/services/key-values.d.ts +1 -1
  39. package/lib/services/labels.d.ts +22 -0
  40. package/lib/services/labels.js +62 -0
  41. package/lib/services/logger.d.ts +1 -1
  42. package/lib/services/logger.js +1 -2
  43. package/lib/template/testomatio.hbs +443 -68
  44. package/lib/uploader.js +10 -6
  45. package/lib/utils/constants.d.ts +12 -0
  46. package/lib/utils/constants.js +15 -0
  47. package/lib/utils/utils.d.ts +10 -1
  48. package/lib/utils/utils.js +70 -22
  49. package/lib/xmlReader.js +57 -19
  50. package/package.json +16 -11
  51. package/src/adapter/codecept.js +320 -214
  52. package/src/adapter/cypress-plugin/index.js +0 -2
  53. package/src/adapter/mocha.js +0 -1
  54. package/src/adapter/nightwatch.js +1 -1
  55. package/src/adapter/playwright.js +10 -7
  56. package/src/adapter/webdriver.js +13 -5
  57. package/src/bin/cli.js +78 -7
  58. package/src/bin/reportXml.js +4 -1
  59. package/src/bin/startTest.js +2 -1
  60. package/src/bin/uploadArtifacts.js +2 -1
  61. package/src/client.js +28 -5
  62. package/src/data-storage.js +6 -6
  63. package/src/junit-adapter/csharp.js +13 -1
  64. package/src/pipe/bitbucket.js +22 -24
  65. package/src/pipe/debug.js +26 -5
  66. package/src/pipe/github.js +1 -2
  67. package/src/pipe/gitlab.js +27 -9
  68. package/src/pipe/html.js +1 -4
  69. package/src/pipe/testomatio.js +112 -107
  70. package/src/replay.js +268 -0
  71. package/src/reporter-functions.js +41 -0
  72. package/src/reporter.js +3 -0
  73. package/src/services/index.js +2 -0
  74. package/src/services/labels.js +59 -0
  75. package/src/services/logger.js +1 -2
  76. package/src/template/testomatio.hbs +443 -68
  77. package/src/uploader.js +11 -6
  78. package/src/utils/constants.js +12 -0
  79. package/src/utils/utils.js +67 -15
  80. package/src/xmlReader.js +73 -18
package/src/replay.js ADDED
@@ -0,0 +1,268 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ import TestomatClient from './client.js';
5
+ import { STATUS } from './constants.js';
6
+ import { config } from './config.js';
7
+
8
+ export class Replay {
9
+ constructor(options = {}) {
10
+ this.apiKey = options.apiKey || config.TESTOMATIO || undefined;
11
+ this.dryRun = options.dryRun || false;
12
+ this.onProgress = options.onProgress || (() => {});
13
+ this.onLog = options.onLog || console.log;
14
+ this.onError = options.onError || console.error;
15
+ }
16
+
17
+ /**
18
+ * Get the default debug file path
19
+ * @returns {string} Path to the latest debug file
20
+ */
21
+ getDefaultDebugFile() {
22
+ return path.join(os.tmpdir(), 'testomatio.debug.latest.json');
23
+ }
24
+
25
+ /**
26
+ * Parse a debug file and extract test data
27
+ * @param {string} debugFile - Path to the debug file
28
+ * @returns {Object} Parsed debug data
29
+ */
30
+ parseDebugFile(debugFile) {
31
+ if (!fs.existsSync(debugFile)) {
32
+ throw new Error(`Debug file not found: ${debugFile}`);
33
+ }
34
+
35
+ const fileContent = fs.readFileSync(debugFile, 'utf-8');
36
+ const lines = fileContent
37
+ .trim()
38
+ .split('\n')
39
+ .filter(line => line.trim() !== '');
40
+
41
+ if (lines.length === 0) {
42
+ throw new Error('Debug file is empty');
43
+ }
44
+
45
+ let runParams = {};
46
+ let finishParams = {};
47
+ let parseErrors = 0;
48
+ const testsMap = new Map(); // Use Map to deduplicate by rid
49
+ const testsWithoutRid = []; // For tests without rid (backward compatibility)
50
+ const envVars = {};
51
+ let runId = null;
52
+
53
+ // Parse debug file line by line
54
+ for (const [lineIndex, line] of lines.entries()) {
55
+ try {
56
+ const logEntry = JSON.parse(line);
57
+
58
+ if (logEntry.data === 'variables' && logEntry.testomatioEnvVars) {
59
+ Object.assign(envVars, logEntry.testomatioEnvVars);
60
+ } else if (logEntry.action === 'createRun') {
61
+ runParams = logEntry.params || {};
62
+ } else if (logEntry.action === 'addTestsBatch' && logEntry.tests) {
63
+ // Extract runId if available
64
+ if (logEntry.runId && !runId) {
65
+ runId = logEntry.runId;
66
+ }
67
+ // Process each test in the batch
68
+ for (const test of logEntry.tests) {
69
+ if (test.rid) {
70
+ // Handle tests with rid (deduplicate)
71
+ const existingTest = testsMap.get(test.rid);
72
+ if (existingTest) {
73
+ // Merge test data - prioritize non-null/non-empty values
74
+ const mergedTest = { ...existingTest };
75
+ Object.keys(test).forEach(key => {
76
+ if (test[key] !== null && test[key] !== undefined) {
77
+ if (key === 'files' && Array.isArray(test[key]) && test[key].length > 0) {
78
+ // Merge files arrays
79
+ mergedTest.files = [...(existingTest.files || []), ...test[key]];
80
+ } else if (key === 'artifacts' && Array.isArray(test[key]) && test[key].length > 0) {
81
+ // Merge artifacts arrays
82
+ mergedTest.artifacts = [...(existingTest.artifacts || []), ...test[key]];
83
+ } else if (
84
+ existingTest[key] === null ||
85
+ existingTest[key] === undefined ||
86
+ (Array.isArray(existingTest[key]) && existingTest[key].length === 0)
87
+ ) {
88
+ // Use new value if existing is null/undefined/empty array
89
+ mergedTest[key] = test[key];
90
+ }
91
+ }
92
+ });
93
+ testsMap.set(test.rid, mergedTest);
94
+ } else {
95
+ testsMap.set(test.rid, { ...test });
96
+ }
97
+ } else {
98
+ // Handle tests without rid (no deduplication)
99
+ testsWithoutRid.push({ ...test });
100
+ }
101
+ }
102
+ } else if (logEntry.action === 'addTest' && logEntry.testId) {
103
+ // Extract runId if available
104
+ if (logEntry.runId && !runId) {
105
+ runId = logEntry.runId;
106
+ }
107
+ const test = logEntry.testId;
108
+ if (test.rid) {
109
+ // Handle tests with rid (deduplicate)
110
+ const existingTest = testsMap.get(test.rid);
111
+ if (existingTest) {
112
+ // Merge with existing test
113
+ const mergedTest = { ...existingTest, ...test };
114
+ testsMap.set(test.rid, mergedTest);
115
+ } else {
116
+ testsMap.set(test.rid, { ...test });
117
+ }
118
+ } else {
119
+ // Handle tests without rid (no deduplication)
120
+ testsWithoutRid.push({ ...test });
121
+ }
122
+ } else if (logEntry.actions === 'finishRun') {
123
+ finishParams = logEntry.params || {};
124
+ }
125
+ } catch (err) {
126
+ parseErrors++;
127
+ if (parseErrors <= 3) {
128
+ // Only show first 3 parse errors
129
+ this.onError(`Failed to parse line ${lineIndex + 1}: ${line.substring(0, 100)}...`);
130
+ }
131
+ }
132
+ }
133
+
134
+ if (parseErrors > 3) {
135
+ this.onError(`${parseErrors - 3} more parse errors occurred`);
136
+ }
137
+
138
+ // Combine tests with rid and tests without rid
139
+ const allTests = [...Array.from(testsMap.values()), ...testsWithoutRid];
140
+
141
+ return {
142
+ runParams,
143
+ finishParams,
144
+ tests: allTests,
145
+ envVars,
146
+ parseErrors,
147
+ totalLines: lines.length,
148
+ runId,
149
+ };
150
+ }
151
+
152
+ /**
153
+ * Restore environment variables from debug data
154
+ * @param {Object} envVars - Environment variables to restore
155
+ */
156
+ restoreEnvironmentVariables(envVars) {
157
+ // Only restore env vars that aren't already set (don't override current values)
158
+ Object.keys(envVars).forEach(key => {
159
+ if (process.env[key] === undefined || process.env[key] === '') {
160
+ process.env[key] = envVars[key];
161
+ }
162
+ });
163
+ }
164
+
165
+ /**
166
+ * Replay test data to Testomat.io
167
+ * @param {string} debugFile - Path to debug file (optional, uses default if not provided)
168
+ * @returns {Promise<Object>} Replay results
169
+ */
170
+ async replay(debugFile) {
171
+ if (!debugFile) {
172
+ debugFile = this.getDefaultDebugFile();
173
+ }
174
+
175
+ if (!this.apiKey) {
176
+ throw new Error('TESTOMATIO API key not found. Set TESTOMATIO environment variable.');
177
+ }
178
+
179
+ this.onLog(`Replaying data from debug file: ${debugFile}`);
180
+
181
+ // Parse the debug file
182
+ const debugData = this.parseDebugFile(debugFile);
183
+ const { runParams, finishParams, tests, envVars, runId } = debugData;
184
+
185
+ this.onLog(`Found ${tests.length} tests to replay`);
186
+
187
+ if (tests.length === 0) {
188
+ throw new Error('No test data found in debug file');
189
+ }
190
+
191
+ // Restore environment variables
192
+ this.restoreEnvironmentVariables(envVars);
193
+
194
+ if (this.dryRun) {
195
+ return {
196
+ success: true,
197
+ testsCount: tests.length,
198
+ runParams,
199
+ finishParams,
200
+ envVars,
201
+ runId,
202
+ dryRun: true,
203
+ };
204
+ }
205
+
206
+ // Create client and restore the run
207
+ const client = new TestomatClient({
208
+ apiKey: this.apiKey,
209
+ isBatchEnabled: true,
210
+ ...runParams,
211
+ });
212
+
213
+ // Use the stored runId if available, otherwise create a new run
214
+ if (runId) {
215
+ this.onLog(`Using existing run ID: ${runId}`);
216
+ client.runId = runId;
217
+ } else {
218
+ this.onLog('Publishing to run...');
219
+ await client.createRun(runParams);
220
+ }
221
+
222
+ // Send each test result
223
+ let successCount = 0;
224
+ let failureCount = 0;
225
+
226
+ for (const [index, test] of tests.entries()) {
227
+ try {
228
+ await client.addTestRun(test.status, { ...test, overwrite: true });
229
+ successCount++;
230
+ this.onProgress({
231
+ current: index + 1,
232
+ total: tests.length,
233
+ test,
234
+ success: true,
235
+ });
236
+ } catch (err) {
237
+ failureCount++;
238
+ this.onError(`Failed to send test ${index + 1}: ${err.message}`);
239
+ this.onProgress({
240
+ current: index + 1,
241
+ total: tests.length,
242
+ test,
243
+ success: false,
244
+ error: err.message,
245
+ });
246
+ }
247
+ }
248
+
249
+ await client.updateRunStatus(finishParams.status || STATUS.FINISHED, finishParams.parallel || false);
250
+
251
+ const result = {
252
+ success: true,
253
+ testsCount: tests.length,
254
+ successCount,
255
+ failureCount,
256
+ runParams,
257
+ finishParams,
258
+ envVars,
259
+ runId: runId || client.runId,
260
+ };
261
+
262
+ this.onLog(`Successfully replayed ${successCount}/${tests.length} tests from debug file`);
263
+
264
+ return result;
265
+ }
266
+ }
267
+
268
+ export default Replay;
@@ -47,9 +47,50 @@ function setKeyValue(keyValue, value = null) {
47
47
  services.keyValues.put(keyValue);
48
48
  }
49
49
 
50
+ /**
51
+ * Add a single label to the test report
52
+ * @param {string} key - label key (e.g. 'severity', 'feature', or just 'smoke' for labels without values)
53
+ * @param {string} [value] - optional label value (e.g. 'high', 'login')
54
+ */
55
+ function setLabel(key, value = null) {
56
+ if (!key || typeof key !== 'string') {
57
+ console.warn('Label key must be a non-empty string');
58
+ return;
59
+ }
60
+
61
+ // Limit key length to 255 characters
62
+ if (key.length > 255) {
63
+ console.warn('Label key is too long, trimmed to 255 characters:', key);
64
+ key = key.substring(0, 255);
65
+ }
66
+
67
+ let labelString = key;
68
+ if (value !== null && value !== undefined && value !== '') {
69
+ if (typeof value !== 'string') {
70
+ console.warn('Label value must be a string, converting:', value);
71
+ value = String(value);
72
+ }
73
+ // Limit value length to 255 characters
74
+ if (value.length > 255) {
75
+ console.warn('Label value is too long, trimmed to 255 characters:', value);
76
+ value = value.substring(0, 255);
77
+ }
78
+ labelString = `${key}:${value}`;
79
+ }
80
+
81
+ // Limit total label length to 255 characters
82
+ if (labelString.length > 255) {
83
+ console.warn('Label is too long, trimmed to 255 characters:', labelString);
84
+ labelString = labelString.substring(0, 255);
85
+ }
86
+
87
+ services.labels.put([labelString]);
88
+ }
89
+
50
90
  export default {
51
91
  artifact: saveArtifact,
52
92
  log: logMessage,
53
93
  step: addStep,
54
94
  keyValue: setKeyValue,
95
+ label: setLabel,
55
96
  };
package/src/reporter.js CHANGED
@@ -8,6 +8,7 @@ export const log = reporterFunctions.log;
8
8
  export const logger = services.logger;
9
9
  export const meta = reporterFunctions.keyValue;
10
10
  export const step = reporterFunctions.step;
11
+ export const label = reporterFunctions.label;
11
12
 
12
13
  /**
13
14
  * @typedef {import('./reporter-functions.js')} artifact
@@ -15,6 +16,7 @@ export const step = reporterFunctions.step;
15
16
  * @typedef {import('./services/index.js')} logger
16
17
  * @typedef {import('./reporter-functions.js')} meta
17
18
  * @typedef {import('./reporter-functions.js')} step
19
+ * @typedef {import('./reporter-functions.js')} label
18
20
  */
19
21
  export default {
20
22
  /**
@@ -27,6 +29,7 @@ export default {
27
29
  logger: services.logger,
28
30
  meta: reporterFunctions.keyValue,
29
31
  step: reporterFunctions.step,
32
+ label: reporterFunctions.label,
30
33
 
31
34
  // TestomatClient,
32
35
  // TRConstants,
@@ -1,12 +1,14 @@
1
1
  import { logger } from './logger.js';
2
2
  import { artifactStorage } from './artifacts.js';
3
3
  import { keyValueStorage } from './key-values.js';
4
+ import { labelStorage } from './labels.js';
4
5
  import { dataStorage } from '../data-storage.js';
5
6
 
6
7
  export const services = {
7
8
  logger,
8
9
  artifacts: artifactStorage,
9
10
  keyValues: keyValueStorage,
11
+ labels: labelStorage,
10
12
  setContext: context => {
11
13
  dataStorage.setContext(context);
12
14
  },
@@ -0,0 +1,59 @@
1
+ import createDebugMessages from 'debug';
2
+ import { dataStorage } from '../data-storage.js';
3
+
4
+ const debug = createDebugMessages('@testomatio/reporter:services-labels');
5
+ class LabelStorage {
6
+ static #instance;
7
+
8
+ /**
9
+ *
10
+ * @returns {LabelStorage}
11
+ */
12
+ static getInstance() {
13
+ if (!this.#instance) {
14
+ this.#instance = new LabelStorage();
15
+ }
16
+ return this.#instance;
17
+ }
18
+
19
+ /**
20
+ * Stores labels array and passes it to reporter
21
+ * @param {string[]} labels - array of label strings
22
+ * @param {*} context - full test title
23
+ */
24
+ put(labels, context = null) {
25
+ if (!labels || !Array.isArray(labels)) return;
26
+ dataStorage.putData('labels', labels, context);
27
+ }
28
+
29
+ /**
30
+ * Returns labels array for the test
31
+ * @param {*} context testId or test context from test runner
32
+ * @returns {string[]} labels array, e.g. ['smoke', 'severity:high', 'feature:user_account']
33
+ */
34
+ get(context = null) {
35
+ const labelsList = dataStorage.getData('labels', context);
36
+ if (!labelsList || !labelsList?.length) return [];
37
+
38
+ const allLabels = [];
39
+ for (const labels of labelsList) {
40
+ if (Array.isArray(labels)) {
41
+ allLabels.push(...labels);
42
+ } else if (typeof labels === 'string') {
43
+ try {
44
+ const parsedLabels = JSON.parse(labels);
45
+ if (Array.isArray(parsedLabels)) {
46
+ allLabels.push(...parsedLabels);
47
+ }
48
+ } catch (e) {
49
+ debug(`Error parsing labels for test ${context}`, labels);
50
+ }
51
+ }
52
+ }
53
+
54
+ // Remove duplicates
55
+ return [...new Set(allLabels)];
56
+ }
57
+ }
58
+
59
+ export const labelStorage = LabelStorage.getInstance();
@@ -93,7 +93,6 @@ class Logger {
93
93
  logs.push(arg.join(' '));
94
94
  } else {
95
95
  try {
96
- // eslint-disable-next-line no-unused-expressions
97
96
  this.prettyObjects ? logs.push(JSON.stringify(arg, null, 2)) : logs.push(JSON.stringify(arg));
98
97
  } catch (e) {
99
98
  debug('Error while stringify object', e);
@@ -123,7 +122,7 @@ class Logger {
123
122
  current +
124
123
  // strings are splitted by args when use tagged template, thus we add arg after each string
125
124
  // it looks like: `string1 arg1 string2 arg2 string3`
126
- (args[index] !== undefined // eslint-disable-line no-nested-ternary
125
+ (args[index] !== undefined
127
126
  ? typeof args[index] === 'string'
128
127
  ? args[index] // add arg as it is
129
128
  : this.#stringifyLogs(args[index]) // stringify arg