@testspectra/reporter 1.1.13

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/LICENSE.md ADDED
@@ -0,0 +1,5 @@
1
+ # TestSpectra Proprietary License
2
+
3
+ Copyright (c) 2026 TestSpectra Team. All rights reserved.
4
+
5
+ This source code is licensed under the proprietary terms specified in the root repository license.
@@ -0,0 +1,38 @@
1
+ import type { ActionKey, AssertionKey } from './types.js';
2
+ /**
3
+ * Extracts a string representation from an element/target identifier or object.
4
+ */
5
+ export declare function extractTargetString(target?: any): string;
6
+ /**
7
+ * Renders a `ScopedSelector`'s `.parent` chain (produced by `element.get()`/`.getAll()` chaining)
8
+ * as `"parent" within "grandparent"`, nearest ancestor first — or null when the target isn't scoped.
9
+ */
10
+ export declare function extractWithinChain(target?: any): string | null;
11
+ /**
12
+ * Formats an element target into a descriptive label.
13
+ */
14
+ export declare function formatElementTarget(target?: any): string;
15
+ /**
16
+ * Formats a collection target into a descriptive label.
17
+ */
18
+ export declare function formatCollectionTarget(target?: any): string;
19
+ /**
20
+ * Maps positional action arguments array to a named argument dictionary.
21
+ */
22
+ export declare function mapActionArgs(key: string, arr: any): Record<string, any>;
23
+ /**
24
+ * Maps positional assertion arguments array to a named argument dictionary.
25
+ */
26
+ export declare function mapAssertionArgs(key: string, arr: any): Record<string, any>;
27
+ /**
28
+ * Transforms raw action commands into grammatically correct English sentences.
29
+ */
30
+ export declare function formatAction(actionKey: ActionKey | string, target?: any, args?: Record<string, any>): string;
31
+ /**
32
+ * Transforms raw matcher assertions into grammatically correct English sentences.
33
+ */
34
+ export declare function formatAssertion(assertionKey: AssertionKey | string, target?: any, args?: Record<string, any>): string;
35
+ /**
36
+ * Parses raw JSON action/assertion payloads or native orchestrator payloads into English sentences.
37
+ */
38
+ export declare function parseAndFormat(cmdText: string): string;
@@ -0,0 +1,379 @@
1
+ /**
2
+ * Extracts a string representation from an element/target identifier or object.
3
+ */
4
+ export function extractTargetString(target) {
5
+ if (!target)
6
+ return '';
7
+ if (typeof target === 'string')
8
+ return target;
9
+ if (typeof target === 'object') {
10
+ if (typeof target.selector === 'string')
11
+ return target.selector;
12
+ if (typeof target.name === 'string')
13
+ return target.name;
14
+ if (typeof target.target === 'string')
15
+ return target.target;
16
+ }
17
+ return String(target);
18
+ }
19
+ /**
20
+ * Renders a `ScopedSelector`'s `.parent` chain (produced by `element.get()`/`.getAll()` chaining)
21
+ * as `"parent" within "grandparent"`, nearest ancestor first — or null when the target isn't scoped.
22
+ */
23
+ export function extractWithinChain(target) {
24
+ if (!target || typeof target !== 'object' || !target.parent)
25
+ return null;
26
+ const chain = [];
27
+ for (let p = target.parent; p; p = p.parent) {
28
+ if (p.selector) {
29
+ chain.push(p.selector);
30
+ }
31
+ }
32
+ return chain.length > 0 ? chain.join('" within "') : null;
33
+ }
34
+ /**
35
+ * Formats an element target into a descriptive label.
36
+ */
37
+ export function formatElementTarget(target) {
38
+ const sel = extractTargetString(target);
39
+ if (!sel)
40
+ return 'element {target}';
41
+ if (sel.startsWith('element "') || sel.startsWith('collection "')) {
42
+ return sel;
43
+ }
44
+ const within = extractWithinChain(target);
45
+ return within ? `element "${sel}" within "${within}"` : `element "${sel}"`;
46
+ }
47
+ /**
48
+ * Formats a collection target into a descriptive label.
49
+ */
50
+ export function formatCollectionTarget(target) {
51
+ const sel = extractTargetString(target);
52
+ if (!sel)
53
+ return 'collection "{selector}"';
54
+ if (sel.startsWith('collection "')) {
55
+ return sel;
56
+ }
57
+ const within = extractWithinChain(target);
58
+ return within ? `collection "${sel}" within "${within}"` : `collection "${sel}"`;
59
+ }
60
+ /**
61
+ * Maps positional action arguments array to a named argument dictionary.
62
+ */
63
+ export function mapActionArgs(key, arr) {
64
+ if (!Array.isArray(arr)) {
65
+ return typeof arr === 'object' && arr !== null ? arr : {};
66
+ }
67
+ switch (key) {
68
+ case 'type':
69
+ return { text: arr[0] };
70
+ case 'select':
71
+ case 'selectOption':
72
+ return { option: arr[0] };
73
+ case 'dragDrop':
74
+ return { dest: arr[0] };
75
+ case 'longPress':
76
+ return { durationMs: arr[0] };
77
+ case 'navigate':
78
+ return { url: arr[0] };
79
+ case 'setCookies':
80
+ return { url: arr[0] };
81
+ case 'setViewport':
82
+ return { width: arr[0], height: arr[1] };
83
+ case 'wait':
84
+ return { ms: arr[0] };
85
+ case 'pressKey':
86
+ return { key: arr[0] };
87
+ case 'scroll':
88
+ return { direction: arr[0], pixels: arr[1] };
89
+ case 'swipe':
90
+ return { direction: arr[0], distance: arr[1] };
91
+ case 'intercept':
92
+ return { method: arr[0], url: arr[1] };
93
+ default:
94
+ return {};
95
+ }
96
+ }
97
+ /**
98
+ * Maps positional assertion arguments array to a named argument dictionary.
99
+ */
100
+ export function mapAssertionArgs(key, arr) {
101
+ if (!Array.isArray(arr)) {
102
+ return typeof arr === 'object' && arr !== null ? arr : { expected: arr };
103
+ }
104
+ if (key === 'have.attr' || key === 'not.have.attr') {
105
+ return arr.length >= 2 ? { name: arr[0], value: arr[1], expected: arr[0] } : { name: arr[0], expected: arr[0] };
106
+ }
107
+ if (key === 'have.css' || key === 'not.have.css') {
108
+ return { name: arr[0], property: arr[0], value: arr[1], expected: arr[0] };
109
+ }
110
+ if (arr.length >= 2) {
111
+ return { name: arr[0], value: arr[1], expected: arr[0] };
112
+ }
113
+ return {
114
+ expected: arr[0],
115
+ name: arr[0],
116
+ text: arr[0],
117
+ value: arr[0],
118
+ title: arr[0],
119
+ url: arr[0],
120
+ length: arr[0],
121
+ min: arr[0],
122
+ max: arr[0],
123
+ };
124
+ }
125
+ /**
126
+ * Transforms raw action commands into grammatically correct English sentences.
127
+ */
128
+ export function formatAction(actionKey, target, args = {}) {
129
+ const el = formatElementTarget(target);
130
+ switch (actionKey) {
131
+ case 'mount':
132
+ return `Mount component "${extractTargetString(target) || '{component}'}"`;
133
+ case 'click':
134
+ return `Click on ${el}`;
135
+ case 'doubleClick':
136
+ return `Double-click on ${el}`;
137
+ case 'rightClick':
138
+ return `Right-click on ${el}`;
139
+ case 'type':
140
+ return `Type "${args.text ?? args.value ?? '{text}'}" into ${el}`;
141
+ case 'clear':
142
+ return `Clear text in ${el}`;
143
+ case 'select':
144
+ case 'selectOption':
145
+ return `Select option "${args.option ?? args.value ?? args.text ?? '{option}'}" in ${el}`;
146
+ case 'hover':
147
+ return `Hover over ${el}`;
148
+ case 'dragDrop': {
149
+ const dest = args.dest || args.destination ? formatElementTarget(args.dest || args.destination) : 'element {dest}';
150
+ return `Drag ${el} and drop onto ${dest}`;
151
+ }
152
+ case 'scrollIntoView':
153
+ return `Scroll ${el} into view`;
154
+ case 'longPress':
155
+ return `Long-press on ${el} for ${args.durationMs ?? args.ms ?? args.duration ?? '{durationMs}'}ms`;
156
+ case 'waitForElement':
157
+ return `Wait for ${el} to be displayed`;
158
+ case 'navigate':
159
+ return `Navigate to "${args.url ?? '{url}'}"`;
160
+ case 'back':
161
+ return `Navigate back in browser`;
162
+ case 'forward':
163
+ return `Navigate forward in browser`;
164
+ case 'refresh':
165
+ return `Refresh current page`;
166
+ case 'setViewport':
167
+ return `Set viewport to ${args.width ?? '{width}'}x${args.height ?? '{height}'}`;
168
+ case 'wait':
169
+ return `Wait for ${args.ms ?? args.durationMs ?? '{ms}'}ms`;
170
+ case 'pressKey':
171
+ return `Press keyboard key "${args.key ?? args.value ?? '{key}'}"`;
172
+ case 'scroll':
173
+ return `Scroll ${args.direction ?? '{direction}'} by ${args.pixels ?? '{pixels}'}px`;
174
+ case 'swipe':
175
+ return `Swipe ${args.direction ?? '{direction}'} by ${args.distance ?? '{distance}'}px`;
176
+ case 'upload':
177
+ return `Upload file "${args.files ?? args.value ?? args.file ?? '{file}'}" to ${el}`;
178
+ case 'intercept':
179
+ return `Intercept ${args.method ?? '{method}'} "${args.url ?? '{url}'}" mock rule registered`;
180
+ case 'clearCookies':
181
+ return `Clear all browser cookies`;
182
+ case 'setCookies':
183
+ return `Set cookies for "${args.url ?? '{url}'}"`;
184
+ case 'clearLocalStorage':
185
+ return `Clear browser localStorage`;
186
+ default:
187
+ return `Perform ${actionKey} on ${el}`;
188
+ }
189
+ }
190
+ /**
191
+ * Transforms raw matcher assertions into grammatically correct English sentences.
192
+ */
193
+ export function formatAssertion(assertionKey, target, args = {}) {
194
+ const el = formatElementTarget(target);
195
+ const col = formatCollectionTarget(target);
196
+ switch (assertionKey) {
197
+ case 'be.visible':
198
+ return `Expect ${el} to be visible`;
199
+ case 'not.be.visible':
200
+ return `Expect ${el} not to be visible`;
201
+ case 'exist':
202
+ return `Expect ${el} to exist in DOM`;
203
+ case 'not.exist':
204
+ return `Expect ${el} not to exist in DOM`;
205
+ case 'be.clickable':
206
+ return `Expect ${el} to be clickable`;
207
+ case 'not.be.clickable':
208
+ return `Expect ${el} not to be clickable`;
209
+ case 'be.enabled':
210
+ return `Expect ${el} to be enabled`;
211
+ case 'be.disabled':
212
+ return `Expect ${el} to be disabled`;
213
+ case 'be.checked':
214
+ return `Expect ${el} to be checked`;
215
+ case 'not.be.checked':
216
+ return `Expect ${el} not to be checked`;
217
+ case 'be.selected':
218
+ return `Expect ${el} to be selected`;
219
+ case 'not.be.selected':
220
+ return `Expect ${el} not to be selected`;
221
+ case 'be.focused':
222
+ return `Expect ${el} to be focused`;
223
+ case 'not.be.focused':
224
+ return `Expect ${el} not to be focused`;
225
+ case 'have.text':
226
+ return `Expect ${el} to have text "${args.expected ?? args.text ?? '{str}'}"`;
227
+ case 'not.have.text':
228
+ return `Expect ${el} not to have text "${args.expected ?? args.text ?? '{str}'}"`;
229
+ case 'contain.text':
230
+ return `Expect ${el} to contain text "${args.expected ?? args.text ?? '{sub}'}"`;
231
+ case 'not.contain.text':
232
+ return `Expect ${el} not to contain text "${args.expected ?? args.text ?? '{s}'}"`;
233
+ case 'have.value':
234
+ return `Expect ${el} to have value "${args.expected ?? args.value ?? '{val}'}"`;
235
+ case 'not.have.value':
236
+ return `Expect ${el} not to have value "${args.expected ?? args.value ?? '{val}'}"`;
237
+ case 'contain.value':
238
+ return `Expect ${el} to contain value "${args.expected ?? args.value ?? '{s}'}"`;
239
+ case 'not.contain.value':
240
+ return `Expect ${el} not to contain value "${args.expected ?? args.value ?? '{s}'}"`;
241
+ case 'have.attr':
242
+ return args.value !== undefined
243
+ ? `Expect ${el} to have attribute "${args.name ?? args.expected ?? '{k}'}" = "${args.value}"`
244
+ : `Expect ${el} to have attribute "${args.name ?? args.expected ?? '{k}'}"`;
245
+ case 'not.have.attr':
246
+ return `Expect ${el} not to have attribute "${args.name ?? args.expected ?? '{k}'}"`;
247
+ case 'have.class':
248
+ return `Expect ${el} to have class "${args.expected ?? args.className ?? '{cls}'}"`;
249
+ case 'not.have.class':
250
+ return `Expect ${el} not to have class "${args.expected ?? args.className ?? '{cls}'}"`;
251
+ case 'have.css':
252
+ return `Expect ${el} to have CSS "${args.name ?? args.property ?? '{prop}'}" = "${args.value ?? '{val}'}"`;
253
+ case 'not.have.css':
254
+ return `Expect ${el} not to have CSS "${args.name ?? args.property ?? '{p}'}" = "${args.value ?? '{v}'}"`;
255
+ // Collections
256
+ case 'have.length':
257
+ return `Expect ${col} count to equal ${args.expected ?? args.length ?? '{n}'}`;
258
+ case 'not.have.length':
259
+ return `Expect ${col} count not to equal ${args.expected ?? args.length ?? '{n}'}`;
260
+ case 'have.length.greaterThan':
261
+ return `Expect ${col} count to be greater than ${args.expected ?? args.min ?? '{n}'}`;
262
+ case 'have.length.lessThan':
263
+ return `Expect ${col} count to be less than ${args.expected ?? args.max ?? '{n}'}`;
264
+ case 'be.empty':
265
+ return `Expect ${col} to be empty`;
266
+ case 'not.be.empty':
267
+ return `Expect ${col} not to be empty`;
268
+ // Browser Context
269
+ case 'have.url':
270
+ case 'shouldHaveUrl':
271
+ return `Expect browser URL to be "${args.expected ?? args.url ?? '{url}'}"`;
272
+ case 'contain.url':
273
+ case 'shouldContainUrl':
274
+ return `Expect browser URL to contain "${args.expected ?? args.url ?? '{url}'}"`;
275
+ case 'have.title':
276
+ case 'shouldHaveTitle':
277
+ return `Expect browser title to be "${args.expected ?? args.title ?? '{title}'}"`;
278
+ case 'contain.title':
279
+ case 'shouldContainTitle':
280
+ return `Expect browser title to contain "${args.expected ?? args.title ?? '{title}'}"`;
281
+ case 'be.loaded':
282
+ case 'shouldBeLoaded':
283
+ case 'be.pageLoaded':
284
+ case 'shouldBePageLoaded':
285
+ return `Expect page to be fully loaded`;
286
+ case 'have.noConsoleErrors':
287
+ case 'shouldHaveNoConsoleErrors':
288
+ return `Expect browser console to have no errors`;
289
+ default:
290
+ return `Expect ${el} to match ${assertionKey}`;
291
+ }
292
+ }
293
+ /**
294
+ * Parses raw JSON action/assertion payloads or native orchestrator payloads into English sentences.
295
+ */
296
+ export function parseAndFormat(cmdText) {
297
+ try {
298
+ const payload = JSON.parse(cmdText);
299
+ const target = payload.target;
300
+ if (payload.type === 'action') {
301
+ return formatAction(payload.key, target, mapActionArgs(payload.key, payload.args || []));
302
+ }
303
+ else if (payload.type === 'assertion' || payload.type === 'browser') {
304
+ return formatAssertion(payload.key, target, mapAssertionArgs(payload.key, payload.args || []));
305
+ }
306
+ // Native Orchestrator payload format ({ test, action, selector, target, ... })
307
+ if (payload.action) {
308
+ const action = payload.action;
309
+ const t = payload.selector || payload.target;
310
+ switch (action) {
311
+ case 'navigate':
312
+ return formatAction('navigate', undefined, {
313
+ url: t || payload.url || payload.value || 'http://localhost:5173',
314
+ });
315
+ case 'click':
316
+ return formatAction('click', t);
317
+ case 'double_click':
318
+ case 'doubleClick':
319
+ return formatAction('doubleClick', t);
320
+ case 'right_click':
321
+ case 'rightClick':
322
+ return formatAction('rightClick', t);
323
+ case 'type':
324
+ case 'type_text':
325
+ case 'set_value':
326
+ return formatAction('type', t, { text: payload.text || payload.value });
327
+ case 'clear':
328
+ return formatAction('clear', t);
329
+ case 'hover':
330
+ return formatAction('hover', t);
331
+ case 'scroll_into_view':
332
+ case 'scrollIntoView':
333
+ return formatAction('scrollIntoView', t);
334
+ case 'should_be_visible':
335
+ return formatAssertion('be.visible', t);
336
+ case 'should_not_be_visible':
337
+ return formatAssertion('not.be.visible', t);
338
+ case 'should_exist':
339
+ return formatAssertion('exist', t);
340
+ case 'should_not_exist':
341
+ return formatAssertion('not.exist', t);
342
+ case 'should_be_enabled':
343
+ return formatAssertion('be.enabled', t);
344
+ case 'should_be_disabled':
345
+ return formatAssertion('be.disabled', t);
346
+ case 'should_be_clickable':
347
+ return formatAssertion('be.clickable', t);
348
+ case 'should_have_text':
349
+ return formatAssertion('have.text', t, { expected: payload.expected || payload.text || payload.value });
350
+ case 'should_contain_text':
351
+ return formatAssertion('contain.text', t, {
352
+ expected: payload.expected || payload.text || payload.value,
353
+ });
354
+ case 'should_have_value':
355
+ return formatAssertion('have.value', t, { expected: payload.expected || payload.value });
356
+ case 'should_contain_value':
357
+ return formatAssertion('contain.value', t, { expected: payload.expected || payload.value });
358
+ case 'should_have_length':
359
+ return formatAssertion('have.length', t, {
360
+ expected: payload.expected || payload.length || payload.value,
361
+ });
362
+ case 'should_have_title':
363
+ return formatAssertion('have.title', undefined, {
364
+ expected: payload.expected || payload.value || t || 'TestSpectra',
365
+ });
366
+ case 'should_be_empty':
367
+ return formatAssertion('be.empty', t);
368
+ case 'should_not_be_empty':
369
+ return formatAssertion('not.be.empty', t);
370
+ default:
371
+ return `Execute step "${action}" on ${t || 'target'}`;
372
+ }
373
+ }
374
+ return cmdText;
375
+ }
376
+ catch {
377
+ return cmdText;
378
+ }
379
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * @packageDocumentation
3
+ * # @testspectra/reporter
4
+ *
5
+ * Canonical semantic reporter, stream parser, and execution telemetry for TestSpectra.
6
+ */
7
+ export * from './types.js';
8
+ export * from './formatter.js';
9
+ export * from './parser.js';
10
+ export * from './reporter.js';
package/dist/index.js ADDED
@@ -0,0 +1,10 @@
1
+ /**
2
+ * @packageDocumentation
3
+ * # @testspectra/reporter
4
+ *
5
+ * Canonical semantic reporter, stream parser, and execution telemetry for TestSpectra.
6
+ */
7
+ export * from './types.js';
8
+ export * from './formatter.js';
9
+ export * from './parser.js';
10
+ export * from './reporter.js';
@@ -0,0 +1,61 @@
1
+ import type { ParsedLineResult, ParsedTestHeader, ParsedToken, TestLog } from './types.js';
2
+ /**
3
+ * Strips ANSI color and control escape sequences from a string.
4
+ */
5
+ export declare function stripAnsi(str: string): string;
6
+ /**
7
+ * Parses test headers in formats like:
8
+ * - "✓ ApiInterception / TC-0008-fetch-mocking (412ms)"
9
+ * - "✗ ApiInterception / TC-0009-post-mocking (65ms)"
10
+ * - "● Playground / TC-0001-form-controls"
11
+ * - "✓ TC-0008-fetch-mocking (412ms)"
12
+ */
13
+ export declare function parseTestHeader(rawText: string): ParsedTestHeader;
14
+ /**
15
+ * Detects whether a terminal stdout line belongs to an execution summary card or phase timing breakdown.
16
+ */
17
+ export declare function isSummaryLine(raw: string): boolean;
18
+ /**
19
+ * Checks whether a raw or cleaned line is an internal machine protocol token
20
+ * (e.g. `[TESTSPECTRA_STEP_START]`, `[TESTSPECTRA_STEP_PASS]`, `[TESTSPECTRA_STEP_FAIL]`,
21
+ * `[TESTSPECTRA_TEST_START]`, `[TESTSPECTRA_TEST_PASS]`, `[TESTSPECTRA_TEST_FAIL]`,
22
+ * `[TESTSPECTRA_TEST_ERROR]`, `[TESTSPECTRA_COMPLETE]`, `[TESTSPECTRA_NETWORK]`, `[TESTSPECTRA_DURATION]`).
23
+ */
24
+ export declare function isInternalProtocolLine(raw: string): boolean;
25
+ /**
26
+ * Real-Time Terminal Stdout Tokenizer & Log Factory.
27
+ */
28
+ export declare class TestOutputParser {
29
+ private _currentError;
30
+ /** Returns the most recently captured error message string. */
31
+ get currentError(): string;
32
+ /** Clears the current error state. */
33
+ clearError(): void;
34
+ /**
35
+ * Extracts optional worker tags such as `[0-0]`, `[0-1]`, `[worker-1]`, `[worker:0]`.
36
+ * Ensures internal tokens like `[TESTSPECTRA_...]` are never falsely classified as worker tags.
37
+ */
38
+ extractWorkerTag(line: string): {
39
+ workerId?: string;
40
+ remaining: string;
41
+ };
42
+ /**
43
+ * Parses a raw line of terminal output, stripping worker tags and extracting lifecycle tokens.
44
+ */
45
+ parseLineWithWorker(line: string): ParsedLineResult;
46
+ /**
47
+ * Evaluates a clean terminal line and extracts lifecycle tokens.
48
+ *
49
+ * @param line - Clean line text without worker prefixes.
50
+ * @returns The extracted `ParsedToken`.
51
+ */
52
+ parseLine(line: string): ParsedToken;
53
+ /**
54
+ * Factory creating a structured `TestLog` from a terminal output line.
55
+ *
56
+ * @param line - Raw line text.
57
+ * @param isError - Whether the line originated from stderr.
58
+ * @param executionId - Associated execution ID.
59
+ */
60
+ createTestLog(line: string, isError: boolean, executionId?: string): TestLog;
61
+ }
package/dist/parser.js ADDED
@@ -0,0 +1,238 @@
1
+ import { parseAndFormat } from './formatter.js';
2
+ /**
3
+ * Strips ANSI color and control escape sequences from a string.
4
+ */
5
+ export function stripAnsi(str) {
6
+ return str.replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, '');
7
+ }
8
+ /**
9
+ * Parses test headers in formats like:
10
+ * - "✓ ApiInterception / TC-0008-fetch-mocking (412ms)"
11
+ * - "✗ ApiInterception / TC-0009-post-mocking (65ms)"
12
+ * - "● Playground / TC-0001-form-controls"
13
+ * - "✓ TC-0008-fetch-mocking (412ms)"
14
+ */
15
+ export function parseTestHeader(rawText) {
16
+ const clean = stripAnsi(rawText)
17
+ .replace(/^\[TESTSPECTRA_TEST_(START|PASS|FAIL)\]/, '')
18
+ .replace(/^[✓✗●✔✖]\s*/, '')
19
+ .trim();
20
+ const matchDuration = clean.match(/\(([^)]+)ms\)$/);
21
+ const durationMs = matchDuration ? parseInt(matchDuration[1], 10) : undefined;
22
+ const titlePart = clean.replace(/\s*\([^)]+\)$/, '').trim();
23
+ if (titlePart.includes(' / ')) {
24
+ const parts = titlePart.split(' / ');
25
+ const suiteName = parts[0].trim();
26
+ const testCaseId = parts.slice(1).join(' / ').trim();
27
+ return {
28
+ suiteName: suiteName || undefined,
29
+ testCaseId: testCaseId || titlePart,
30
+ durationMs,
31
+ };
32
+ }
33
+ return {
34
+ testCaseId: titlePart,
35
+ durationMs,
36
+ };
37
+ }
38
+ /**
39
+ * Detects whether a terminal stdout line belongs to an execution summary card or phase timing breakdown.
40
+ */
41
+ export function isSummaryLine(raw) {
42
+ const clean = stripAnsi(raw).trim();
43
+ return (clean.includes('TESTSPECTRA EXECUTION SUMMARY') ||
44
+ clean.includes('TestSpectra Execution Summary') ||
45
+ clean.includes('EXECUTION TIMING BREAKDOWN') ||
46
+ clean.startsWith('╭') ||
47
+ clean.startsWith('├') ||
48
+ clean.startsWith('╰') ||
49
+ clean.startsWith('┌') ||
50
+ clean.startsWith('└') ||
51
+ clean.startsWith('│') ||
52
+ /^\d+\.\s+(Master Chromium|Worker Spawning|Incognito Context|Total Test Execution|Context Teardown)/.test(clean));
53
+ }
54
+ /**
55
+ * Checks whether a raw or cleaned line is an internal machine protocol token
56
+ * (e.g. `[TESTSPECTRA_STEP_START]`, `[TESTSPECTRA_STEP_PASS]`, `[TESTSPECTRA_STEP_FAIL]`,
57
+ * `[TESTSPECTRA_TEST_START]`, `[TESTSPECTRA_TEST_PASS]`, `[TESTSPECTRA_TEST_FAIL]`,
58
+ * `[TESTSPECTRA_TEST_ERROR]`, `[TESTSPECTRA_COMPLETE]`, `[TESTSPECTRA_NETWORK]`, `[TESTSPECTRA_DURATION]`).
59
+ */
60
+ export function isInternalProtocolLine(raw) {
61
+ const clean = stripAnsi(raw).trim();
62
+ return /^\[TESTSPECTRA_[A-Z0-9_]+\]/i.test(clean);
63
+ }
64
+ /**
65
+ * Real-Time Terminal Stdout Tokenizer & Log Factory.
66
+ */
67
+ export class TestOutputParser {
68
+ _currentError = '';
69
+ /** Returns the most recently captured error message string. */
70
+ get currentError() {
71
+ return this._currentError;
72
+ }
73
+ /** Clears the current error state. */
74
+ clearError() {
75
+ this._currentError = '';
76
+ }
77
+ /**
78
+ * Extracts optional worker tags such as `[0-0]`, `[0-1]`, `[worker-1]`, `[worker:0]`.
79
+ * Ensures internal tokens like `[TESTSPECTRA_...]` are never falsely classified as worker tags.
80
+ */
81
+ extractWorkerTag(line) {
82
+ const raw = stripAnsi(line).trim();
83
+ if (/^\[(TESTSPECTRA|Network|TestSpectra)/i.test(raw)) {
84
+ return { workerId: undefined, remaining: raw };
85
+ }
86
+ const match = raw.match(/^\[(\d+-\d+|worker[:_-]?\w+|\d+)\]\s*(.*)$/i);
87
+ if (match) {
88
+ return { workerId: match[1], remaining: match[2] };
89
+ }
90
+ return { workerId: undefined, remaining: raw };
91
+ }
92
+ /**
93
+ * Parses a raw line of terminal output, stripping worker tags and extracting lifecycle tokens.
94
+ */
95
+ parseLineWithWorker(line) {
96
+ const { workerId, remaining } = this.extractWorkerTag(line);
97
+ const token = this.parseLine(remaining);
98
+ return { workerId, token, cleanLine: remaining };
99
+ }
100
+ /**
101
+ * Evaluates a clean terminal line and extracts lifecycle tokens.
102
+ *
103
+ * @param line - Clean line text without worker prefixes.
104
+ * @returns The extracted `ParsedToken`.
105
+ */
106
+ parseLine(line) {
107
+ const raw = stripAnsi(line).trim();
108
+ if (!raw)
109
+ return { type: 'none' };
110
+ // 1. Check for summary cards or timing tables
111
+ if (isSummaryLine(raw)) {
112
+ return { type: 'summary_banner', text: raw };
113
+ }
114
+ // 2. Spec file announcement: "RUNNING in <path>"
115
+ const runningMatch = raw.match(/RUNNING in (.+)/);
116
+ if (runningMatch) {
117
+ let specPath = runningMatch[1].trim();
118
+ specPath = specPath.replace(/\s+-\s+[a-zA-Z0-9_-]+$/, '').trim();
119
+ specPath = specPath.replace(/^[a-zA-Z0-9_-]+\s+-\s+/, '').trim();
120
+ return { type: 'spec_file', specPath };
121
+ }
122
+ // 3. Step passed: "✔ <stepText> (<duration>ms)" or "[TESTSPECTRA_STEP_PASS]"
123
+ if (raw.startsWith('✔ ') || raw.startsWith('[TESTSPECTRA_STEP_PASS]')) {
124
+ const fullText = raw
125
+ .replace('[TESTSPECTRA_STEP_PASS]', '')
126
+ .replace(/^✔\s*/, '')
127
+ .trim();
128
+ const match = fullText.match(/\(([^)]+)ms\)$/);
129
+ const durationMs = match ? parseInt(match[1], 10) : undefined;
130
+ const cmdText = fullText.replace(/\s*\([^)]+\)$/, '').trim();
131
+ const formatted = parseAndFormat(cmdText);
132
+ return { type: 'step_pass', stepText: formatted, durationMs };
133
+ }
134
+ // 4. Step failed: "✖ <stepText> (<duration>ms)" or "[TESTSPECTRA_STEP_FAIL]"
135
+ if (raw.startsWith('✖ ') || raw.startsWith('[TESTSPECTRA_STEP_FAIL]')) {
136
+ const fullText = raw
137
+ .replace('[TESTSPECTRA_STEP_FAIL]', '')
138
+ .replace(/^✖\s*/, '')
139
+ .trim();
140
+ const match = fullText.match(/\(([^)]+)ms\)$/);
141
+ const durationMs = match ? parseInt(match[1], 10) : undefined;
142
+ const cmdText = fullText.replace(/\s*\([^)]+\)$/, '').trim();
143
+ const formatted = parseAndFormat(cmdText);
144
+ return { type: 'step_fail', stepText: formatted, durationMs };
145
+ }
146
+ // 5. Test error: "[TESTSPECTRA_TEST_ERROR]" or "↳ Error: <msg>"
147
+ if (raw.startsWith('[TESTSPECTRA_TEST_ERROR]') || raw.includes('↳ Error:')) {
148
+ const message = raw
149
+ .replace('[TESTSPECTRA_TEST_ERROR]', '')
150
+ .replace(/.*↳\s*Error:\s*/, '')
151
+ .trim();
152
+ this._currentError = message;
153
+ return { type: 'test_error', message };
154
+ }
155
+ // 6. Test started: "[TESTSPECTRA_TEST_START]" or "● <title>"
156
+ if (raw.startsWith('[TESTSPECTRA_TEST_START]') || raw.startsWith('● ')) {
157
+ const content = raw
158
+ .replace('[TESTSPECTRA_TEST_START]', '')
159
+ .replace(/^●\s*/, '')
160
+ .trim();
161
+ try {
162
+ const parsed = JSON.parse(content);
163
+ if (parsed && typeof parsed === 'object') {
164
+ const title = parsed.title || parsed.id || '';
165
+ const id = typeof parsed.id === 'string' ? parsed.id : undefined;
166
+ const testFilePath = typeof parsed.testFilePath === 'string' ? parsed.testFilePath : undefined;
167
+ const workerId = typeof parsed.workerId === 'number' ? parsed.workerId : undefined;
168
+ const header = parseTestHeader(title);
169
+ return {
170
+ type: 'test_start',
171
+ title,
172
+ suiteName: header.suiteName,
173
+ testCaseId: header.testCaseId,
174
+ id,
175
+ testFilePath,
176
+ workerId,
177
+ };
178
+ }
179
+ }
180
+ catch { }
181
+ const header = parseTestHeader(content);
182
+ return {
183
+ type: 'test_start',
184
+ title: content,
185
+ suiteName: header.suiteName,
186
+ testCaseId: header.testCaseId,
187
+ };
188
+ }
189
+ // 7. Test passed: "[TESTSPECTRA_TEST_PASS]" or "✓ <title> (<duration>ms)"
190
+ if (raw.startsWith('[TESTSPECTRA_TEST_PASS]') || raw.startsWith('✓ ')) {
191
+ const header = parseTestHeader(raw);
192
+ this._currentError = '';
193
+ return {
194
+ type: 'test_pass',
195
+ title: header.testCaseId,
196
+ suiteName: header.suiteName,
197
+ testCaseId: header.testCaseId,
198
+ durationMs: header.durationMs,
199
+ };
200
+ }
201
+ // 8. Test failed: "[TESTSPECTRA_TEST_FAIL]" or "✗ <title> (<duration>ms)"
202
+ if (raw.startsWith('[TESTSPECTRA_TEST_FAIL]') || raw.startsWith('✗ ')) {
203
+ const header = parseTestHeader(raw);
204
+ return {
205
+ type: 'test_fail',
206
+ title: header.testCaseId,
207
+ suiteName: header.suiteName,
208
+ testCaseId: header.testCaseId,
209
+ durationMs: header.durationMs,
210
+ };
211
+ }
212
+ return { type: 'none' };
213
+ }
214
+ /**
215
+ * Factory creating a structured `TestLog` from a terminal output line.
216
+ *
217
+ * @param line - Raw line text.
218
+ * @param isError - Whether the line originated from stderr.
219
+ * @param executionId - Associated execution ID.
220
+ */
221
+ createTestLog(line, isError, executionId) {
222
+ const now = new Date();
223
+ const timeStr = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`;
224
+ let level = isError ? 'ERROR' : 'INFO';
225
+ if (line.includes('✓') || line.includes('✔') || line.includes('PASSED'))
226
+ level = 'SUCCESS';
227
+ else if (line.includes('✗') || line.includes('✖') || line.includes('FAILED'))
228
+ level = 'ERROR';
229
+ else if (line.includes('WARN') || line.includes('Warning:'))
230
+ level = 'WARNING';
231
+ return {
232
+ timestamp: timeStr,
233
+ level,
234
+ message: line,
235
+ runId: executionId,
236
+ };
237
+ }
238
+ }
@@ -0,0 +1,32 @@
1
+ import type { ExecutionSummary, NetworkResource, PendingTestCase, ReporterOptions, TestLog, TestRunResult } from './types.js';
2
+ export declare class SemanticReporter {
3
+ protected logs: TestLog[];
4
+ protected networkEvents: NetworkResource[];
5
+ protected pendingTests: Map<string, PendingTestCase>;
6
+ protected workerToTest: Map<number, string>;
7
+ protected pendingErrorMsg?: string;
8
+ protected passedCount: number;
9
+ protected failedCount: number;
10
+ protected options: ReporterOptions;
11
+ constructor(options?: ReporterOptions);
12
+ /**
13
+ * Resolves an active pending test case by test title or worker ID.
14
+ */
15
+ resolveTestCase(testTitle?: string, workerId?: number): PendingTestCase | undefined;
16
+ /**
17
+ * Ingests a raw string line or structured TestLog entry from stdout/stderr.
18
+ */
19
+ addLog(input: string | TestLog): void;
20
+ /**
21
+ * Returns a snapshot of currently running pending test cases.
22
+ */
23
+ getPendingTests(): PendingTestCase[];
24
+ /**
25
+ * Returns aggregated test run telemetry results.
26
+ */
27
+ getResult(): TestRunResult;
28
+ /**
29
+ * Computes an execution summary.
30
+ */
31
+ getSummary(reportLocation?: string): ExecutionSummary;
32
+ }
@@ -0,0 +1,270 @@
1
+ import { parseAndFormat } from './formatter.js';
2
+ import { stripAnsi } from './parser.js';
3
+ export class SemanticReporter {
4
+ logs = [];
5
+ networkEvents = [];
6
+ pendingTests = new Map();
7
+ workerToTest = new Map();
8
+ pendingErrorMsg;
9
+ passedCount = 0;
10
+ failedCount = 0;
11
+ options;
12
+ constructor(options = {}) {
13
+ this.options = options;
14
+ }
15
+ /**
16
+ * Resolves an active pending test case by test title or worker ID.
17
+ */
18
+ resolveTestCase(testTitle, workerId) {
19
+ if (testTitle && this.pendingTests.has(testTitle)) {
20
+ return this.pendingTests.get(testTitle);
21
+ }
22
+ if (workerId !== undefined && this.workerToTest.has(workerId)) {
23
+ const title = this.workerToTest.get(workerId);
24
+ if (title && this.pendingTests.has(title)) {
25
+ return this.pendingTests.get(title);
26
+ }
27
+ }
28
+ if (this.pendingTests.size === 1) {
29
+ return this.pendingTests.values().next().value;
30
+ }
31
+ return Array.from(this.pendingTests.values()).pop();
32
+ }
33
+ /**
34
+ * Ingests a raw string line or structured TestLog entry from stdout/stderr.
35
+ */
36
+ addLog(input) {
37
+ const log = typeof input === 'string'
38
+ ? {
39
+ timestamp: new Date().toISOString(),
40
+ level: 'INFO',
41
+ message: input,
42
+ }
43
+ : input;
44
+ this.logs.push(log);
45
+ this.options.onLog?.(log);
46
+ const raw = stripAnsi(log.message || '').trim();
47
+ if (!raw)
48
+ return;
49
+ // Filter out WDIO framework internal noise
50
+ if (raw.includes('RUNNING in chrome') ||
51
+ raw.includes('PASSED in chrome') ||
52
+ raw.includes('FAILED in chrome') ||
53
+ raw.includes('[NetworkRecorder]') ||
54
+ raw.includes('"spec" Reporter:') ||
55
+ raw.includes('"DotReporter" Reporter:') ||
56
+ raw.includes('SilentReporter') ||
57
+ raw.startsWith('---') ||
58
+ raw === '.' ||
59
+ raw.includes('Session ID:') ||
60
+ raw.includes('Running: chrome') ||
61
+ raw.includes('[chrome') ||
62
+ raw.includes('[firefox') ||
63
+ raw.includes('[safari') ||
64
+ raw.includes('[edge') ||
65
+ raw.includes('(root)') ||
66
+ raw.includes('passing (') ||
67
+ raw.includes('failing (') ||
68
+ raw.includes('Spec Files:') ||
69
+ (raw.includes('Execution of') && raw.includes('workers started')) ||
70
+ raw.includes('Spawning WDIO execution') ||
71
+ raw.includes('WARN webdriver:') ||
72
+ raw.includes('WebDriverError: unknown error: net::ERR_CONNECTION_REFUSED') ||
73
+ raw.includes('when running "url" with method "POST"') ||
74
+ raw.includes('Session info: chrome=')) {
75
+ return;
76
+ }
77
+ // 1. Test started -> initialize test buffer & worker slot
78
+ if (raw.startsWith('[TESTSPECTRA_TEST_START]')) {
79
+ const content = raw.replace('[TESTSPECTRA_TEST_START]', '').trim();
80
+ let title = content;
81
+ let workerId;
82
+ try {
83
+ const parsed = JSON.parse(content);
84
+ if (parsed.title) {
85
+ title = parsed.title;
86
+ workerId = parsed.workerId;
87
+ }
88
+ }
89
+ catch { }
90
+ const pending = {
91
+ title,
92
+ workerId,
93
+ steps: [],
94
+ currentStep: 'Running suite...',
95
+ startTime: Date.now(),
96
+ };
97
+ this.pendingTests.set(title, pending);
98
+ if (workerId !== undefined) {
99
+ this.workerToTest.set(workerId, title);
100
+ }
101
+ this.options.onTestStart?.(pending);
102
+ return;
103
+ }
104
+ // 2. Step in-progress / start
105
+ if (raw.startsWith('[TESTSPECTRA_STEP_START]')) {
106
+ const fullText = raw.replace('[TESTSPECTRA_STEP_START]', '').trim();
107
+ let testTitle = '';
108
+ let workerId;
109
+ try {
110
+ const payload = JSON.parse(fullText);
111
+ testTitle = payload.test || payload.title || '';
112
+ workerId = payload.workerId;
113
+ }
114
+ catch { }
115
+ const formatted = parseAndFormat(fullText);
116
+ const testCase = this.resolveTestCase(testTitle, workerId);
117
+ if (testCase) {
118
+ testCase.currentStep = formatted;
119
+ }
120
+ this.options.onStepStart?.(testTitle || testCase?.title || '', formatted, workerId ?? testCase?.workerId);
121
+ return;
122
+ }
123
+ // 3. Step passed -> record step to matching test case
124
+ if (raw.startsWith('[TESTSPECTRA_STEP_PASS]')) {
125
+ const fullText = raw.replace('[TESTSPECTRA_STEP_PASS]', '').trim();
126
+ const match = fullText.match(/\(([^)]+)ms\)$/);
127
+ const duration = match ? parseInt(match[1], 10) : 0;
128
+ const cmdText = fullText.replace(/\s*\([^)]+\)$/, '').trim();
129
+ const formatted = parseAndFormat(cmdText);
130
+ let testTitle = '';
131
+ let workerId;
132
+ try {
133
+ const payload = JSON.parse(cmdText);
134
+ testTitle = payload.test || payload.title || '';
135
+ workerId = payload.workerId;
136
+ }
137
+ catch { }
138
+ const testCase = this.resolveTestCase(testTitle, workerId);
139
+ const step = { text: formatted, duration, passed: true };
140
+ if (testCase) {
141
+ testCase.steps.push(step);
142
+ testCase.currentStep = formatted;
143
+ }
144
+ this.options.onStepPass?.(testTitle || testCase?.title || '', step, workerId ?? testCase?.workerId);
145
+ return;
146
+ }
147
+ // 4. Step failed -> record failed step to matching test case
148
+ if (raw.startsWith('[TESTSPECTRA_STEP_FAIL]')) {
149
+ const fullText = raw.replace('[TESTSPECTRA_STEP_FAIL]', '').trim();
150
+ const match = fullText.match(/\(([^)]+)ms\)$/);
151
+ const duration = match ? parseInt(match[1], 10) : 0;
152
+ const cmdText = fullText.replace(/\s*\([^)]+\)$/, '').trim();
153
+ const formatted = parseAndFormat(cmdText);
154
+ let testTitle = '';
155
+ let workerId;
156
+ try {
157
+ const payload = JSON.parse(cmdText);
158
+ testTitle = payload.test || payload.title || '';
159
+ workerId = payload.workerId;
160
+ }
161
+ catch { }
162
+ const testCase = this.resolveTestCase(testTitle, workerId);
163
+ const step = { text: formatted, duration, passed: false };
164
+ if (testCase) {
165
+ testCase.steps.push(step);
166
+ testCase.currentStep = formatted;
167
+ }
168
+ this.options.onStepFail?.(testTitle || testCase?.title || '', step, workerId ?? testCase?.workerId);
169
+ return;
170
+ }
171
+ // 5. Test error detail
172
+ if (raw.startsWith('[TESTSPECTRA_TEST_ERROR]')) {
173
+ const err = raw.replace('[TESTSPECTRA_TEST_ERROR]', '').trim();
174
+ const latest = Array.from(this.pendingTests.values()).pop();
175
+ if (latest) {
176
+ latest.errorMsg = latest.errorMsg ? `${latest.errorMsg}\n${err}` : err;
177
+ }
178
+ else {
179
+ this.pendingErrorMsg = this.pendingErrorMsg ? `${this.pendingErrorMsg}\n${err}` : err;
180
+ }
181
+ this.options.onTestError?.(err, latest?.title, latest?.workerId);
182
+ return;
183
+ }
184
+ // 6. Test passed completely -> flush atomic test block
185
+ if (raw.startsWith('[TESTSPECTRA_TEST_PASS]')) {
186
+ const fullText = raw.replace('[TESTSPECTRA_TEST_PASS]', '').trim();
187
+ const match = fullText.match(/\(([^)]+)ms\)$/);
188
+ const duration = match ? parseInt(match[1], 10) : 0;
189
+ const title = fullText.replace(/\s*\([^)]+\)$/, '').trim();
190
+ this.passedCount++;
191
+ const testCase = this.pendingTests.get(title) || this.resolveTestCase(title);
192
+ const steps = testCase?.steps || [];
193
+ const workerId = testCase?.workerId;
194
+ this.pendingTests.delete(title);
195
+ if (workerId !== undefined) {
196
+ this.workerToTest.delete(workerId);
197
+ }
198
+ this.options.onTestPass?.({ title, duration, steps, workerId });
199
+ return;
200
+ }
201
+ // 7. Test failed completely -> flush atomic failed test block
202
+ if (raw.startsWith('[TESTSPECTRA_TEST_FAIL]')) {
203
+ const fullText = raw.replace('[TESTSPECTRA_TEST_FAIL]', '').trim();
204
+ const match = fullText.match(/\(([^)]+)ms\)$/);
205
+ const duration = match ? parseInt(match[1], 10) : 0;
206
+ const title = fullText.replace(/\s*\([^)]+\)$/, '').trim();
207
+ this.failedCount++;
208
+ const testCase = this.pendingTests.get(title) || this.resolveTestCase(title);
209
+ const steps = testCase?.steps || [];
210
+ const errorMsg = testCase?.errorMsg || this.pendingErrorMsg;
211
+ const workerId = testCase?.workerId;
212
+ this.pendingTests.delete(title);
213
+ if (workerId !== undefined) {
214
+ this.workerToTest.delete(workerId);
215
+ }
216
+ this.pendingErrorMsg = undefined;
217
+ this.options.onTestFail?.({ title, duration, steps, errorMsg, workerId });
218
+ return;
219
+ }
220
+ // 8. Test duration token
221
+ if (raw.startsWith('[TESTSPECTRA_DURATION]')) {
222
+ return;
223
+ }
224
+ // 9. Network resource telemetry
225
+ if (raw.startsWith('[TESTSPECTRA_NETWORK]')) {
226
+ try {
227
+ const jsonStr = raw.replace('[TESTSPECTRA_NETWORK]', '').trim();
228
+ const payload = JSON.parse(jsonStr);
229
+ this.networkEvents.push(payload);
230
+ this.options.onNetwork?.(payload);
231
+ }
232
+ catch { }
233
+ return;
234
+ }
235
+ }
236
+ /**
237
+ * Returns a snapshot of currently running pending test cases.
238
+ */
239
+ getPendingTests() {
240
+ return Array.from(this.pendingTests.values());
241
+ }
242
+ /**
243
+ * Returns aggregated test run telemetry results.
244
+ */
245
+ getResult() {
246
+ const hasFailures = this.failedCount > 0;
247
+ return {
248
+ status: hasFailures ? 'failed' : 'passed',
249
+ duration: '0s',
250
+ passedCount: this.passedCount,
251
+ failedCount: this.failedCount,
252
+ logs: this.logs,
253
+ networkResources: this.networkEvents.length > 0 ? this.networkEvents : undefined,
254
+ };
255
+ }
256
+ /**
257
+ * Computes an execution summary.
258
+ */
259
+ getSummary(reportLocation = '.testspectra/reports/result.json') {
260
+ const totalTests = this.passedCount + this.failedCount;
261
+ return {
262
+ totalSuites: 1,
263
+ totalTests,
264
+ passedCount: this.passedCount,
265
+ failedCount: this.failedCount,
266
+ durationMs: 0,
267
+ reportLocation,
268
+ };
269
+ }
270
+ }
@@ -0,0 +1,138 @@
1
+ export type ActionKey = 'click' | 'doubleClick' | 'rightClick' | 'type' | 'clear' | 'select' | 'selectOption' | 'hover' | 'dragDrop' | 'scrollIntoView' | 'longPress' | 'waitForElement' | 'navigate' | 'back' | 'forward' | 'refresh' | 'setViewport' | 'wait' | 'pressKey' | 'scroll' | 'swipe' | 'upload' | 'intercept' | 'clearCookies' | 'setCookies' | 'clearLocalStorage' | 'mount';
2
+ export type AssertionKey = 'be.visible' | 'not.be.visible' | 'exist' | 'not.exist' | 'be.clickable' | 'not.be.clickable' | 'be.enabled' | 'be.disabled' | 'be.checked' | 'not.be.checked' | 'be.selected' | 'not.be.selected' | 'be.focused' | 'not.be.focused' | 'have.text' | 'not.have.text' | 'contain.text' | 'not.contain.text' | 'have.value' | 'not.have.value' | 'contain.value' | 'not.contain.value' | 'have.attr' | 'not.have.attr' | 'have.class' | 'not.have.class' | 'have.css' | 'not.have.css' | 'have.length' | 'not.have.length' | 'have.length.greaterThan' | 'have.length.lessThan' | 'be.empty' | 'not.be.empty' | 'shouldHaveUrl' | 'shouldContainUrl' | 'shouldHaveTitle' | 'shouldContainTitle' | 'shouldBeLoaded' | 'shouldBePageLoaded' | 'shouldHaveNoConsoleErrors' | 'have.url' | 'contain.url' | 'have.title' | 'contain.title' | 'be.loaded' | 'be.pageLoaded' | 'have.noConsoleErrors';
3
+ export type TestLogLevel = 'INFO' | 'SUCCESS' | 'WARNING' | 'ERROR' | 'DEBUG';
4
+ export interface TestLog {
5
+ timestamp: string;
6
+ level: TestLogLevel;
7
+ message: string;
8
+ runId?: string;
9
+ }
10
+ export interface NetworkResource {
11
+ requestId: string;
12
+ url: string;
13
+ method: string;
14
+ status: number;
15
+ type?: string;
16
+ statusText?: string;
17
+ domain?: string;
18
+ protocol?: string;
19
+ timing?: {
20
+ waiting: number;
21
+ download: number;
22
+ };
23
+ size?: number;
24
+ transferSize?: number;
25
+ priority?: string;
26
+ cached?: boolean;
27
+ initiator?: string;
28
+ remoteIP?: string;
29
+ connectionId?: string;
30
+ startTime?: number;
31
+ runId?: string;
32
+ testTitle?: string;
33
+ }
34
+ export interface PendingStep {
35
+ text: string;
36
+ duration: number;
37
+ passed: boolean;
38
+ }
39
+ export interface PendingTestCase {
40
+ title: string;
41
+ workerId?: number;
42
+ steps: PendingStep[];
43
+ currentStep?: string;
44
+ errorMsg?: string;
45
+ videoPath?: string;
46
+ startTime: number;
47
+ }
48
+ export interface TestRunResult {
49
+ status: 'passed' | 'failed' | 'error' | string;
50
+ duration: string;
51
+ passedCount: number;
52
+ failedCount: number;
53
+ logs: TestLog[];
54
+ networkResources?: NetworkResource[];
55
+ }
56
+ export interface ExecutionSummary {
57
+ totalSuites: number;
58
+ totalTests: number;
59
+ passedCount: number;
60
+ failedCount: number;
61
+ durationMs: number;
62
+ reportLocation: string;
63
+ }
64
+ export interface ParsedTestHeader {
65
+ suiteName?: string;
66
+ testCaseId: string;
67
+ durationMs?: number;
68
+ }
69
+ export type ParsedToken = {
70
+ type: 'spec_file';
71
+ specPath: string;
72
+ } | {
73
+ type: 'test_start';
74
+ title: string;
75
+ suiteName?: string;
76
+ testCaseId?: string;
77
+ id?: string;
78
+ testFilePath?: string;
79
+ workerId?: number;
80
+ } | {
81
+ type: 'test_error';
82
+ message: string;
83
+ } | {
84
+ type: 'test_pass';
85
+ title: string;
86
+ suiteName?: string;
87
+ testCaseId?: string;
88
+ durationMs?: number;
89
+ } | {
90
+ type: 'test_fail';
91
+ title: string;
92
+ suiteName?: string;
93
+ testCaseId?: string;
94
+ durationMs?: number;
95
+ } | {
96
+ type: 'step_pass';
97
+ stepText: string;
98
+ durationMs?: number;
99
+ } | {
100
+ type: 'step_fail';
101
+ stepText: string;
102
+ durationMs?: number;
103
+ } | {
104
+ type: 'summary_banner';
105
+ text: string;
106
+ } | {
107
+ type: 'none';
108
+ };
109
+ export interface ParsedLineResult {
110
+ workerId?: string;
111
+ token: ParsedToken;
112
+ cleanLine: string;
113
+ }
114
+ export interface ReporterCallbacks {
115
+ onTestStart?: (test: PendingTestCase) => void;
116
+ onStepStart?: (testTitle: string, stepText: string, workerId?: number) => void;
117
+ onStepPass?: (testTitle: string, step: PendingStep, workerId?: number) => void;
118
+ onStepFail?: (testTitle: string, step: PendingStep, workerId?: number) => void;
119
+ onTestPass?: (test: {
120
+ title: string;
121
+ duration: number;
122
+ steps: PendingStep[];
123
+ workerId?: number;
124
+ }) => void;
125
+ onTestFail?: (test: {
126
+ title: string;
127
+ duration: number;
128
+ steps: PendingStep[];
129
+ errorMsg?: string;
130
+ workerId?: number;
131
+ }) => void;
132
+ onTestError?: (errorMessage: string, testTitle?: string, workerId?: number) => void;
133
+ onNetwork?: (resource: NetworkResource) => void;
134
+ onLog?: (log: TestLog) => void;
135
+ }
136
+ export interface ReporterOptions extends ReporterCallbacks {
137
+ isInteractive?: boolean;
138
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@testspectra/reporter",
3
+ "version": "1.1.13",
4
+ "description": "Canonical semantic reporter, output stream parser, and execution telemetry for TestSpectra",
5
+ "files": [
6
+ "dist",
7
+ "LICENSE.md"
8
+ ],
9
+ "main": "dist/index.js",
10
+ "types": "dist/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.js",
15
+ "default": "./dist/index.js"
16
+ }
17
+ },
18
+ "devDependencies": {
19
+ "@types/node": "^20.14.0",
20
+ "typescript": "^5.6.3",
21
+ "vitest": "^2.1.8"
22
+ },
23
+ "type": "module",
24
+ "license": "SEE LICENSE IN LICENSE.md",
25
+ "scripts": {
26
+ "build": "tsc",
27
+ "watch": "tsc -w",
28
+ "typecheck": "tsc --noEmit",
29
+ "test": "vitest run"
30
+ }
31
+ }