@testomatio/reporter 2.6.0 → 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.
- package/README.md +7 -7
- package/lib/adapter/playwright.d.ts +2 -12
- package/lib/adapter/playwright.js +15 -72
- package/lib/adapter/utils/playwright.d.ts +25 -0
- package/lib/adapter/utils/playwright.js +123 -0
- package/lib/adapter/vitest.js +2 -1
- package/lib/bin/cli.js +1 -1
- package/lib/data-storage.js +1 -0
- package/lib/pipe/html.d.ts +2 -3
- package/lib/pipe/html.js +745 -37
- package/lib/pipe/testomatio.js +12 -1
- package/lib/reporter-functions.d.ts +36 -11
- package/lib/reporter-functions.js +67 -25
- package/lib/reporter.d.ts +12 -8
- package/lib/template/testomatio-old.hbs +1421 -0
- package/lib/template/testomatio.hbs +3200 -1157
- package/lib/utils/log-formatter.js +8 -4
- package/package.json +2 -2
- package/src/adapter/playwright.js +15 -79
- package/src/adapter/utils/playwright.js +121 -0
- package/src/adapter/vitest.js +2 -1
- package/src/bin/cli.js +1 -1
- package/src/data-storage.js +1 -0
- package/src/pipe/html.js +844 -38
- package/src/pipe/testomatio.js +12 -1
- package/src/reporter-functions.js +68 -28
- package/src/template/testomatio-old.hbs +1421 -0
- package/src/template/testomatio.hbs +3200 -1157
- package/src/utils/log-formatter.js +9 -4
- package/types/types.d.ts +29 -5
- package/lib/services/labels.d.ts +0 -0
- package/lib/services/labels.js +0 -0
- package/src/services/labels.js +0 -1
|
@@ -114,10 +114,14 @@ function formatError(error, message) {
|
|
|
114
114
|
* @returns {boolean}
|
|
115
115
|
*/
|
|
116
116
|
function isNotInternalFrame(frame) {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@testomatio/reporter",
|
|
3
|
-
"version": "2.6.
|
|
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": "^
|
|
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 createDebugMessages from 'debug';
|
|
2
1
|
import crypto from 'crypto';
|
|
3
2
|
import os from 'os';
|
|
4
3
|
import path from 'path';
|
|
@@ -11,9 +10,9 @@ import { services } from '../services/index.js';
|
|
|
11
10
|
import { dataStorage } from '../data-storage.js';
|
|
12
11
|
import { extensionMap } from '../utils/constants.js';
|
|
13
12
|
import pc from 'picocolors';
|
|
13
|
+
import { fetchLinksFromLogs } from './utils/playwright.js';
|
|
14
14
|
|
|
15
15
|
const reportTestPromises = [];
|
|
16
|
-
const debug = createDebugMessages('@testomatio/reporter:adapter-playwright');
|
|
17
16
|
|
|
18
17
|
class PlaywrightReporter {
|
|
19
18
|
constructor(config = {}) {
|
|
@@ -43,6 +42,16 @@ class PlaywrightReporter {
|
|
|
43
42
|
|
|
44
43
|
const { title } = test;
|
|
45
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
|
+
|
|
46
55
|
const suite_title = test.parent ? test.parent?.title : path.basename(test?.location?.file);
|
|
47
56
|
|
|
48
57
|
const steps = [];
|
|
@@ -60,7 +69,7 @@ class PlaywrightReporter {
|
|
|
60
69
|
|
|
61
70
|
let logs = '';
|
|
62
71
|
// get links along with filtered logs (liks related logs removed)
|
|
63
|
-
const { stdout: filteredStdout, links } = fetchLinksFromLogs(result.stdout);
|
|
72
|
+
const { stdout: filteredStdout, links, meta } = fetchLinksFromLogs(result.stdout);
|
|
64
73
|
if (filteredStdout?.length || result.stderr?.length) {
|
|
65
74
|
logs = `\n\n${pc.bold('Logs:')}\n${pc.red(result.stderr.join(''))}\n${filteredStdout.join('')}`;
|
|
66
75
|
}
|
|
@@ -117,12 +126,14 @@ class PlaywrightReporter {
|
|
|
117
126
|
logs,
|
|
118
127
|
links,
|
|
119
128
|
manuallyAttachedArtifacts,
|
|
129
|
+
files: files.length ? files : undefined,
|
|
120
130
|
meta: {
|
|
121
131
|
browser: project.browser,
|
|
122
132
|
isMobile: project.isMobile,
|
|
123
133
|
project: project.name,
|
|
124
134
|
projectDependencies: project.dependencies?.length ? project.dependencies : null,
|
|
125
135
|
...testMeta,
|
|
136
|
+
...meta,
|
|
126
137
|
...project.metadata, // metadata has any type (in playwright), but we will stringify it in client.js
|
|
127
138
|
...test.annotations?.reduce((acc, annotation) => {
|
|
128
139
|
acc[annotation.type] = annotation.description;
|
|
@@ -135,7 +146,7 @@ class PlaywrightReporter {
|
|
|
135
146
|
this.uploads.push({
|
|
136
147
|
rid: `${rid}-${project.name}`,
|
|
137
148
|
title: test.title,
|
|
138
|
-
files:
|
|
149
|
+
files: pwAttachments,
|
|
139
150
|
file: test.location?.file,
|
|
140
151
|
});
|
|
141
152
|
// remove empty uploads
|
|
@@ -303,80 +314,5 @@ function getTestContextName(test) {
|
|
|
303
314
|
return `${test._requireFile || ''}_${test.title}`;
|
|
304
315
|
}
|
|
305
316
|
|
|
306
|
-
/**
|
|
307
|
-
* Fetches links from stdout. Returns links and filtered stdout (without data containing markers)
|
|
308
|
-
*
|
|
309
|
-
* @param {(string | Buffer)[]} stdout
|
|
310
|
-
* @returns {{ links: { [key: 'test' | 'jira']: string }[], stdout: (string | Buffer)[] }}
|
|
311
|
-
*/
|
|
312
|
-
function fetchLinksFromLogs(stdout) {
|
|
313
|
-
const links = [];
|
|
314
|
-
|
|
315
|
-
const markers = [
|
|
316
|
-
{ key: '[TESTOMATIO-LINK-TESTS]', type: 'test' },
|
|
317
|
-
{ key: '[TESTOMATIO-LINK-JIRA]', type: 'jira' },
|
|
318
|
-
];
|
|
319
|
-
|
|
320
|
-
const filteredStdout = [];
|
|
321
|
-
|
|
322
|
-
stdout.forEach(entry => {
|
|
323
|
-
if (typeof entry !== 'string') {
|
|
324
|
-
filteredStdout.push(entry);
|
|
325
|
-
return;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
// check if entry contains any of markers
|
|
329
|
-
if (!markers.some(m => entry.includes(m.key))) {
|
|
330
|
-
filteredStdout.push(entry);
|
|
331
|
-
return;
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
const newEntryLines = [];
|
|
335
|
-
entry.split('\n').forEach(line => {
|
|
336
|
-
line = line.trim();
|
|
337
|
-
let hasMarker = false;
|
|
338
|
-
for (const marker of markers) {
|
|
339
|
-
if (line.includes(marker.key)) {
|
|
340
|
-
hasMarker = true;
|
|
341
|
-
try {
|
|
342
|
-
const rawJson = line.split(marker.key)[1]?.trim();
|
|
343
|
-
if (!rawJson) continue;
|
|
344
|
-
|
|
345
|
-
// smart JSON extraction: take until the last ']', otherwise take the whole string
|
|
346
|
-
const lastBracketIndex = rawJson.lastIndexOf(']');
|
|
347
|
-
const jsonStr = lastBracketIndex !== -1 ? rawJson.substring(0, lastBracketIndex + 1) : rawJson;
|
|
348
|
-
|
|
349
|
-
// test ids or jira ids
|
|
350
|
-
const ids = JSON.parse(jsonStr);
|
|
351
|
-
links.push(
|
|
352
|
-
...ids
|
|
353
|
-
// filter non-truthy ids
|
|
354
|
-
.filter(id => !!id)
|
|
355
|
-
.map(id => ({
|
|
356
|
-
// marker type is either 'test' or 'jira'
|
|
357
|
-
[marker.type]: id,
|
|
358
|
-
})),
|
|
359
|
-
);
|
|
360
|
-
} catch (e) {
|
|
361
|
-
debug('Error parsing links from string:', line, '\n', e);
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
if (!hasMarker && line) {
|
|
366
|
-
newEntryLines.push(line);
|
|
367
|
-
}
|
|
368
|
-
});
|
|
369
|
-
|
|
370
|
-
if (newEntryLines.length) {
|
|
371
|
-
filteredStdout.push(newEntryLines.join('\n'));
|
|
372
|
-
}
|
|
373
|
-
});
|
|
374
|
-
|
|
375
|
-
return {
|
|
376
|
-
stdout: filteredStdout,
|
|
377
|
-
links,
|
|
378
|
-
};
|
|
379
|
-
}
|
|
380
|
-
|
|
381
317
|
export default PlaywrightReporter;
|
|
382
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
|
+
}
|
package/src/adapter/vitest.js
CHANGED
|
@@ -101,7 +101,7 @@ class VitestReporter {
|
|
|
101
101
|
*
|
|
102
102
|
* @param {VitestTest} test
|
|
103
103
|
*
|
|
104
|
-
* @returns {TestData & {status:
|
|
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
|
@@ -15,7 +15,7 @@ import { filesize as prettyBytes } from 'filesize';
|
|
|
15
15
|
import dotenv from 'dotenv';
|
|
16
16
|
import Replay from '../replay.js';
|
|
17
17
|
|
|
18
|
-
const debug = createDebugMessages('@testomatio/reporter:
|
|
18
|
+
const debug = createDebugMessages('@testomatio/reporter:cli');
|
|
19
19
|
const version = getPackageVersion();
|
|
20
20
|
console.log(pc.cyan(pc.bold(` 🤩 Testomat.io Reporter v${version}`)));
|
|
21
21
|
const program = new Command();
|
package/src/data-storage.js
CHANGED
|
@@ -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`);
|