@wp-tester/results 0.0.5 → 0.1.0
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/diff-utils.d.ts +27 -0
- package/dist/diff-utils.d.ts.map +1 -0
- package/dist/diff-utils.js +116 -0
- package/dist/diff-utils.js.map +1 -0
- package/dist/diff-utils.spec.d.ts +5 -0
- package/dist/diff-utils.spec.d.ts.map +1 -0
- package/dist/diff-utils.spec.js +85 -0
- package/dist/diff-utils.spec.js.map +1 -0
- package/dist/index.d.ts +7 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/merge.d.ts.map +1 -1
- package/dist/merge.js +21 -2
- package/dist/merge.js.map +1 -1
- package/dist/merge.spec.js +230 -0
- package/dist/merge.spec.js.map +1 -1
- package/dist/phpunit-streaming-reporter.d.ts +19 -0
- package/dist/phpunit-streaming-reporter.d.ts.map +1 -0
- package/dist/phpunit-streaming-reporter.js +26 -0
- package/dist/phpunit-streaming-reporter.js.map +1 -0
- package/dist/spinner.d.ts +20 -0
- package/dist/spinner.d.ts.map +1 -0
- package/dist/spinner.js +36 -0
- package/dist/spinner.js.map +1 -0
- package/dist/streaming.d.ts +36 -0
- package/dist/streaming.d.ts.map +1 -1
- package/dist/streaming.js +250 -56
- package/dist/streaming.js.map +1 -1
- package/dist/streaming.spec.js +115 -2
- package/dist/streaming.spec.js.map +1 -1
- package/dist/summary.d.ts +12 -1
- package/dist/summary.d.ts.map +1 -1
- package/dist/summary.js +40 -6
- package/dist/summary.js.map +1 -1
- package/dist/teamcity-parser.d.ts.map +1 -1
- package/dist/teamcity-parser.js +19 -1
- package/dist/teamcity-parser.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/vitest-streaming-reporter.d.ts +3 -3
- package/dist/vitest-streaming-reporter.d.ts.map +1 -1
- package/dist/vitest-streaming-reporter.js +63 -4
- package/dist/vitest-streaming-reporter.js.map +1 -1
- package/dist/vitest-streaming.d.ts +19 -0
- package/dist/vitest-streaming.d.ts.map +1 -0
- package/dist/vitest-streaming.js +28 -0
- package/dist/vitest-streaming.js.map +1 -0
- package/package.json +1 -1
package/dist/streaming.js
CHANGED
|
@@ -8,10 +8,8 @@
|
|
|
8
8
|
* parallel test execution without output interference.
|
|
9
9
|
*/
|
|
10
10
|
import pc from "picocolors";
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
*/
|
|
14
|
-
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
11
|
+
import { applyDiffHighlighting } from "./diff-utils.js";
|
|
12
|
+
import { SPINNER_FRAMES } from "./spinner.js";
|
|
15
13
|
/**
|
|
16
14
|
* Default stdout writer
|
|
17
15
|
*/
|
|
@@ -55,6 +53,14 @@ export class StreamingReporter {
|
|
|
55
53
|
this.showRunBoundaries = options.showRunBoundaries ?? true;
|
|
56
54
|
this.showSummary = options.showSummary ?? true;
|
|
57
55
|
this.enabled = options.enabled ?? true;
|
|
56
|
+
// Default filter: show all statuses (for backwards compatibility)
|
|
57
|
+
this.filter = options.filter ?? {
|
|
58
|
+
passed: true,
|
|
59
|
+
failed: true,
|
|
60
|
+
skipped: true,
|
|
61
|
+
pending: true,
|
|
62
|
+
other: true,
|
|
63
|
+
};
|
|
58
64
|
this.state = {
|
|
59
65
|
files: new Map(),
|
|
60
66
|
toolName: "wp-tester",
|
|
@@ -63,8 +69,17 @@ export class StreamingReporter {
|
|
|
63
69
|
passedTests: 0,
|
|
64
70
|
failedTests: 0,
|
|
65
71
|
skippedTests: 0,
|
|
72
|
+
pendingTests: 0,
|
|
73
|
+
isRunning: false,
|
|
66
74
|
};
|
|
67
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* Generate filter command for re-running a specific test
|
|
78
|
+
* Override in subclasses for framework-specific behavior
|
|
79
|
+
*/
|
|
80
|
+
getFilterCommand(_testName, _suiteName) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
68
83
|
/**
|
|
69
84
|
* Enable or disable run boundaries (header and summary)
|
|
70
85
|
*/
|
|
@@ -154,7 +169,8 @@ export class StreamingReporter {
|
|
|
154
169
|
let hasRunningTests = false;
|
|
155
170
|
for (const file of this.state.files.values()) {
|
|
156
171
|
for (const suite of file.suites) {
|
|
157
|
-
if (suite.isLoading ||
|
|
172
|
+
if (suite.isLoading ||
|
|
173
|
+
suite.tests.some((t) => t.status === "running")) {
|
|
158
174
|
hasRunningTests = true;
|
|
159
175
|
break;
|
|
160
176
|
}
|
|
@@ -180,22 +196,66 @@ export class StreamingReporter {
|
|
|
180
196
|
this.stopSpinner();
|
|
181
197
|
}
|
|
182
198
|
}
|
|
199
|
+
/**
|
|
200
|
+
* Check if a suite or any of its descendants has visible content
|
|
201
|
+
*/
|
|
202
|
+
hasVisibleContent(file, suiteIndex) {
|
|
203
|
+
const suite = file.suites[suiteIndex];
|
|
204
|
+
// Check if suite is loading (always visible during loading)
|
|
205
|
+
if (suite.isLoading && suite.tests.length === 0) {
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
// Check if suite has any visible tests
|
|
209
|
+
const hasVisibleTests = suite.tests.some(test => this.shouldShowStatus(test.status));
|
|
210
|
+
if (hasVisibleTests) {
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
213
|
+
// Check if any child suites (with depth > current depth) have visible content
|
|
214
|
+
const currentDepth = suite.depth;
|
|
215
|
+
for (let i = suiteIndex + 1; i < file.suites.length; i++) {
|
|
216
|
+
const nextSuite = file.suites[i];
|
|
217
|
+
// If we've returned to the same or lower depth, we've exited the children
|
|
218
|
+
if (nextSuite.depth <= currentDepth) {
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
// Only check immediate children (depth = current + 1)
|
|
222
|
+
if (nextSuite.depth === currentDepth + 1) {
|
|
223
|
+
if (this.hasVisibleContent(file, i)) {
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
183
230
|
/**
|
|
184
231
|
* Render a file's tests to output lines
|
|
185
232
|
*/
|
|
186
233
|
renderFile(file, lines) {
|
|
187
|
-
for (
|
|
188
|
-
this.renderSuite(
|
|
234
|
+
for (let i = 0; i < file.suites.length; i++) {
|
|
235
|
+
this.renderSuite(file, i, lines);
|
|
189
236
|
}
|
|
190
237
|
}
|
|
191
238
|
/**
|
|
192
239
|
* Render a suite and its tests
|
|
193
240
|
*/
|
|
194
|
-
renderSuite(
|
|
241
|
+
renderSuite(file, suiteIndex, lines) {
|
|
242
|
+
const suite = file.suites[suiteIndex];
|
|
195
243
|
const indent = " ".repeat(suite.depth);
|
|
196
244
|
// Only show spinner if suite is loading AND has no tests yet
|
|
197
245
|
// Don't show spinner if tests have been reported (loading is complete)
|
|
198
246
|
const showSpinner = suite.isLoading && suite.tests.length === 0;
|
|
247
|
+
// Collect visible tests first to determine if suite should be shown
|
|
248
|
+
const visibleTestLines = [];
|
|
249
|
+
for (const test of suite.tests) {
|
|
250
|
+
if (this.shouldShowStatus(test.status)) {
|
|
251
|
+
this.renderTest(test, suite.depth + 1, visibleTestLines);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
// Check if this suite has visible content (tests or child suites with content)
|
|
255
|
+
if (!this.hasVisibleContent(file, suiteIndex)) {
|
|
256
|
+
return; // Skip rendering this suite entirely
|
|
257
|
+
}
|
|
258
|
+
// Render the suite name
|
|
199
259
|
if (showSpinner) {
|
|
200
260
|
const spinner = pc.cyan(SPINNER_FRAMES[this.spinnerFrame]);
|
|
201
261
|
lines.push(`${indent}${spinner} ${pc.bold(suite.name)}`);
|
|
@@ -204,10 +264,26 @@ export class StreamingReporter {
|
|
|
204
264
|
// Use two spaces to replace the spinner character and space, preventing text shift
|
|
205
265
|
lines.push(`${indent} ${pc.bold(suite.name)}`);
|
|
206
266
|
}
|
|
207
|
-
//
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
267
|
+
// Add the visible test lines
|
|
268
|
+
lines.push(...visibleTestLines);
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Check if a test status should be displayed based on filter settings
|
|
272
|
+
*/
|
|
273
|
+
shouldShowStatus(status) {
|
|
274
|
+
// Always show running tests (transient state)
|
|
275
|
+
if (status === "running") {
|
|
276
|
+
return true;
|
|
277
|
+
}
|
|
278
|
+
// Map status to filter property
|
|
279
|
+
const filterMap = {
|
|
280
|
+
passed: "passed",
|
|
281
|
+
failed: "failed",
|
|
282
|
+
skipped: "skipped",
|
|
283
|
+
pending: "pending",
|
|
284
|
+
};
|
|
285
|
+
const filterKey = filterMap[status];
|
|
286
|
+
return this.filter[filterKey] ?? false;
|
|
211
287
|
}
|
|
212
288
|
/**
|
|
213
289
|
* Render a single test
|
|
@@ -221,38 +297,96 @@ export class StreamingReporter {
|
|
|
221
297
|
break;
|
|
222
298
|
}
|
|
223
299
|
case "passed": {
|
|
224
|
-
const durationStr = test.duration
|
|
300
|
+
const durationStr = test.duration
|
|
301
|
+
? pc.dim(` ${formatDuration(test.duration)}`)
|
|
302
|
+
: "";
|
|
225
303
|
lines.push(`${indent}${pc.green("✓")} ${test.name}${durationStr}`);
|
|
226
304
|
break;
|
|
227
305
|
}
|
|
228
306
|
case "failed": {
|
|
229
|
-
const durationStr = test.duration
|
|
307
|
+
const durationStr = test.duration
|
|
308
|
+
? pc.dim(` ${formatDuration(test.duration)}`)
|
|
309
|
+
: "";
|
|
230
310
|
lines.push(`${indent}${pc.red("✗")} ${test.name}${durationStr}`);
|
|
231
311
|
if (test.message) {
|
|
232
312
|
const messageIndent = " ".repeat(depth + 1);
|
|
233
|
-
const
|
|
234
|
-
.
|
|
235
|
-
|
|
236
|
-
.join("\n");
|
|
237
|
-
lines.push(indentedMessage);
|
|
313
|
+
for (const line of test.message.split("\n")) {
|
|
314
|
+
lines.push(`${messageIndent}${pc.red(line)}`);
|
|
315
|
+
}
|
|
238
316
|
}
|
|
239
317
|
if (test.trace) {
|
|
240
318
|
const traceIndent = " ".repeat(depth + 1);
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
.
|
|
245
|
-
|
|
319
|
+
const traceLines = test.trace.split("\n");
|
|
320
|
+
for (let i = 0; i < traceLines.length; i++) {
|
|
321
|
+
const line = traceLines[i];
|
|
322
|
+
const trimmed = line.trim();
|
|
323
|
+
// Detect Expected/Actual labels and color the values (supports multiline values)
|
|
324
|
+
if (trimmed === "Expected:" && i + 1 < traceLines.length) {
|
|
325
|
+
lines.push(`${traceIndent}${pc.dim("Expected:")}`);
|
|
326
|
+
i++; // Move to the first value line
|
|
327
|
+
// Continue processing lines until we hit Actual:, an empty line, or end of trace
|
|
328
|
+
while (i < traceLines.length) {
|
|
329
|
+
const valueLine = traceLines[i];
|
|
330
|
+
const valueTrimmed = valueLine.trim();
|
|
331
|
+
// Stop if we hit the Actual: label or empty line
|
|
332
|
+
if (valueTrimmed === "Actual:" || valueTrimmed === "") {
|
|
333
|
+
i--; // Back up so the outer loop can process this line
|
|
334
|
+
break;
|
|
335
|
+
}
|
|
336
|
+
if (valueTrimmed) {
|
|
337
|
+
lines.push(`${traceIndent}${applyDiffHighlighting(valueLine, pc.red)}`);
|
|
338
|
+
}
|
|
339
|
+
i++;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
else if (trimmed === "Actual:" && i + 1 < traceLines.length) {
|
|
343
|
+
lines.push(`${traceIndent}${pc.dim("Actual:")}`);
|
|
344
|
+
i++; // Move to the first value line
|
|
345
|
+
// Continue processing lines until we hit an empty line or end of trace
|
|
346
|
+
while (i < traceLines.length) {
|
|
347
|
+
const valueLine = traceLines[i];
|
|
348
|
+
const valueTrimmed = valueLine.trim();
|
|
349
|
+
// Stop if we hit an empty line
|
|
350
|
+
if (valueTrimmed === "") {
|
|
351
|
+
i--; // Back up so the outer loop can process this line
|
|
352
|
+
break;
|
|
353
|
+
}
|
|
354
|
+
if (valueTrimmed) {
|
|
355
|
+
lines.push(`${traceIndent}${applyDiffHighlighting(valueLine, pc.green)}`);
|
|
356
|
+
}
|
|
357
|
+
i++;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
else if (trimmed) {
|
|
361
|
+
// File paths, context lines, other trace info
|
|
362
|
+
lines.push(`${traceIndent}${pc.dim(line)}`);
|
|
363
|
+
}
|
|
364
|
+
else {
|
|
365
|
+
// Empty lines (preserve them for spacing)
|
|
366
|
+
lines.push("");
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
// Add filter to re-run this specific test
|
|
370
|
+
if (test.name) {
|
|
371
|
+
const filterCmd = this.getFilterCommand(test.name, test.suiteName);
|
|
372
|
+
if (filterCmd) {
|
|
373
|
+
lines.push(`${traceIndent}${pc.dim("Re-run only this test by appending:")}`);
|
|
374
|
+
lines.push(`${traceIndent}${pc.dim(filterCmd)}`);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
246
377
|
}
|
|
247
378
|
break;
|
|
248
379
|
}
|
|
249
380
|
case "skipped": {
|
|
250
|
-
const reasonStr = test.message
|
|
381
|
+
const reasonStr = test.message
|
|
382
|
+
? ` ${pc.dim(`(${test.message})`)}`
|
|
383
|
+
: ` ${pc.dim("(Skipped)")}`;
|
|
251
384
|
lines.push(`${indent}${pc.yellow("○")} ${pc.dim(test.name)}${reasonStr}`);
|
|
252
385
|
break;
|
|
253
386
|
}
|
|
254
387
|
case "pending": {
|
|
255
|
-
|
|
388
|
+
const message = test.message ? ` ${pc.dim(`(${test.message})`)}` : "";
|
|
389
|
+
lines.push(`${indent}${pc.yellow("◔")} ${pc.dim(test.name)}${message}`);
|
|
256
390
|
break;
|
|
257
391
|
}
|
|
258
392
|
}
|
|
@@ -278,7 +412,7 @@ export class StreamingReporter {
|
|
|
278
412
|
*/
|
|
279
413
|
getOrCreateSuite(file, suiteName) {
|
|
280
414
|
// Find existing suite
|
|
281
|
-
let suite = file.suites.find(s => s.name === suiteName);
|
|
415
|
+
let suite = file.suites.find((s) => s.name === suiteName);
|
|
282
416
|
if (!suite) {
|
|
283
417
|
suite = {
|
|
284
418
|
name: suiteName,
|
|
@@ -290,18 +424,33 @@ export class StreamingReporter {
|
|
|
290
424
|
}
|
|
291
425
|
return suite;
|
|
292
426
|
}
|
|
427
|
+
/**
|
|
428
|
+
* Get display name from tool name
|
|
429
|
+
*/
|
|
430
|
+
getDisplayName(toolName) {
|
|
431
|
+
const nameMap = {
|
|
432
|
+
"wp-tester-phpunit": "PHPUnit Tests",
|
|
433
|
+
"wp-tester-smoke": "WordPress Tests",
|
|
434
|
+
"wp-tester": "Tests",
|
|
435
|
+
};
|
|
436
|
+
return nameMap[toolName] || toolName;
|
|
437
|
+
}
|
|
293
438
|
/**
|
|
294
439
|
* Called when test run starts
|
|
295
440
|
*/
|
|
296
441
|
onRunStart(toolName) {
|
|
442
|
+
const tool = toolName || "wp-tester";
|
|
297
443
|
this.state = {
|
|
298
444
|
files: new Map(),
|
|
299
|
-
toolName:
|
|
445
|
+
toolName: tool,
|
|
446
|
+
displayName: this.getDisplayName(tool),
|
|
300
447
|
startTime: Date.now(),
|
|
301
448
|
totalTests: 0,
|
|
302
449
|
passedTests: 0,
|
|
303
450
|
failedTests: 0,
|
|
304
451
|
skippedTests: 0,
|
|
452
|
+
pendingTests: 0,
|
|
453
|
+
isRunning: true,
|
|
305
454
|
};
|
|
306
455
|
this.lastOutputLineCount = 0;
|
|
307
456
|
}
|
|
@@ -310,6 +459,7 @@ export class StreamingReporter {
|
|
|
310
459
|
*/
|
|
311
460
|
onRunEnd() {
|
|
312
461
|
this.stopSpinner();
|
|
462
|
+
this.state.isRunning = false;
|
|
313
463
|
// Clean up any tests still in "running" state across all files
|
|
314
464
|
// This ensures we never show spinners in the final output
|
|
315
465
|
for (const file of this.state.files.values()) {
|
|
@@ -318,6 +468,9 @@ export class StreamingReporter {
|
|
|
318
468
|
for (const test of suite.tests) {
|
|
319
469
|
if (test.status === "running") {
|
|
320
470
|
test.status = "pending";
|
|
471
|
+
test.message = test.message || "Did not complete";
|
|
472
|
+
this.state.pendingTests++;
|
|
473
|
+
this.state.totalTests++;
|
|
321
474
|
}
|
|
322
475
|
}
|
|
323
476
|
}
|
|
@@ -347,7 +500,9 @@ export class StreamingReporter {
|
|
|
347
500
|
* Called when a test suite starts
|
|
348
501
|
*/
|
|
349
502
|
onSuiteStart(name, fileId) {
|
|
350
|
-
const file = fileId
|
|
503
|
+
const file = fileId
|
|
504
|
+
? this.getOrCreateFile(fileId)
|
|
505
|
+
: this.getOrCreateFile("__global__");
|
|
351
506
|
file.currentSuiteStack.push(name);
|
|
352
507
|
const suite = this.getOrCreateSuite(file, name);
|
|
353
508
|
// When a child suite is created, mark all parent suites as not loading
|
|
@@ -363,7 +518,9 @@ export class StreamingReporter {
|
|
|
363
518
|
* Called when a test suite ends
|
|
364
519
|
*/
|
|
365
520
|
onSuiteEnd(name, fileId) {
|
|
366
|
-
const file = fileId
|
|
521
|
+
const file = fileId
|
|
522
|
+
? this.state.files.get(fileId)
|
|
523
|
+
: this.state.files.get("__global__");
|
|
367
524
|
if (file) {
|
|
368
525
|
const index = file.currentSuiteStack.lastIndexOf(name);
|
|
369
526
|
if (index !== -1) {
|
|
@@ -371,7 +528,7 @@ export class StreamingReporter {
|
|
|
371
528
|
}
|
|
372
529
|
// Mark suite as no longer loading when it ends
|
|
373
530
|
// This ensures loaders are removed even if no tests were reported
|
|
374
|
-
const suite = file.suites.find(s => s.name === name);
|
|
531
|
+
const suite = file.suites.find((s) => s.name === name);
|
|
375
532
|
if (suite) {
|
|
376
533
|
suite.isLoading = false;
|
|
377
534
|
// Clean up any tests still in "running" state - they should be marked as pending
|
|
@@ -379,6 +536,9 @@ export class StreamingReporter {
|
|
|
379
536
|
for (const test of suite.tests) {
|
|
380
537
|
if (test.status === "running") {
|
|
381
538
|
test.status = "pending";
|
|
539
|
+
test.message = test.message || "Did not complete";
|
|
540
|
+
this.state.pendingTests++;
|
|
541
|
+
this.state.totalTests++;
|
|
382
542
|
}
|
|
383
543
|
}
|
|
384
544
|
}
|
|
@@ -389,42 +549,55 @@ export class StreamingReporter {
|
|
|
389
549
|
* Called when a test starts
|
|
390
550
|
*/
|
|
391
551
|
onTestStart(name, suiteName, fileId) {
|
|
392
|
-
const file = fileId
|
|
552
|
+
const file = fileId
|
|
553
|
+
? this.getOrCreateFile(fileId)
|
|
554
|
+
: this.getOrCreateFile("__global__");
|
|
393
555
|
const suite = this.getOrCreateSuite(file, suiteName || "Tests");
|
|
394
556
|
// Mark ALL suites in this file as no longer loading once any test starts
|
|
395
557
|
// This handles nested suite structures where parent suites don't directly contain tests
|
|
396
558
|
for (const s of file.suites) {
|
|
397
559
|
s.isLoading = false;
|
|
398
560
|
}
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
561
|
+
// Check if test already exists (completion event arrived before start event)
|
|
562
|
+
const existingTest = suite.tests.find((t) => t.name === name && t.suiteName === suiteName);
|
|
563
|
+
if (!existingTest) {
|
|
564
|
+
// Test doesn't exist yet - create it in running state
|
|
565
|
+
suite.tests.push({
|
|
566
|
+
name,
|
|
567
|
+
suiteName,
|
|
568
|
+
status: "running",
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
// If test already exists with a completed status, don't change it
|
|
404
572
|
this.render();
|
|
405
573
|
}
|
|
406
574
|
/**
|
|
407
575
|
* Called when a test passes
|
|
408
576
|
*/
|
|
409
577
|
onTestPass(name, duration, suiteName, fileId) {
|
|
410
|
-
const file = fileId
|
|
578
|
+
const file = fileId
|
|
579
|
+
? this.getOrCreateFile(fileId)
|
|
580
|
+
: this.getOrCreateFile("__global__");
|
|
411
581
|
const suite = this.getOrCreateSuite(file, suiteName || "Tests");
|
|
412
582
|
// Mark ALL suites in this file as no longer loading once any test completes
|
|
413
583
|
// This handles nested suite structures where parent suites don't directly contain tests
|
|
414
584
|
for (const s of file.suites) {
|
|
415
585
|
s.isLoading = false;
|
|
416
586
|
}
|
|
417
|
-
// Find and update the test
|
|
418
|
-
|
|
587
|
+
// Find and update the test
|
|
588
|
+
// Look for a test in "running" state first (most common case - completion follows start)
|
|
589
|
+
let test = suite.tests.find((t) => t.name === name && t.status === "running");
|
|
419
590
|
if (!test) {
|
|
420
|
-
// Fallback: try
|
|
421
|
-
test = suite.tests.find(t => t.name === name && t.
|
|
591
|
+
// Fallback: try exact match with suiteName (handles case where test completed before start event)
|
|
592
|
+
test = suite.tests.find((t) => t.name === name && t.suiteName === suiteName);
|
|
422
593
|
}
|
|
423
594
|
if (test) {
|
|
424
595
|
test.status = "passed";
|
|
425
596
|
test.duration = duration;
|
|
597
|
+
test.suiteName = suiteName; // Ensure suiteName is set
|
|
426
598
|
}
|
|
427
599
|
else {
|
|
600
|
+
// Test start event hasn't arrived yet - create the test with completed state
|
|
428
601
|
suite.tests.push({
|
|
429
602
|
name,
|
|
430
603
|
suiteName,
|
|
@@ -440,26 +613,31 @@ export class StreamingReporter {
|
|
|
440
613
|
* Called when a test fails
|
|
441
614
|
*/
|
|
442
615
|
onTestFail(name, duration, message, trace, suiteName, fileId) {
|
|
443
|
-
const file = fileId
|
|
616
|
+
const file = fileId
|
|
617
|
+
? this.getOrCreateFile(fileId)
|
|
618
|
+
: this.getOrCreateFile("__global__");
|
|
444
619
|
const suite = this.getOrCreateSuite(file, suiteName || "Tests");
|
|
445
620
|
// Mark ALL suites in this file as no longer loading once any test completes
|
|
446
621
|
// This handles nested suite structures where parent suites don't directly contain tests
|
|
447
622
|
for (const s of file.suites) {
|
|
448
623
|
s.isLoading = false;
|
|
449
624
|
}
|
|
450
|
-
// Find and update the test
|
|
451
|
-
|
|
625
|
+
// Find and update the test
|
|
626
|
+
// Look for a test in "running" state first (most common case - completion follows start)
|
|
627
|
+
let test = suite.tests.find((t) => t.name === name && t.status === "running");
|
|
452
628
|
if (!test) {
|
|
453
|
-
// Fallback: try
|
|
454
|
-
test = suite.tests.find(t => t.name === name && t.
|
|
629
|
+
// Fallback: try exact match with suiteName (handles case where test completed before start event)
|
|
630
|
+
test = suite.tests.find((t) => t.name === name && t.suiteName === suiteName);
|
|
455
631
|
}
|
|
456
632
|
if (test) {
|
|
457
633
|
test.status = "failed";
|
|
458
634
|
test.duration = duration;
|
|
459
635
|
test.message = message;
|
|
460
636
|
test.trace = trace;
|
|
637
|
+
test.suiteName = suiteName; // Ensure suiteName is set
|
|
461
638
|
}
|
|
462
639
|
else {
|
|
640
|
+
// Test start event hasn't arrived yet - create the test with completed state
|
|
463
641
|
suite.tests.push({
|
|
464
642
|
name,
|
|
465
643
|
suiteName,
|
|
@@ -477,24 +655,29 @@ export class StreamingReporter {
|
|
|
477
655
|
* Called when a test is skipped
|
|
478
656
|
*/
|
|
479
657
|
onTestSkip(name, reason, suiteName, fileId) {
|
|
480
|
-
const file = fileId
|
|
658
|
+
const file = fileId
|
|
659
|
+
? this.getOrCreateFile(fileId)
|
|
660
|
+
: this.getOrCreateFile("__global__");
|
|
481
661
|
const suite = this.getOrCreateSuite(file, suiteName || "Tests");
|
|
482
662
|
// Mark ALL suites in this file as no longer loading once any test completes
|
|
483
663
|
// This handles nested suite structures where parent suites don't directly contain tests
|
|
484
664
|
for (const s of file.suites) {
|
|
485
665
|
s.isLoading = false;
|
|
486
666
|
}
|
|
487
|
-
// Find and update the test
|
|
488
|
-
|
|
667
|
+
// Find and update the test
|
|
668
|
+
// Look for a test in "running" state first (most common case - completion follows start)
|
|
669
|
+
let test = suite.tests.find((t) => t.name === name && t.status === "running");
|
|
489
670
|
if (!test) {
|
|
490
|
-
// Fallback: try
|
|
491
|
-
test = suite.tests.find(t => t.name === name && t.
|
|
671
|
+
// Fallback: try exact match with suiteName (handles case where test completed before start event)
|
|
672
|
+
test = suite.tests.find((t) => t.name === name && t.suiteName === suiteName);
|
|
492
673
|
}
|
|
493
674
|
if (test) {
|
|
494
675
|
test.status = "skipped";
|
|
495
676
|
test.message = reason;
|
|
677
|
+
test.suiteName = suiteName; // Ensure suiteName is set
|
|
496
678
|
}
|
|
497
679
|
else {
|
|
680
|
+
// Test start event hasn't arrived yet - create the test with completed state
|
|
498
681
|
suite.tests.push({
|
|
499
682
|
name,
|
|
500
683
|
suiteName,
|
|
@@ -510,12 +693,14 @@ export class StreamingReporter {
|
|
|
510
693
|
* Called when a test is pending
|
|
511
694
|
*/
|
|
512
695
|
onTestPending(name, suiteName, fileId) {
|
|
513
|
-
const file = fileId
|
|
696
|
+
const file = fileId
|
|
697
|
+
? this.getOrCreateFile(fileId)
|
|
698
|
+
: this.getOrCreateFile("__global__");
|
|
514
699
|
const suite = this.getOrCreateSuite(file, suiteName || "Tests");
|
|
515
700
|
// Mark suite as no longer loading once first test completes
|
|
516
701
|
suite.isLoading = false;
|
|
517
702
|
// Find and update the test
|
|
518
|
-
const test = suite.tests.find(t => t.name === name && t.suiteName === suiteName);
|
|
703
|
+
const test = suite.tests.find((t) => t.name === name && t.suiteName === suiteName);
|
|
519
704
|
if (test) {
|
|
520
705
|
test.status = "pending";
|
|
521
706
|
}
|
|
@@ -543,9 +728,15 @@ export class StreamingReporter {
|
|
|
543
728
|
if (this.state.skippedTests > 0) {
|
|
544
729
|
this.writer.writeLine(pc.yellow(` ○ ${this.state.skippedTests} skipped`));
|
|
545
730
|
}
|
|
731
|
+
if (this.state.pendingTests > 0) {
|
|
732
|
+
this.writer.writeLine(pc.yellow(` ◔ ${this.state.pendingTests} pending`));
|
|
733
|
+
}
|
|
546
734
|
this.writer.writeLine("");
|
|
547
735
|
this.writer.writeLine(pc.dim(` ${this.state.totalTests} tests in ${formatDuration(duration)}`));
|
|
548
736
|
this.writer.writeLine("");
|
|
737
|
+
// Print icon legend
|
|
738
|
+
this.writer.writeLine(pc.dim(" Legend: ✓ passed ✗ failed ○ skipped ◔ pending"));
|
|
739
|
+
this.writer.writeLine("");
|
|
549
740
|
}
|
|
550
741
|
/**
|
|
551
742
|
* Get the current report in CTRF format
|
|
@@ -557,7 +748,9 @@ export class StreamingReporter {
|
|
|
557
748
|
for (const suite of file.suites) {
|
|
558
749
|
for (const test of suite.tests) {
|
|
559
750
|
const ctrf = {
|
|
560
|
-
name: test.suiteName
|
|
751
|
+
name: test.suiteName
|
|
752
|
+
? `${test.suiteName}::${test.name}`
|
|
753
|
+
: test.name,
|
|
561
754
|
status: test.status === "running" ? "other" : test.status,
|
|
562
755
|
duration: test.duration || 0,
|
|
563
756
|
};
|
|
@@ -583,7 +776,7 @@ export class StreamingReporter {
|
|
|
583
776
|
passed: this.state.passedTests,
|
|
584
777
|
failed: this.state.failedTests,
|
|
585
778
|
skipped: this.state.skippedTests,
|
|
586
|
-
pending:
|
|
779
|
+
pending: this.state.pendingTests,
|
|
587
780
|
other: 0,
|
|
588
781
|
start: this.state.startTime,
|
|
589
782
|
stop: Date.now(),
|
|
@@ -601,6 +794,7 @@ export class StreamingReporter {
|
|
|
601
794
|
passed: this.state.passedTests,
|
|
602
795
|
failed: this.state.failedTests,
|
|
603
796
|
skipped: this.state.skippedTests,
|
|
797
|
+
pending: this.state.pendingTests,
|
|
604
798
|
};
|
|
605
799
|
}
|
|
606
800
|
}
|