reslop 0.2.4 → 0.2.6
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/CHANGELOG.md +25 -1
- package/LICENSE +1 -1
- package/README.md +12 -13
- package/bin/reslop.js +3 -8
- package/lib/ansi.js +32 -19
- package/lib/capabilities.js +8 -12
- package/lib/cli.js +40 -74
- package/lib/clipboard.js +48 -18
- package/lib/dashboard/git.js +128 -0
- package/lib/dashboard/model.js +332 -0
- package/lib/dashboard/npm.js +193 -0
- package/lib/dashboard/tree.js +278 -0
- package/lib/deps-diff.js +196 -404
- package/lib/deps.js +240 -399
- package/lib/detect.js +8 -18
- package/lib/diff/align.js +41 -113
- package/lib/diff/diff.js +12 -43
- package/lib/diff/display.js +25 -36
- package/lib/diff/edit.js +6 -20
- package/lib/diff/inline.js +138 -141
- package/lib/diff/items.js +33 -47
- package/lib/diff/parse.js +28 -56
- package/lib/diff/patch.js +12 -33
- package/lib/diff/unit.js +90 -206
- package/lib/editor.js +98 -55
- package/lib/files.js +86 -103
- package/lib/git-branches.js +254 -416
- package/lib/git-deps.js +182 -344
- package/lib/git-reword-todo.js +5 -13
- package/lib/git-worktree.js +141 -212
- package/lib/git.js +21 -49
- package/lib/github.js +67 -181
- package/lib/gitlab.js +40 -90
- package/lib/highlight/bash.js +55 -177
- package/lib/highlight/core.js +157 -132
- package/lib/highlight/css.js +25 -58
- package/lib/highlight/csv.js +19 -37
- package/lib/highlight/dart.js +72 -213
- package/lib/highlight/dot.js +16 -26
- package/lib/highlight/html.js +42 -63
- package/lib/highlight/js.js +177 -506
- package/lib/highlight/json.js +17 -37
- package/lib/highlight/log.js +22 -35
- package/lib/highlight/txt.js +9 -22
- package/lib/highlight.js +17 -46
- package/lib/keys.js +62 -85
- package/lib/manifest.js +67 -130
- package/lib/npm-commands.js +119 -189
- package/lib/remote.js +141 -197
- package/lib/render/branches.js +66 -73
- package/lib/render/chrome.js +264 -323
- package/lib/render/commits.js +187 -500
- package/lib/render/dash-activity.js +212 -0
- package/lib/render/dash-blocks.js +245 -0
- package/lib/render/dash-table.js +290 -0
- package/lib/render/dashboard.js +62 -0
- package/lib/render/diff.js +123 -228
- package/lib/render/files.js +45 -99
- package/lib/render/frame.js +129 -183
- package/lib/render/notes.js +144 -160
- package/lib/render/npm.js +234 -112
- package/lib/render/present.js +4 -11
- package/lib/render/primitives.js +131 -170
- package/lib/render/render.js +5 -14
- package/lib/render/report.js +43 -0
- package/lib/render/tiles.js +260 -0
- package/lib/render/unit.js +15 -28
- package/lib/report-model.js +110 -33
- package/lib/report-parse.js +56 -38
- package/lib/report-render.js +102 -59
- package/lib/report-run.js +87 -78
- package/lib/report-test.js +256 -141
- package/lib/report-tools.js +96 -161
- package/lib/review-merge.js +114 -152
- package/lib/review.js +115 -189
- package/lib/runs.js +225 -0
- package/lib/select.js +89 -75
- package/lib/session/accessors.js +14 -21
- package/lib/session/actions.js +82 -63
- package/lib/session/branches.js +193 -247
- package/lib/session/change-actions.js +171 -214
- package/lib/session/commit.js +280 -342
- package/lib/session/compose.js +559 -674
- package/lib/session/dashboard.js +385 -0
- package/lib/session/dispatch.js +164 -157
- package/lib/session/edit-click.js +57 -0
- package/lib/session/files-pane.js +85 -53
- package/lib/session/items.js +73 -104
- package/lib/session/lifecycle.js +403 -541
- package/lib/session/load.js +48 -68
- package/lib/session/navigation.js +96 -87
- package/lib/session/npm.js +409 -345
- package/lib/session/ops.js +196 -63
- package/lib/session/pointer.js +23 -15
- package/lib/session/progress.js +33 -21
- package/lib/session/review.js +33 -54
- package/lib/session/tasks.js +333 -0
- package/lib/session/templates.js +51 -59
- package/lib/session/terminal.js +85 -136
- package/lib/session/unit.js +3 -4
- package/lib/session/update.js +76 -97
- package/lib/session/view.js +52 -83
- package/lib/session/watch.js +208 -204
- package/lib/session.js +112 -237
- package/lib/update.js +45 -116
- package/lib/utilities.js +67 -89
- package/lib/wrap.js +33 -44
- package/package.json +1 -1
- package/lib/session/todos.js +0 -293
package/lib/report-test.js
CHANGED
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
const model = require('./report-model.js');
|
|
4
4
|
const npmCommands = require('./npm-commands.js');
|
|
5
5
|
const { isPassingTest } = npmCommands;
|
|
6
|
+
const utilities = require('./utilities.js');
|
|
7
|
+
const { unquote } = utilities;
|
|
6
8
|
|
|
7
|
-
const { summary, problem, report, userTrace } = model;
|
|
9
|
+
const { summary, problem, report, userTrace, errorKey } = model;
|
|
8
10
|
|
|
9
11
|
const KNOWN = [
|
|
10
12
|
'message',
|
|
@@ -20,8 +22,38 @@ const KNOWN = [
|
|
|
20
22
|
'location',
|
|
21
23
|
'severity',
|
|
22
24
|
'type',
|
|
25
|
+
'at',
|
|
26
|
+
'found',
|
|
27
|
+
'wanted',
|
|
28
|
+
'got',
|
|
29
|
+
'expect',
|
|
23
30
|
];
|
|
24
31
|
|
|
32
|
+
const HIDDEN = ['generatedMessage', 'diff'];
|
|
33
|
+
|
|
34
|
+
const ERROR_HEAD = /^([\w.]*Error)(?: \[([^\]]+)\])?:\s*/;
|
|
35
|
+
|
|
36
|
+
const exceptionFields = (lines) => {
|
|
37
|
+
const rows = lines.map((line) => line.replace(/^\s*# ?/, ''));
|
|
38
|
+
const header = rows.findIndex((line) => ERROR_HEAD.test(line.trim()));
|
|
39
|
+
const frame = rows.findIndex((line) => /^\s*at\s/.test(line));
|
|
40
|
+
const start = header < 0 ? 0 : header;
|
|
41
|
+
const end = frame < 0 ? rows.length : frame;
|
|
42
|
+
const text = rows
|
|
43
|
+
.slice(start, end)
|
|
44
|
+
.map((line) => line.trim())
|
|
45
|
+
.join('\n');
|
|
46
|
+
const match = ERROR_HEAD.exec(text);
|
|
47
|
+
const message = match ? text.slice(match[0].length).trim() : text.trim();
|
|
48
|
+
const stack = frame < 0 ? '' : rows.slice(frame).join('\n');
|
|
49
|
+
return [
|
|
50
|
+
['message', message],
|
|
51
|
+
['name', match ? match[1] : ''],
|
|
52
|
+
['code', match ? match[2] || '' : ''],
|
|
53
|
+
['stack', stack],
|
|
54
|
+
];
|
|
55
|
+
};
|
|
56
|
+
|
|
25
57
|
const show = (value) => {
|
|
26
58
|
if (value === null || value === undefined) return '';
|
|
27
59
|
if (typeof value === 'string') return value;
|
|
@@ -34,7 +66,7 @@ const show = (value) => {
|
|
|
34
66
|
const extraFrom = (fields) => {
|
|
35
67
|
const extra = [];
|
|
36
68
|
for (const field of fields) {
|
|
37
|
-
if (KNOWN.includes(field[0])) continue;
|
|
69
|
+
if (KNOWN.includes(field[0]) || HIDDEN.includes(field[0])) continue;
|
|
38
70
|
const value = show(field[1]);
|
|
39
71
|
if (!value) continue;
|
|
40
72
|
const shown = value.length > 500 ? `${value.slice(0, 500)}…` : value;
|
|
@@ -43,41 +75,70 @@ const extraFrom = (fields) => {
|
|
|
43
75
|
return extra;
|
|
44
76
|
};
|
|
45
77
|
|
|
46
|
-
const
|
|
47
|
-
const map = new Map();
|
|
48
|
-
for (const field of fields) map.set(field[0], field[1]);
|
|
49
|
-
return map;
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
const problemFromFields = (name, fields, root) => {
|
|
53
|
-
const map = fieldMap(fields);
|
|
54
|
-
const message = show(map.get('error') || map.get('message'));
|
|
78
|
+
const problemFromFields = (name, fields, root, fallback = '') => {
|
|
79
|
+
const map = new Map(fields);
|
|
55
80
|
const stack = show(map.get('stack'));
|
|
56
|
-
const
|
|
57
|
-
const
|
|
81
|
+
const parsed = new Map(exceptionFields(stack.split(/\r?\n/)));
|
|
82
|
+
const text = show(
|
|
83
|
+
map.get('error') || map.get('message') || parsed.get('message'),
|
|
84
|
+
);
|
|
85
|
+
const header = ERROR_HEAD.exec(text);
|
|
86
|
+
const message = header ? text.slice(header[0].length).trim() : text;
|
|
87
|
+
const location = show(map.get('at') || map.get('location'));
|
|
58
88
|
const duration = show(map.get('duration_ms') || map.get('duration'));
|
|
59
|
-
|
|
89
|
+
const details = {
|
|
60
90
|
name,
|
|
61
91
|
message,
|
|
62
|
-
trace,
|
|
63
92
|
duration,
|
|
64
|
-
code: show(map.get('code')),
|
|
93
|
+
code: show(map.get('code') || (header && header[2]) || parsed.get('code')),
|
|
65
94
|
operator: show(map.get('operator')),
|
|
66
|
-
expected: show(
|
|
67
|
-
|
|
95
|
+
expected: show(
|
|
96
|
+
map.get('expected') ?? map.get('wanted') ?? map.get('expect'),
|
|
97
|
+
),
|
|
98
|
+
actual: show(map.get('actual') ?? map.get('found') ?? map.get('got')),
|
|
68
99
|
extra: extraFrom(fields),
|
|
69
|
-
}
|
|
100
|
+
};
|
|
101
|
+
const type = show(
|
|
102
|
+
map.get('name') || (header && header[1]) || parsed.get('name'),
|
|
103
|
+
);
|
|
104
|
+
if (type) details.extra.unshift(['type', type]);
|
|
105
|
+
const known =
|
|
106
|
+
message ||
|
|
107
|
+
stack ||
|
|
108
|
+
location ||
|
|
109
|
+
details.operator ||
|
|
110
|
+
details.code ||
|
|
111
|
+
details.expected ||
|
|
112
|
+
details.actual;
|
|
113
|
+
if (!known) details.message = fallback;
|
|
114
|
+
const source = show(map.get('at')) || stack || location;
|
|
115
|
+
const key = errorKey({ ...details, type }, source, root);
|
|
116
|
+
const trace = userTrace(parsed.get('stack') || stack || location, root);
|
|
117
|
+
return problem({ ...details, key, trace });
|
|
70
118
|
};
|
|
71
119
|
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
120
|
+
const quoted = (value) => value.startsWith('"') || value.startsWith('\x27');
|
|
121
|
+
|
|
122
|
+
const scalarText = (value) => {
|
|
123
|
+
if (value.startsWith('"')) {
|
|
124
|
+
try {
|
|
125
|
+
return JSON.parse(value.replace(/\r?\n/g, '\\n'));
|
|
126
|
+
} catch {
|
|
127
|
+
return unquote(value);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return unquote(value);
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const blockText = (lines) => {
|
|
134
|
+
let indent = Infinity;
|
|
135
|
+
for (const line of lines) {
|
|
136
|
+
if (line.trim()) indent = Math.min(indent, line.match(/^\s*/)[0].length);
|
|
137
|
+
}
|
|
138
|
+
return lines
|
|
139
|
+
.map((line) => line.slice(indent))
|
|
140
|
+
.join('\n')
|
|
141
|
+
.trimEnd();
|
|
81
142
|
};
|
|
82
143
|
|
|
83
144
|
const yamlFields = (lines) => {
|
|
@@ -90,70 +151,128 @@ const yamlFields = (lines) => {
|
|
|
90
151
|
continue;
|
|
91
152
|
}
|
|
92
153
|
const key = match[2];
|
|
93
|
-
|
|
154
|
+
let value = match[3].trim().replace(/,$/, '');
|
|
94
155
|
const block = value === '|-' || value === '|' || value === '>';
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
156
|
+
index += 1;
|
|
157
|
+
if (key === 'error' && !value) continue;
|
|
158
|
+
if (quoted(value)) {
|
|
159
|
+
const quote = value[0];
|
|
160
|
+
while (
|
|
161
|
+
index < lines.length &&
|
|
162
|
+
(value.length < 2 || value.at(-1) !== quote)
|
|
163
|
+
) {
|
|
164
|
+
value = `${value}\n${lines[index]}`;
|
|
165
|
+
index += 1;
|
|
166
|
+
}
|
|
167
|
+
fields.push([key, scalarText(value)]);
|
|
98
168
|
continue;
|
|
99
169
|
}
|
|
100
170
|
const rows = [];
|
|
101
|
-
|
|
102
|
-
while (index < lines.length
|
|
103
|
-
|
|
171
|
+
const indent = match[1].length;
|
|
172
|
+
while (index < lines.length) {
|
|
173
|
+
const line = lines[index];
|
|
174
|
+
if (line.trim() && line.match(/^\s*/)[0].length <= indent) break;
|
|
175
|
+
rows.push(lines[index]);
|
|
104
176
|
index += 1;
|
|
105
177
|
}
|
|
106
|
-
|
|
178
|
+
if (block || rows.some((line) => line.trim())) {
|
|
179
|
+
const text = blockText(rows);
|
|
180
|
+
fields.push([key, block ? text : `${value}\n${text}`.trim()]);
|
|
181
|
+
} else {
|
|
182
|
+
fields.push([key, scalarText(value)]);
|
|
183
|
+
}
|
|
107
184
|
}
|
|
108
185
|
return fields;
|
|
109
186
|
};
|
|
110
187
|
|
|
111
|
-
const
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
188
|
+
const tapPoint = (line) => {
|
|
189
|
+
const match = /^\s*(not ok|ok)\b(?:\s+\d+)?(?:\s+-)?(?:\s+(.*))?$/.exec(line);
|
|
190
|
+
if (!match) return null;
|
|
191
|
+
const text = match[2] || '';
|
|
192
|
+
return {
|
|
193
|
+
passed: match[1] === 'ok',
|
|
194
|
+
name: text
|
|
195
|
+
.split(/\s+#/)[0]
|
|
196
|
+
.replace(/\s*\{\s*$/, '')
|
|
197
|
+
.trim(),
|
|
198
|
+
skipped: /#\s*(SKIP|TODO)\b/i.test(text),
|
|
199
|
+
container: /\{\s*$/.test(text),
|
|
200
|
+
};
|
|
119
201
|
};
|
|
120
202
|
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
203
|
+
const STAT_FIELD = {
|
|
204
|
+
tests: 'tests',
|
|
205
|
+
pass: 'passed',
|
|
206
|
+
fail: 'failed',
|
|
207
|
+
skipped: 'skipped',
|
|
208
|
+
skip: 'skipped',
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const applyStat = (stats, key, value) => {
|
|
212
|
+
if (key === 'duration_ms') stats.duration = value;
|
|
213
|
+
const field = Object.hasOwn(STAT_FIELD, key) ? STAT_FIELD[key] : '';
|
|
214
|
+
if (field) stats[field] = Number(value);
|
|
125
215
|
};
|
|
126
216
|
|
|
127
217
|
const applyTapComment = (stats, line) => {
|
|
128
218
|
const match = /^#\s+(\w+)\s+(.+)$/.exec(line.trim());
|
|
129
|
-
if (
|
|
130
|
-
const key = match[1];
|
|
131
|
-
const value = match[2].trim();
|
|
132
|
-
if (key === 'tests') stats.tests = Number(value);
|
|
133
|
-
if (key === 'pass') stats.passed = Number(value);
|
|
134
|
-
if (key === 'fail') stats.failed = Number(value);
|
|
135
|
-
if (key === 'skipped' || key === 'skip') stats.skipped = Number(value);
|
|
136
|
-
if (key === 'duration_ms') stats.duration = value;
|
|
219
|
+
if (match) applyStat(stats, match[1], match[2].trim());
|
|
137
220
|
};
|
|
138
221
|
|
|
139
222
|
const isTapLine = (line) => {
|
|
140
223
|
const text = line.trim();
|
|
141
224
|
if (text.startsWith('TAP version')) return true;
|
|
142
|
-
if (
|
|
225
|
+
if (tapPoint(text)) return true;
|
|
143
226
|
if (/^1\.\.\d+/.test(text)) return true;
|
|
144
227
|
if (text.startsWith('Bail out!')) return true;
|
|
145
228
|
return false;
|
|
146
229
|
};
|
|
147
230
|
|
|
231
|
+
const tapBoundary = (line) => {
|
|
232
|
+
if (isTapLine(line) || line.trim() === '}') return true;
|
|
233
|
+
const stats =
|
|
234
|
+
/^\s*#\s+(?:Subtest:|(?:tests|pass|fail|skipped|duration_ms)\s+\d)/;
|
|
235
|
+
return stats.test(line);
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
const readYaml = (lines, start) => {
|
|
239
|
+
const body = [];
|
|
240
|
+
const indent = lines[start].match(/^\s*/)[0].length;
|
|
241
|
+
let index = start + 1;
|
|
242
|
+
for (; index < lines.length; index++) {
|
|
243
|
+
if (lines[index].trim() === '...') break;
|
|
244
|
+
const lead = lines[index].match(/^\s*/)[0].length;
|
|
245
|
+
if (lead <= indent && tapBoundary(lines[index])) break;
|
|
246
|
+
body.push(lines[index]);
|
|
247
|
+
}
|
|
248
|
+
const end = lines[index] && lines[index].trim() === '...';
|
|
249
|
+
return { fields: yamlFields(body), next: end ? index + 1 : index };
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
const readTapFields = (lines, start) => {
|
|
253
|
+
const fields = [];
|
|
254
|
+
const body = [];
|
|
255
|
+
let index = start;
|
|
256
|
+
while (index < lines.length && !tapBoundary(lines[index])) {
|
|
257
|
+
if (lines[index].trim() === '---') {
|
|
258
|
+
const yaml = readYaml(lines, index);
|
|
259
|
+
fields.push(...yaml.fields);
|
|
260
|
+
index = yaml.next;
|
|
261
|
+
} else {
|
|
262
|
+
body.push(lines[index]);
|
|
263
|
+
index += 1;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
if (!fields.length) fields.push(...exceptionFields(body));
|
|
267
|
+
return { fields, next: index };
|
|
268
|
+
};
|
|
269
|
+
|
|
148
270
|
const parseTap = (text, root) => {
|
|
149
271
|
const lines = `${text ?? ''}`.split(/\r?\n/);
|
|
150
|
-
|
|
151
|
-
for (const line of lines) {
|
|
152
|
-
if (isTapLine(line)) seen = true;
|
|
153
|
-
}
|
|
154
|
-
if (!seen) return null;
|
|
272
|
+
if (!lines.some(isTapLine)) return null;
|
|
155
273
|
const stats = summary();
|
|
156
274
|
const problems = [];
|
|
275
|
+
const counts = { tests: 0, passed: 0, failed: 0, skipped: 0 };
|
|
157
276
|
let index = 0;
|
|
158
277
|
while (index < lines.length) {
|
|
159
278
|
const line = lines[index];
|
|
@@ -162,73 +281,67 @@ const parseTap = (text, root) => {
|
|
|
162
281
|
if (trimmed.startsWith('Bail out!')) {
|
|
163
282
|
problems.push(problem({ name: 'Bail out!', message: trimmed }));
|
|
164
283
|
}
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
if (!failed && !passed) {
|
|
284
|
+
const point = tapPoint(line);
|
|
285
|
+
if (!point || point.container) {
|
|
168
286
|
index += 1;
|
|
169
287
|
continue;
|
|
170
288
|
}
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
289
|
+
const body = readTapFields(lines, index + 1);
|
|
290
|
+
index = body.next;
|
|
291
|
+
const failure = new Map(body.fields).get('failureType');
|
|
292
|
+
if (failure === 'subtestsFailed') continue;
|
|
293
|
+
counts.tests += 1;
|
|
294
|
+
if (point.skipped) {
|
|
295
|
+
counts.skipped += 1;
|
|
296
|
+
} else if (point.passed) {
|
|
297
|
+
counts.passed += 1;
|
|
177
298
|
} else {
|
|
178
|
-
|
|
299
|
+
counts.failed += 1;
|
|
300
|
+
problems.push(
|
|
301
|
+
problemFromFields(point.name, body.fields, root, point.name),
|
|
302
|
+
);
|
|
179
303
|
}
|
|
180
|
-
if (failed) problems.push(problemFromFields(name, fields, root));
|
|
181
304
|
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
const countStatus = (stats, status) => {
|
|
186
|
-
if (status === 'passed' || status === 'pass') {
|
|
187
|
-
stats.passed = (stats.passed || 0) + 1;
|
|
188
|
-
return;
|
|
189
|
-
}
|
|
190
|
-
if (status === 'failed' || status === 'fail' || status === 'timedOut') {
|
|
191
|
-
stats.failed = (stats.failed || 0) + 1;
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
194
|
-
if (status === 'skipped' || status === 'pending' || status === 'todo') {
|
|
195
|
-
stats.skipped = (stats.skipped || 0) + 1;
|
|
305
|
+
for (const key of Object.keys(counts)) {
|
|
306
|
+
if (stats[key] === null) stats[key] = counts[key];
|
|
196
307
|
}
|
|
308
|
+
return report('test', { summary: stats, problems });
|
|
197
309
|
};
|
|
198
310
|
|
|
199
311
|
const nodeEventProblem = (event, root) => {
|
|
200
312
|
const data = event.data || {};
|
|
201
313
|
const details = data.details || {};
|
|
202
314
|
const error = details.error || {};
|
|
203
|
-
const fields =
|
|
204
|
-
for (const key of Object.keys(error)) fields.push([key, error[key]]);
|
|
315
|
+
const fields = Object.entries(error);
|
|
205
316
|
if (details.duration_ms !== undefined) {
|
|
206
317
|
fields.push(['duration_ms', details.duration_ms]);
|
|
207
318
|
}
|
|
208
|
-
|
|
209
|
-
return problemFromFields(name, fields, root);
|
|
319
|
+
return problemFromFields(data.name || '', fields, root);
|
|
210
320
|
};
|
|
211
321
|
|
|
212
322
|
const parseNodeEvents = (events, root) => {
|
|
213
|
-
const stats = summary();
|
|
214
323
|
const problems = [];
|
|
215
324
|
let seen = false;
|
|
325
|
+
let passed = 0;
|
|
326
|
+
let failed = 0;
|
|
327
|
+
let skipped = 0;
|
|
216
328
|
for (const event of events) {
|
|
217
329
|
const type = event && event.type;
|
|
218
330
|
if (typeof type !== 'string' || !type.startsWith('test:')) continue;
|
|
219
331
|
seen = true;
|
|
220
|
-
if (type === 'test:pass')
|
|
332
|
+
if (type === 'test:pass') passed += 1;
|
|
333
|
+
if (type === 'test:skip' || type === 'test:todo') skipped += 1;
|
|
221
334
|
if (type === 'test:fail') {
|
|
222
|
-
|
|
335
|
+
failed += 1;
|
|
223
336
|
problems.push(nodeEventProblem(event, root));
|
|
224
337
|
}
|
|
225
|
-
if (type === 'test:skip' || type === 'test:todo') {
|
|
226
|
-
countStatus(stats, 'skipped');
|
|
227
|
-
}
|
|
228
338
|
}
|
|
229
339
|
if (!seen) return null;
|
|
230
|
-
const
|
|
231
|
-
stats.tests =
|
|
340
|
+
const stats = summary();
|
|
341
|
+
stats.tests = passed + failed + skipped;
|
|
342
|
+
stats.passed = passed || null;
|
|
343
|
+
stats.failed = failed || null;
|
|
344
|
+
stats.skipped = skipped || null;
|
|
232
345
|
return report('test', { summary: stats, problems });
|
|
233
346
|
};
|
|
234
347
|
|
|
@@ -259,6 +372,8 @@ const jestTrace = (text, file, root) => {
|
|
|
259
372
|
return userTrace(file, root);
|
|
260
373
|
};
|
|
261
374
|
|
|
375
|
+
const durationText = (value) => (value === undefined ? '' : `${value}`);
|
|
376
|
+
|
|
262
377
|
const jestProblem = (item, file, root) => {
|
|
263
378
|
const messages = item.failureMessages || [];
|
|
264
379
|
const text = messages.join('\n');
|
|
@@ -270,7 +385,7 @@ const jestProblem = (item, file, root) => {
|
|
|
270
385
|
trace: jestTrace(text, file, root),
|
|
271
386
|
expected: expected ? expected[1] : '',
|
|
272
387
|
actual: received ? received[1] : '',
|
|
273
|
-
duration: item.duration
|
|
388
|
+
duration: durationText(item.duration),
|
|
274
389
|
});
|
|
275
390
|
};
|
|
276
391
|
|
|
@@ -298,29 +413,27 @@ const parseJest = (data, root) => {
|
|
|
298
413
|
return report('test', { summary: stats, problems });
|
|
299
414
|
};
|
|
300
415
|
|
|
416
|
+
const PLAYWRIGHT_FAILED = ['failed', 'timedOut'];
|
|
417
|
+
|
|
418
|
+
const playwrightProblem = (spec, result, root) => {
|
|
419
|
+
const error = result.error || {};
|
|
420
|
+
return problem({
|
|
421
|
+
name: spec.title || '',
|
|
422
|
+
message: error.message || result.status,
|
|
423
|
+
trace: userTrace(error.stack || '', root),
|
|
424
|
+
duration: durationText(result.duration),
|
|
425
|
+
});
|
|
426
|
+
};
|
|
427
|
+
|
|
301
428
|
const walkPlaywright = (suite, problems, root) => {
|
|
302
|
-
const
|
|
303
|
-
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
for (const result of results) {
|
|
308
|
-
const status = result.status;
|
|
309
|
-
if (status !== 'failed' && status !== 'timedOut') continue;
|
|
310
|
-
const error = result.error || {};
|
|
311
|
-
problems.push(
|
|
312
|
-
problem({
|
|
313
|
-
name: spec.title || '',
|
|
314
|
-
message: error.message || status,
|
|
315
|
-
trace: userTrace(error.stack || '', root),
|
|
316
|
-
duration: result.duration === undefined ? '' : `${result.duration}`,
|
|
317
|
-
}),
|
|
318
|
-
);
|
|
319
|
-
}
|
|
429
|
+
for (const spec of suite.specs || []) {
|
|
430
|
+
const results = (spec.tests || []).flatMap((item) => item.results || []);
|
|
431
|
+
for (const result of results) {
|
|
432
|
+
if (!PLAYWRIGHT_FAILED.includes(result.status)) continue;
|
|
433
|
+
problems.push(playwrightProblem(spec, result, root));
|
|
320
434
|
}
|
|
321
435
|
}
|
|
322
|
-
const
|
|
323
|
-
for (const child of children) walkPlaywright(child, problems, root);
|
|
436
|
+
for (const child of suite.suites || []) walkPlaywright(child, problems, root);
|
|
324
437
|
};
|
|
325
438
|
|
|
326
439
|
const parsePlaywright = (data, root) => {
|
|
@@ -349,12 +462,7 @@ const specStats = (stats, line) => {
|
|
|
349
462
|
const match = /^[ℹi]\s+(\w+)\s+(.+)$/.exec(line.trim());
|
|
350
463
|
if (!match) return false;
|
|
351
464
|
const key = match[1];
|
|
352
|
-
|
|
353
|
-
if (key === 'tests') stats.tests = Number(value);
|
|
354
|
-
if (key === 'pass') stats.passed = Number(value);
|
|
355
|
-
if (key === 'fail') stats.failed = Number(value);
|
|
356
|
-
if (key === 'skipped') stats.skipped = Number(value);
|
|
357
|
-
if (key === 'duration_ms') stats.duration = value;
|
|
465
|
+
applyStat(stats, key, match[2].trim());
|
|
358
466
|
return key === 'tests' || key === 'pass' || key === 'fail';
|
|
359
467
|
};
|
|
360
468
|
|
|
@@ -391,19 +499,17 @@ const readSpecObject = (lines, start) => {
|
|
|
391
499
|
|
|
392
500
|
const pushSpec = (problems, current, root) => {
|
|
393
501
|
if (!current) return;
|
|
394
|
-
const fields = current.fields;
|
|
395
|
-
|
|
396
|
-
if (current.
|
|
397
|
-
|
|
398
|
-
if (
|
|
399
|
-
|
|
400
|
-
if (current.actual) item.actual = current.actual;
|
|
401
|
-
problems.push(item);
|
|
502
|
+
const fields = [...current.fields];
|
|
503
|
+
if (current.message) fields.push(['error', current.message.trimEnd()]);
|
|
504
|
+
if (current.trace.length) fields.push(['stack', current.trace.join('\n')]);
|
|
505
|
+
if (current.expected) fields.push(['expected', current.expected]);
|
|
506
|
+
if (current.actual) fields.push(['actual', current.actual]);
|
|
507
|
+
problems.push(problemFromFields(current.name, fields, root));
|
|
402
508
|
};
|
|
403
509
|
|
|
404
510
|
const takeSpecLine = (current, line) => {
|
|
405
511
|
const text = line.trim();
|
|
406
|
-
if (/^(
|
|
512
|
+
if (/^[\w.]*Error(?: \[[^\]]+\])?:/.test(text)) {
|
|
407
513
|
current.message = text;
|
|
408
514
|
return;
|
|
409
515
|
}
|
|
@@ -411,6 +517,9 @@ const takeSpecLine = (current, line) => {
|
|
|
411
517
|
if (expected) current.expected = expected[1];
|
|
412
518
|
const received = /^Received:\s*(.*)$/.exec(text);
|
|
413
519
|
if (received) current.actual = received[1];
|
|
520
|
+
if (current.message && !current.trace.length) {
|
|
521
|
+
current.message = `${current.message}\n${text}`;
|
|
522
|
+
}
|
|
414
523
|
};
|
|
415
524
|
|
|
416
525
|
const takeSpecFrame = (lines, index, current) => {
|
|
@@ -440,12 +549,8 @@ const dropPreviews = (problems) => {
|
|
|
440
549
|
for (const item of problems) {
|
|
441
550
|
if (item.name && hasDetail(item)) named.add(item.name);
|
|
442
551
|
}
|
|
443
|
-
const
|
|
444
|
-
|
|
445
|
-
if (item.name && named.has(item.name) && !hasDetail(item)) continue;
|
|
446
|
-
kept.push(item);
|
|
447
|
-
}
|
|
448
|
-
return kept;
|
|
552
|
+
const isPreview = (item) => named.has(item.name) && !hasDetail(item);
|
|
553
|
+
return problems.filter((item) => !isPreview(item));
|
|
449
554
|
};
|
|
450
555
|
|
|
451
556
|
const parseSpec = (text, root) => {
|
|
@@ -454,9 +559,19 @@ const parseSpec = (text, root) => {
|
|
|
454
559
|
const problems = [];
|
|
455
560
|
let current = null;
|
|
456
561
|
let marked = false;
|
|
562
|
+
let summarized = false;
|
|
457
563
|
let index = 0;
|
|
458
564
|
while (index < lines.length) {
|
|
459
565
|
const line = lines[index];
|
|
566
|
+
if (specHeader(line)) {
|
|
567
|
+
pushSpec(problems, current, root);
|
|
568
|
+
current = null;
|
|
569
|
+
if (!summarized) problems.length = 0;
|
|
570
|
+
summarized = true;
|
|
571
|
+
marked = true;
|
|
572
|
+
index += 1;
|
|
573
|
+
continue;
|
|
574
|
+
}
|
|
460
575
|
if (specStats(stats, line)) marked = true;
|
|
461
576
|
if (isPassingTest(line)) {
|
|
462
577
|
marked = true;
|