testomatio-reporter-cli 2.8.4 → 2.8.5-beta.2-yarn
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/README.md +3 -3
- package/bin/cli.js +6 -26
- package/package.json +39 -4
- package/src/adapter/codecept.js +626 -0
- package/src/adapter/cucumber/current.js +230 -0
- package/src/adapter/cucumber/legacy.js +158 -0
- package/src/adapter/cucumber.js +4 -0
- package/src/adapter/cypress-plugin/index.js +110 -0
- package/src/adapter/jasmine.js +60 -0
- package/src/adapter/jest.js +108 -0
- package/src/adapter/mocha.cjs +2 -0
- package/src/adapter/mocha.js +211 -0
- package/src/adapter/nightwatch.js +88 -0
- package/src/adapter/playwright.js +343 -0
- package/src/adapter/utils/playwright.js +121 -0
- package/src/adapter/utils/step-formatter.js +232 -0
- package/src/adapter/vitest.js +455 -0
- package/src/adapter/webdriver.js +201 -0
- package/src/bin/cli.js +507 -0
- package/src/bin/reportXml.js +79 -0
- package/src/bin/startTest.js +54 -0
- package/src/bin/uploadArtifacts.js +91 -0
- package/src/client.js +524 -0
- package/src/config.js +30 -0
- package/src/constants.js +72 -0
- package/src/data-storage.js +204 -0
- package/src/helpers.js +1 -0
- package/src/junit-adapter/adapter.js +23 -0
- package/src/junit-adapter/csharp.js +70 -0
- package/src/junit-adapter/index.js +28 -0
- package/src/junit-adapter/java.js +58 -0
- package/src/junit-adapter/javascript.js +31 -0
- package/src/junit-adapter/nunit-parser.js +474 -0
- package/src/junit-adapter/python.js +42 -0
- package/src/junit-adapter/ruby.js +10 -0
- package/src/output.js +57 -0
- package/src/pipe/bitbucket.js +285 -0
- package/src/pipe/coverage.js +500 -0
- package/src/pipe/csv.js +161 -0
- package/src/pipe/debug.js +143 -0
- package/src/pipe/github.js +256 -0
- package/src/pipe/gitlab.js +258 -0
- package/src/pipe/html.js +1153 -0
- package/src/pipe/index.js +73 -0
- package/src/pipe/markdown.js +753 -0
- package/src/pipe/testomatio.js +707 -0
- package/src/replay.js +274 -0
- package/src/reporter-functions.js +155 -0
- package/src/reporter.js +42 -0
- package/src/services/artifacts.js +59 -0
- package/src/services/index.js +15 -0
- package/src/services/key-values.js +59 -0
- package/src/services/links.js +69 -0
- package/src/services/logger.js +320 -0
- package/src/template/emptyData.svg +23 -0
- package/src/template/testomatio-old.hbs +1421 -0
- package/src/template/testomatio.hbs +3726 -0
- package/src/uploader.js +382 -0
- package/src/utils/constants.js +12 -0
- package/src/utils/debug.js +20 -0
- package/src/utils/log-formatter.js +118 -0
- package/src/utils/log.js +88 -0
- package/src/utils/pipe_utils.js +193 -0
- package/src/utils/utils.js +732 -0
- package/src/xmlReader.js +834 -0
- package/types/types.d.ts +425 -0
- package/types/vitest.types.d.ts +93 -0
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { truncate } from '../../utils/utils.js';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import crypto from 'crypto';
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Generates a short unique filename from screenshot path
|
|
8
|
+
* If original filename is too long, uses hash-based name
|
|
9
|
+
*
|
|
10
|
+
* @param {string} screenshotPath - Path to screenshot file
|
|
11
|
+
* @returns {string} Short filename (max 80 chars)
|
|
12
|
+
*/
|
|
13
|
+
export function generateShortFilename(screenshotPath) {
|
|
14
|
+
const originalFilename = path.basename(screenshotPath);
|
|
15
|
+
const stepPrefix = originalFilename.match(/^(\d{3,4}_)/)?.[1] || '';
|
|
16
|
+
|
|
17
|
+
if (originalFilename.length < 40) {
|
|
18
|
+
return originalFilename;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const ext = path.extname(screenshotPath);
|
|
22
|
+
|
|
23
|
+
const hash = crypto
|
|
24
|
+
.createHash('sha256')
|
|
25
|
+
.update(screenshotPath)
|
|
26
|
+
.digest('hex')
|
|
27
|
+
.slice(0, 16);
|
|
28
|
+
|
|
29
|
+
return `${stepPrefix}screenshot_${hash}${ext}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Formats a step object according to Testomat.io Step Schema
|
|
34
|
+
*
|
|
35
|
+
* This function transforms a raw step object from test frameworks (CodeceptJS, Playwright, etc.)
|
|
36
|
+
* into a standardized format compatible with Testomat.io API. It ensures all text fields are
|
|
37
|
+
* truncated to 250 characters as defined in testomat-api-definition.yml.
|
|
38
|
+
*
|
|
39
|
+
* Processed fields:
|
|
40
|
+
* - category: step type (framework, user, hook) - defaults to 'user'
|
|
41
|
+
* - title: step name/description, truncated to 250 chars
|
|
42
|
+
* - duration: step execution time in seconds
|
|
43
|
+
* - log: optional log output, truncated to 250 chars
|
|
44
|
+
* - artifacts: optional array of artifact URLs (screenshots), each truncated to 250 chars
|
|
45
|
+
* - error: error details (message + stack) if step failed, each truncated to 250 chars
|
|
46
|
+
* - steps: recursively formats nested steps
|
|
47
|
+
*
|
|
48
|
+
* Schema reference: testomat-api-definition.yml (Step object)
|
|
49
|
+
*
|
|
50
|
+
* @param {Object} step - Raw step object from test framework
|
|
51
|
+
* @param {string} [step.category] - Step category: 'user', 'framework', or 'hook'
|
|
52
|
+
* @param {string} [step.title] - Step title/name
|
|
53
|
+
* @param {number} [step.duration] - Step duration in seconds
|
|
54
|
+
* @param {string} [step.log] - Log output for this step
|
|
55
|
+
* @param {string[]} [step.artifacts] - Array of artifact URLs (screenshots)
|
|
56
|
+
* @param {string|Object} [step.error] - Error details - can be string or object with message/stack
|
|
57
|
+
* @param {Object[]} [step.steps] - Array of nested child steps
|
|
58
|
+
* @returns {Object} Formatted step object matching Testomat.io Step Schema with:
|
|
59
|
+
* category, title, duration, and optional log, artifacts, error, and steps fields
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* const rawStep = {
|
|
63
|
+
* category: 'user',
|
|
64
|
+
* title: 'I click on button',
|
|
65
|
+
* duration: 1.5,
|
|
66
|
+
* error: { message: 'Element not found', stack: 'at test.js:10:5' }
|
|
67
|
+
* };
|
|
68
|
+
* const formatted = formatStep(rawStep);
|
|
69
|
+
* // Returns: { category: 'user', title: 'I click on button', duration: 1.5, error: {...} }
|
|
70
|
+
*/
|
|
71
|
+
export function formatStep(step) {
|
|
72
|
+
const formattedStep = {
|
|
73
|
+
category: step.category || 'user',
|
|
74
|
+
title: truncate(String(step.title || ''), 250),
|
|
75
|
+
duration: step.duration || 0,
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
if (step.log) {
|
|
79
|
+
formattedStep.log = truncate(String(step.log), 250);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (step.artifacts && Array.isArray(step.artifacts)) {
|
|
83
|
+
formattedStep.artifacts = step.artifacts.map(artifact => truncate(String(artifact), 250));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (step.error) {
|
|
87
|
+
if (typeof step.error === 'object') {
|
|
88
|
+
formattedStep.error = {
|
|
89
|
+
message: truncate(String(step.error.message || 'Step failed'), 250),
|
|
90
|
+
stack: truncate(String(step.error.stack || ''), 250),
|
|
91
|
+
};
|
|
92
|
+
} else {
|
|
93
|
+
formattedStep.error = truncate(String(step.error), 250);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (step.steps && Array.isArray(step.steps)) {
|
|
98
|
+
formattedStep.steps = step.steps.map(s => formatStep(s));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return formattedStep;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Adds status field to step
|
|
106
|
+
*
|
|
107
|
+
* Normalizes step status from test frameworks to Testomat.io standard format.
|
|
108
|
+
* Maps framework-specific statuses ('success', 'failed', 'passed') to Testomat.io
|
|
109
|
+
* standard values ('passed', 'failed').
|
|
110
|
+
*
|
|
111
|
+
* Status mapping:
|
|
112
|
+
* - 'success' → 'passed'
|
|
113
|
+
* - 'passed' → 'passed'
|
|
114
|
+
* - 'failed' → 'failed'
|
|
115
|
+
* - Any other value → 'passed' (default)
|
|
116
|
+
*
|
|
117
|
+
* If step already has a status, it won't be overwritten. If error is provided
|
|
118
|
+
* and step doesn't have status, it will be set to 'failed'.
|
|
119
|
+
*
|
|
120
|
+
* Schema reference: testomat-api-definition.yml (Step.status enum)
|
|
121
|
+
*
|
|
122
|
+
* @param {Object} step - Step object to add status to (modified in place)
|
|
123
|
+
* @param {string} [step.status] - Existing status (won't be overwritten if present)
|
|
124
|
+
* @param {string} status - Status from test framework: 'success', 'failed', or 'passed'
|
|
125
|
+
* @param {Error|Object|null} err - Error object if step failed
|
|
126
|
+
* @returns {Object} The same step object with added status field
|
|
127
|
+
*
|
|
128
|
+
* @example
|
|
129
|
+
* const step = { title: 'Click button' };
|
|
130
|
+
* addStatusToStep(step, 'success', null);
|
|
131
|
+
* // step.status === 'passed'
|
|
132
|
+
*
|
|
133
|
+
* @example
|
|
134
|
+
* const step2 = { title: 'Find element' };
|
|
135
|
+
* addStatusToStep(step2, 'failed', new Error('Not found'));
|
|
136
|
+
* // step2.status === 'failed'
|
|
137
|
+
*/
|
|
138
|
+
export function addStatusToStep(step, status, err) {
|
|
139
|
+
if (step.status) return step;
|
|
140
|
+
|
|
141
|
+
const statusMap = {
|
|
142
|
+
'success': 'passed',
|
|
143
|
+
'failed': 'failed',
|
|
144
|
+
'passed': 'passed',
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
step.status = statusMap[status] || 'passed';
|
|
148
|
+
|
|
149
|
+
if (err && !step.status) {
|
|
150
|
+
step.status = 'failed';
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return step;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Adds screenshot to step as artifacts array
|
|
158
|
+
*
|
|
159
|
+
* Extracts screenshot path from artifacts and adds it to the step's artifacts array.
|
|
160
|
+
* The actual upload will happen in the client's addTestRun method.
|
|
161
|
+
*
|
|
162
|
+
* Artifact format supports:
|
|
163
|
+
* - Array format: [{ screenshot: '/path/to/screenshot.png' }]
|
|
164
|
+
* - Object format: { screenshot: '/path/to/screenshot.png' }
|
|
165
|
+
*
|
|
166
|
+
* Screenshot path can be specified as:
|
|
167
|
+
* - Object with path property: { screenshot: { path: '/path/to/file.png' } }
|
|
168
|
+
* - Object with screenshot property: { screenshot: { screenshot: '/path/to/file.png' } }
|
|
169
|
+
* - Direct string path: { screenshot: '/path/to/file.png' }
|
|
170
|
+
*
|
|
171
|
+
* @param {Object} step - Step object to add artifacts to (modified in place)
|
|
172
|
+
* @param {string[]} [step.artifacts] - Existing artifacts array (won't be overwritten if present)
|
|
173
|
+
* @param {Object|Object[]|null} artifacts - Artifacts from test framework
|
|
174
|
+
* @returns {Object} The same step object with artifacts array added
|
|
175
|
+
*
|
|
176
|
+
* @example
|
|
177
|
+
* const step = { title: 'Click button' };
|
|
178
|
+
* const artifacts = { screenshot: '/tmp/screenshot.png' };
|
|
179
|
+
* addArtifactsToStep(step, artifacts);
|
|
180
|
+
* // step.artifacts === ['/tmp/screenshot.png']
|
|
181
|
+
*/
|
|
182
|
+
export function addArtifactsToStep(step, artifacts) {
|
|
183
|
+
if (!artifacts) return step;
|
|
184
|
+
|
|
185
|
+
let screenshotPath = null;
|
|
186
|
+
|
|
187
|
+
if (Array.isArray(artifacts)) {
|
|
188
|
+
const screenshotArtifact = artifacts.find(a => a.screenshot);
|
|
189
|
+
if (screenshotArtifact && screenshotArtifact.path) {
|
|
190
|
+
screenshotPath = screenshotArtifact.path;
|
|
191
|
+
} else if (screenshotArtifact && screenshotArtifact.screenshot) {
|
|
192
|
+
screenshotPath = screenshotArtifact.screenshot;
|
|
193
|
+
}
|
|
194
|
+
} else if (artifacts.screenshot) {
|
|
195
|
+
screenshotPath = artifacts.screenshot;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (screenshotPath && fs.existsSync(screenshotPath)) {
|
|
199
|
+
const truncatedPath = truncate(String(screenshotPath), 250);
|
|
200
|
+
if (step.artifacts && Array.isArray(step.artifacts)) {
|
|
201
|
+
step.artifacts.push(truncatedPath);
|
|
202
|
+
} else {
|
|
203
|
+
step.artifacts = [truncatedPath];
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return step;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Appends one artifact path to a step.
|
|
212
|
+
*
|
|
213
|
+
* Unlike addArtifactsToStep, this helper accepts a direct path (or URL-like string)
|
|
214
|
+
* and does not check file existence, so callers can attach fallback artifacts
|
|
215
|
+
* collected from logs or async trace outputs.
|
|
216
|
+
*
|
|
217
|
+
* @param {Object} step - Step object to update (modified in place)
|
|
218
|
+
* @param {string} artifactPath - Artifact path to append
|
|
219
|
+
* @returns {Object} The same step object with updated artifacts
|
|
220
|
+
*/
|
|
221
|
+
export function addArtifactPathToStep(step, artifactPath) {
|
|
222
|
+
if (!step || !artifactPath) return step;
|
|
223
|
+
|
|
224
|
+
const truncatedPath = truncate(String(artifactPath), 250);
|
|
225
|
+
if (step.artifacts && Array.isArray(step.artifacts)) {
|
|
226
|
+
if (!step.artifacts.includes(truncatedPath)) step.artifacts.push(truncatedPath);
|
|
227
|
+
} else {
|
|
228
|
+
step.artifacts = [truncatedPath];
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return step;
|
|
232
|
+
}
|