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.
Files changed (109) hide show
  1. package/CHANGELOG.md +25 -1
  2. package/LICENSE +1 -1
  3. package/README.md +12 -13
  4. package/bin/reslop.js +3 -8
  5. package/lib/ansi.js +32 -19
  6. package/lib/capabilities.js +8 -12
  7. package/lib/cli.js +40 -74
  8. package/lib/clipboard.js +48 -18
  9. package/lib/dashboard/git.js +128 -0
  10. package/lib/dashboard/model.js +332 -0
  11. package/lib/dashboard/npm.js +193 -0
  12. package/lib/dashboard/tree.js +278 -0
  13. package/lib/deps-diff.js +196 -404
  14. package/lib/deps.js +240 -399
  15. package/lib/detect.js +8 -18
  16. package/lib/diff/align.js +41 -113
  17. package/lib/diff/diff.js +12 -43
  18. package/lib/diff/display.js +25 -36
  19. package/lib/diff/edit.js +6 -20
  20. package/lib/diff/inline.js +138 -141
  21. package/lib/diff/items.js +33 -47
  22. package/lib/diff/parse.js +28 -56
  23. package/lib/diff/patch.js +12 -33
  24. package/lib/diff/unit.js +90 -206
  25. package/lib/editor.js +98 -55
  26. package/lib/files.js +86 -103
  27. package/lib/git-branches.js +254 -416
  28. package/lib/git-deps.js +182 -344
  29. package/lib/git-reword-todo.js +5 -13
  30. package/lib/git-worktree.js +141 -212
  31. package/lib/git.js +21 -49
  32. package/lib/github.js +67 -181
  33. package/lib/gitlab.js +40 -90
  34. package/lib/highlight/bash.js +55 -177
  35. package/lib/highlight/core.js +157 -132
  36. package/lib/highlight/css.js +25 -58
  37. package/lib/highlight/csv.js +19 -37
  38. package/lib/highlight/dart.js +72 -213
  39. package/lib/highlight/dot.js +16 -26
  40. package/lib/highlight/html.js +42 -63
  41. package/lib/highlight/js.js +177 -506
  42. package/lib/highlight/json.js +17 -37
  43. package/lib/highlight/log.js +22 -35
  44. package/lib/highlight/txt.js +9 -22
  45. package/lib/highlight.js +17 -46
  46. package/lib/keys.js +62 -85
  47. package/lib/manifest.js +67 -130
  48. package/lib/npm-commands.js +119 -189
  49. package/lib/remote.js +141 -197
  50. package/lib/render/branches.js +66 -73
  51. package/lib/render/chrome.js +264 -323
  52. package/lib/render/commits.js +187 -500
  53. package/lib/render/dash-activity.js +212 -0
  54. package/lib/render/dash-blocks.js +245 -0
  55. package/lib/render/dash-table.js +290 -0
  56. package/lib/render/dashboard.js +62 -0
  57. package/lib/render/diff.js +123 -228
  58. package/lib/render/files.js +45 -99
  59. package/lib/render/frame.js +129 -183
  60. package/lib/render/notes.js +144 -160
  61. package/lib/render/npm.js +234 -112
  62. package/lib/render/present.js +4 -11
  63. package/lib/render/primitives.js +131 -170
  64. package/lib/render/render.js +5 -14
  65. package/lib/render/report.js +43 -0
  66. package/lib/render/tiles.js +260 -0
  67. package/lib/render/unit.js +15 -28
  68. package/lib/report-model.js +110 -33
  69. package/lib/report-parse.js +56 -38
  70. package/lib/report-render.js +102 -59
  71. package/lib/report-run.js +87 -78
  72. package/lib/report-test.js +256 -141
  73. package/lib/report-tools.js +96 -161
  74. package/lib/review-merge.js +114 -152
  75. package/lib/review.js +115 -189
  76. package/lib/runs.js +225 -0
  77. package/lib/select.js +89 -75
  78. package/lib/session/accessors.js +14 -21
  79. package/lib/session/actions.js +82 -63
  80. package/lib/session/branches.js +193 -247
  81. package/lib/session/change-actions.js +171 -214
  82. package/lib/session/commit.js +280 -342
  83. package/lib/session/compose.js +559 -674
  84. package/lib/session/dashboard.js +385 -0
  85. package/lib/session/dispatch.js +164 -157
  86. package/lib/session/edit-click.js +57 -0
  87. package/lib/session/files-pane.js +85 -53
  88. package/lib/session/items.js +73 -104
  89. package/lib/session/lifecycle.js +403 -541
  90. package/lib/session/load.js +48 -68
  91. package/lib/session/navigation.js +96 -87
  92. package/lib/session/npm.js +409 -345
  93. package/lib/session/ops.js +196 -63
  94. package/lib/session/pointer.js +23 -15
  95. package/lib/session/progress.js +33 -21
  96. package/lib/session/review.js +33 -54
  97. package/lib/session/tasks.js +333 -0
  98. package/lib/session/templates.js +51 -59
  99. package/lib/session/terminal.js +85 -136
  100. package/lib/session/unit.js +3 -4
  101. package/lib/session/update.js +76 -97
  102. package/lib/session/view.js +52 -83
  103. package/lib/session/watch.js +208 -204
  104. package/lib/session.js +112 -237
  105. package/lib/update.js +45 -116
  106. package/lib/utilities.js +67 -89
  107. package/lib/wrap.js +33 -44
  108. package/package.json +1 -1
  109. package/lib/session/todos.js +0 -293
