@testspectra/reporter 1.3.5 → 1.3.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/dist/formatter.d.ts +25 -0
- package/dist/formatter.js +41 -0
- package/dist/parser.js +25 -3
- package/dist/reporter.js +22 -3
- package/dist/types.d.ts +23 -0
- package/package.json +1 -1
package/dist/formatter.d.ts
CHANGED
|
@@ -36,3 +36,28 @@ export declare function formatAssertion(assertionKey: AssertionKey | string, tar
|
|
|
36
36
|
* Parses raw JSON action/assertion payloads or native orchestrator payloads into English sentences.
|
|
37
37
|
*/
|
|
38
38
|
export declare function parseAndFormat(cmdText: string): string;
|
|
39
|
+
/** The key a `Spectra.step` group is stored under: its path, outermost first. */
|
|
40
|
+
export declare function groupKey(path: readonly string[]): string;
|
|
41
|
+
/** Reads `groupPath` from a step event's JSON payload (absent outside any `Spectra.step`). */
|
|
42
|
+
export declare function readGroupPath(payloadText: string): string[] | undefined;
|
|
43
|
+
/** One printed line of a test case's step tree: a `Spectra.step` heading or an action/assertion. */
|
|
44
|
+
export interface StepTreeLine {
|
|
45
|
+
kind: 'group' | 'step';
|
|
46
|
+
text: string;
|
|
47
|
+
depth: number;
|
|
48
|
+
passed: boolean;
|
|
49
|
+
duration?: number;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Lays a test case's steps out as a tree: each `Spectra.step` heading once, before the first step
|
|
53
|
+
* it holds, and every step indented under the groups it ran inside.
|
|
54
|
+
*/
|
|
55
|
+
export declare function stepTree(steps: readonly {
|
|
56
|
+
text: string;
|
|
57
|
+
duration: number;
|
|
58
|
+
passed: boolean;
|
|
59
|
+
groupPath?: string[];
|
|
60
|
+
}[], groups?: Record<string, {
|
|
61
|
+
passed: boolean;
|
|
62
|
+
duration: number;
|
|
63
|
+
}>): StepTreeLine[];
|
package/dist/formatter.js
CHANGED
|
@@ -395,3 +395,44 @@ export function parseAndFormat(cmdText) {
|
|
|
395
395
|
return cmdText;
|
|
396
396
|
}
|
|
397
397
|
}
|
|
398
|
+
/** The key a `Spectra.step` group is stored under: its path, outermost first. */
|
|
399
|
+
export function groupKey(path) {
|
|
400
|
+
return path.join(' › ');
|
|
401
|
+
}
|
|
402
|
+
/** Reads `groupPath` from a step event's JSON payload (absent outside any `Spectra.step`). */
|
|
403
|
+
export function readGroupPath(payloadText) {
|
|
404
|
+
try {
|
|
405
|
+
const payload = JSON.parse(payloadText);
|
|
406
|
+
return Array.isArray(payload.groupPath) ? payload.groupPath.map(String) : undefined;
|
|
407
|
+
}
|
|
408
|
+
catch {
|
|
409
|
+
return undefined;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Lays a test case's steps out as a tree: each `Spectra.step` heading once, before the first step
|
|
414
|
+
* it holds, and every step indented under the groups it ran inside.
|
|
415
|
+
*/
|
|
416
|
+
export function stepTree(steps, groups = {}) {
|
|
417
|
+
const lines = [];
|
|
418
|
+
let open = [];
|
|
419
|
+
for (const step of steps) {
|
|
420
|
+
const path = step.groupPath ?? [];
|
|
421
|
+
let shared = 0;
|
|
422
|
+
while (shared < open.length && shared < path.length && open[shared] === path[shared])
|
|
423
|
+
shared++;
|
|
424
|
+
for (let depth = shared; depth < path.length; depth++) {
|
|
425
|
+
const result = groups[groupKey(path.slice(0, depth + 1))];
|
|
426
|
+
lines.push({
|
|
427
|
+
kind: 'group',
|
|
428
|
+
text: path[depth],
|
|
429
|
+
depth,
|
|
430
|
+
passed: result ? result.passed : step.passed,
|
|
431
|
+
duration: result?.duration,
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
open = path;
|
|
435
|
+
lines.push({ kind: 'step', text: step.text, depth: path.length, passed: step.passed, duration: step.duration });
|
|
436
|
+
}
|
|
437
|
+
return lines;
|
|
438
|
+
}
|
package/dist/parser.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { parseAndFormat } from './formatter.js';
|
|
1
|
+
import { parseAndFormat, readGroupPath } from './formatter.js';
|
|
2
2
|
/**
|
|
3
3
|
* Strips ANSI color and control escape sequences from a string.
|
|
4
4
|
*/
|
|
@@ -119,6 +119,28 @@ export class TestOutputParser {
|
|
|
119
119
|
specPath = specPath.replace(/^[a-zA-Z0-9_-]+\s+-\s+/, '').trim();
|
|
120
120
|
return { type: 'spec_file', specPath };
|
|
121
121
|
}
|
|
122
|
+
// 2.5 `Spectra.step` group boundaries: "[TESTSPECTRA_GROUP_START|PASS|FAIL] {title, path, depth}"
|
|
123
|
+
const group = raw.match(/^\[TESTSPECTRA_GROUP_(START|PASS|FAIL)\]\s*(.*)$/);
|
|
124
|
+
if (group) {
|
|
125
|
+
const durationMatch = group[2].match(/\(([^)]+)ms\)$/);
|
|
126
|
+
try {
|
|
127
|
+
const payload = JSON.parse(group[2].replace(/\s*\([^)]+\)$/, '').trim());
|
|
128
|
+
const title = String(payload.title ?? '');
|
|
129
|
+
const depth = typeof payload.depth === 'number' ? payload.depth : 0;
|
|
130
|
+
if (group[1] === 'START')
|
|
131
|
+
return { type: 'group_start', title, depth };
|
|
132
|
+
return {
|
|
133
|
+
type: 'group_end',
|
|
134
|
+
title,
|
|
135
|
+
depth,
|
|
136
|
+
passed: group[1] === 'PASS',
|
|
137
|
+
durationMs: durationMatch ? parseInt(durationMatch[1], 10) : undefined,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return { type: 'none' };
|
|
142
|
+
}
|
|
143
|
+
}
|
|
122
144
|
// 3. Step passed: "✔ <stepText> (<duration>ms)" or "[TESTSPECTRA_STEP_PASS]"
|
|
123
145
|
if (raw.startsWith('✔ ') || raw.startsWith('[TESTSPECTRA_STEP_PASS]')) {
|
|
124
146
|
const fullText = raw
|
|
@@ -129,7 +151,7 @@ export class TestOutputParser {
|
|
|
129
151
|
const durationMs = match ? parseInt(match[1], 10) : undefined;
|
|
130
152
|
const cmdText = fullText.replace(/\s*\([^)]+\)$/, '').trim();
|
|
131
153
|
const formatted = parseAndFormat(cmdText);
|
|
132
|
-
return { type: 'step_pass', stepText: formatted, durationMs };
|
|
154
|
+
return { type: 'step_pass', stepText: formatted, durationMs, depth: readGroupPath(cmdText)?.length };
|
|
133
155
|
}
|
|
134
156
|
// 4. Step failed: "✖ <stepText> (<duration>ms)" or "[TESTSPECTRA_STEP_FAIL]"
|
|
135
157
|
if (raw.startsWith('✖ ') || raw.startsWith('[TESTSPECTRA_STEP_FAIL]')) {
|
|
@@ -141,7 +163,7 @@ export class TestOutputParser {
|
|
|
141
163
|
const durationMs = match ? parseInt(match[1], 10) : undefined;
|
|
142
164
|
const cmdText = fullText.replace(/\s*\([^)]+\)$/, '').trim();
|
|
143
165
|
const formatted = parseAndFormat(cmdText);
|
|
144
|
-
return { type: 'step_fail', stepText: formatted, durationMs };
|
|
166
|
+
return { type: 'step_fail', stepText: formatted, durationMs, depth: readGroupPath(cmdText)?.length };
|
|
145
167
|
}
|
|
146
168
|
// 5. Test error: "[TESTSPECTRA_TEST_ERROR]" or "↳ Error: <msg>"
|
|
147
169
|
if (raw.startsWith('[TESTSPECTRA_TEST_ERROR]') || raw.includes('↳ Error:')) {
|
package/dist/reporter.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { parseAndFormat } from './formatter.js';
|
|
1
|
+
import { groupKey, parseAndFormat, readGroupPath } from './formatter.js';
|
|
2
2
|
import { stripAnsi } from './parser.js';
|
|
3
3
|
export class SemanticReporter {
|
|
4
4
|
logs = [];
|
|
@@ -120,6 +120,25 @@ export class SemanticReporter {
|
|
|
120
120
|
this.options.onStepStart?.(testTitle || testCase?.title || '', formatted, workerId ?? testCase?.workerId);
|
|
121
121
|
return;
|
|
122
122
|
}
|
|
123
|
+
// 2.5 A `Spectra.step` group ended -> record its outcome for the step tree
|
|
124
|
+
if (raw.startsWith('[TESTSPECTRA_GROUP_PASS]') || raw.startsWith('[TESTSPECTRA_GROUP_FAIL]')) {
|
|
125
|
+
const passed = raw.startsWith('[TESTSPECTRA_GROUP_PASS]');
|
|
126
|
+
const fullText = raw.replace(/^\[TESTSPECTRA_GROUP_(PASS|FAIL)\]/, '').trim();
|
|
127
|
+
const match = fullText.match(/\(([^)]+)ms\)$/);
|
|
128
|
+
const duration = match ? parseInt(match[1], 10) : 0;
|
|
129
|
+
try {
|
|
130
|
+
const payload = JSON.parse(fullText.replace(/\s*\([^)]+\)$/, '').trim());
|
|
131
|
+
const testCase = this.resolveTestCase(payload.test || '', payload.workerId);
|
|
132
|
+
if (testCase && Array.isArray(payload.path)) {
|
|
133
|
+
testCase.groups = { ...testCase.groups, [groupKey(payload.path)]: { passed, duration } };
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
catch { }
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (raw.startsWith('[TESTSPECTRA_GROUP_START]')) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
123
142
|
// 3. Step passed -> record step to matching test case
|
|
124
143
|
if (raw.startsWith('[TESTSPECTRA_STEP_PASS]')) {
|
|
125
144
|
const fullText = raw.replace('[TESTSPECTRA_STEP_PASS]', '').trim();
|
|
@@ -136,7 +155,7 @@ export class SemanticReporter {
|
|
|
136
155
|
}
|
|
137
156
|
catch { }
|
|
138
157
|
const testCase = this.resolveTestCase(testTitle, workerId);
|
|
139
|
-
const step = { text: formatted, duration, passed: true };
|
|
158
|
+
const step = { text: formatted, duration, passed: true, groupPath: readGroupPath(cmdText) };
|
|
140
159
|
if (testCase) {
|
|
141
160
|
testCase.steps.push(step);
|
|
142
161
|
testCase.currentStep = formatted;
|
|
@@ -160,7 +179,7 @@ export class SemanticReporter {
|
|
|
160
179
|
}
|
|
161
180
|
catch { }
|
|
162
181
|
const testCase = this.resolveTestCase(testTitle, workerId);
|
|
163
|
-
const step = { text: formatted, duration, passed: false };
|
|
182
|
+
const step = { text: formatted, duration, passed: false, groupPath: readGroupPath(cmdText) };
|
|
164
183
|
if (testCase) {
|
|
165
184
|
testCase.steps.push(step);
|
|
166
185
|
testCase.currentStep = formatted;
|
package/dist/types.d.ts
CHANGED
|
@@ -35,11 +35,20 @@ export interface PendingStep {
|
|
|
35
35
|
text: string;
|
|
36
36
|
duration: number;
|
|
37
37
|
passed: boolean;
|
|
38
|
+
/** The `Spectra.step` titles the step ran inside, outermost first (absent outside any). */
|
|
39
|
+
groupPath?: string[];
|
|
40
|
+
}
|
|
41
|
+
/** How a `Spectra.step` ended, keyed by its path (`groupKey`). */
|
|
42
|
+
export interface StepGroupResult {
|
|
43
|
+
passed: boolean;
|
|
44
|
+
duration: number;
|
|
38
45
|
}
|
|
39
46
|
export interface PendingTestCase {
|
|
40
47
|
title: string;
|
|
41
48
|
workerId?: number;
|
|
42
49
|
steps: PendingStep[];
|
|
50
|
+
/** `Spectra.step` outcomes by `groupKey(path)`. */
|
|
51
|
+
groups?: Record<string, StepGroupResult>;
|
|
43
52
|
currentStep?: string;
|
|
44
53
|
errorMsg?: string;
|
|
45
54
|
videoPath?: string;
|
|
@@ -96,10 +105,24 @@ export type ParsedToken = {
|
|
|
96
105
|
type: 'step_pass';
|
|
97
106
|
stepText: string;
|
|
98
107
|
durationMs?: number;
|
|
108
|
+
/** How many `Spectra.step` groups the step ran inside. */
|
|
109
|
+
depth?: number;
|
|
99
110
|
} | {
|
|
100
111
|
type: 'step_fail';
|
|
101
112
|
stepText: string;
|
|
102
113
|
durationMs?: number;
|
|
114
|
+
depth?: number;
|
|
115
|
+
} | {
|
|
116
|
+
type: 'group_start';
|
|
117
|
+
title: string;
|
|
118
|
+
/** 0 for a step directly in the test, 1 for one inside another step, … */
|
|
119
|
+
depth: number;
|
|
120
|
+
} | {
|
|
121
|
+
type: 'group_end';
|
|
122
|
+
title: string;
|
|
123
|
+
depth: number;
|
|
124
|
+
passed: boolean;
|
|
125
|
+
durationMs?: number;
|
|
103
126
|
} | {
|
|
104
127
|
type: 'summary_banner';
|
|
105
128
|
text: string;
|