@@ -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 fieldMap = (fields) => {
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 location = show(map.get('location'));
57
- const trace = userTrace(stack || location, root);
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
- return problem({
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(map.get('expected')),
67
- actual: show(map.get('actual')),
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 yamlValue = (raw) => {
73
- const text = `${raw ?? ''}`.trim();
74
- if (text.length < 2) return text;
75
- const open = text[0];
76
- const close = text[text.length - 1];
77
- const code = open.charCodeAt(0);
78
- const quoted = open === close && (code === 34 || code === 39);
79
- if (quoted) return text.slice(1, -1);
80
- return text;
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
- const value = match[3].trim().replace(/,$/, '');
154
+ let value = match[3].trim().replace(/,$/, '');
94
155
  const block = value === '|-' || value === '|' || value === '>';
95
- if (!block) {
96
- fields.push([key, yamlValue(value)]);
97
- index += 1;
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
- index += 1;
102
- while (index < lines.length && /^\s+\S/.test(lines[index])) {
103
- rows.push(lines[index].trim());
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
- fields.push([key, rows.join('\n')]);
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 readYaml = (lines, start) => {
112
- const body = [];
113
- let index = start + 1;
114
- for (; index < lines.length; index++) {
115
- if (lines[index].trim() === '...') break;
116
- body.push(lines[index]);
117
- }
118
- return { fields: yamlFields(body), next: index + 1 };
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 tapName = (line) => {
122
- const match = /^(?:not )?ok\s+\d+\s+-?\s*(.*)$/.exec(line.trim());
123
- if (!match) return line.trim();
124
- return match[1].trim();
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 (!match) return;
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 (/^(not )?ok\s+\d+\b/.test(text)) return true;
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
- let seen = false;
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 failed = /^not ok\s+\d+\b/.test(trimmed);
166
- const passed = /^ok\s+\d+\b/.test(trimmed);
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 name = tapName(trimmed);
172
- let fields = [];
173
- if (lines[index + 1] && lines[index + 1].trim() === '---') {
174
- const yaml = readYaml(lines, index + 1);
175
- fields = yaml.fields;
176
- index = yaml.next;
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
- index += 1;
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
- return report('test', { summary: stats, problems });
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
- const name = data.name || '';
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') countStatus(stats, 'passed');
332
+ if (type === 'test:pass') passed += 1;
333
+ if (type === 'test:skip' || type === 'test:todo') skipped += 1;
221
334
  if (type === 'test:fail') {
222
- countStatus(stats, 'failed');
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 skipped = stats.skipped || 0;
231
- stats.tests = (stats.passed || 0) + (stats.failed || 0) + skipped;
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 === undefined ? '' : `${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 specs = suite.specs || [];
303
- for (const spec of specs) {
304
- const tests = spec.tests || [];
305
- for (const item of tests) {
306
- const results = item.results || [];
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 children = suite.suites || [];
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
- const value = match[2].trim();
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
- const item = problemFromFields(current.name, fields, root);
396
- if (current.message && !item.message) item.message = current.message;
397
- const trace = userTrace(current.trace.join('\n'), root);
398
- if (trace) item.trace = trace;
399
- if (current.expected) item.expected = current.expected;
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 (/^(AssertionError|Error)\b/.test(text)) {
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 kept = [];
444
- for (const item of problems) {
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;