@wp-tester/results 0.0.5 → 0.1.1

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 (48) hide show
  1. package/dist/diff-utils.d.ts +27 -0
  2. package/dist/diff-utils.d.ts.map +1 -0
  3. package/dist/diff-utils.js +116 -0
  4. package/dist/diff-utils.js.map +1 -0
  5. package/dist/diff-utils.spec.d.ts +5 -0
  6. package/dist/diff-utils.spec.d.ts.map +1 -0
  7. package/dist/diff-utils.spec.js +85 -0
  8. package/dist/diff-utils.spec.js.map +1 -0
  9. package/dist/index.d.ts +7 -3
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +4 -0
  12. package/dist/index.js.map +1 -1
  13. package/dist/merge.d.ts.map +1 -1
  14. package/dist/merge.js +21 -2
  15. package/dist/merge.js.map +1 -1
  16. package/dist/merge.spec.js +230 -0
  17. package/dist/merge.spec.js.map +1 -1
  18. package/dist/phpunit-streaming-reporter.d.ts +19 -0
  19. package/dist/phpunit-streaming-reporter.d.ts.map +1 -0
  20. package/dist/phpunit-streaming-reporter.js +26 -0
  21. package/dist/phpunit-streaming-reporter.js.map +1 -0
  22. package/dist/spinner.d.ts +20 -0
  23. package/dist/spinner.d.ts.map +1 -0
  24. package/dist/spinner.js +36 -0
  25. package/dist/spinner.js.map +1 -0
  26. package/dist/streaming.d.ts +36 -0
  27. package/dist/streaming.d.ts.map +1 -1
  28. package/dist/streaming.js +250 -56
  29. package/dist/streaming.js.map +1 -1
  30. package/dist/streaming.spec.js +115 -2
  31. package/dist/streaming.spec.js.map +1 -1
  32. package/dist/summary.d.ts +12 -1
  33. package/dist/summary.d.ts.map +1 -1
  34. package/dist/summary.js +40 -6
  35. package/dist/summary.js.map +1 -1
  36. package/dist/teamcity-parser.d.ts.map +1 -1
  37. package/dist/teamcity-parser.js +19 -1
  38. package/dist/teamcity-parser.js.map +1 -1
  39. package/dist/tsconfig.tsbuildinfo +1 -1
  40. package/dist/vitest-streaming-reporter.d.ts +3 -3
  41. package/dist/vitest-streaming-reporter.d.ts.map +1 -1
  42. package/dist/vitest-streaming-reporter.js +63 -4
  43. package/dist/vitest-streaming-reporter.js.map +1 -1
  44. package/dist/vitest-streaming.d.ts +19 -0
  45. package/dist/vitest-streaming.d.ts.map +1 -0
  46. package/dist/vitest-streaming.js +28 -0
  47. package/dist/vitest-streaming.js.map +1 -0
  48. 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
- * Spinner frames for animated loader
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 || suite.tests.some(t => t.status === "running")) {
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 (const suite of file.suites) {
188
- this.renderSuite(suite, lines);
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(suite, lines) {
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
- // Render tests
208
- for (const test of suite.tests) {
209
- this.renderTest(test, suite.depth + 1, lines);
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 ? pc.dim(` ${formatDuration(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 ? pc.dim(` ${formatDuration(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 indentedMessage = test.message
234
- .split("\n")
235
- .map((line) => `${messageIndent}${pc.red(line)}`)
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 indentedTrace = test.trace
242
- .split("\n")
243
- .map((line) => `${traceIndent}${pc.dim(line)}`)
244
- .join("\n");
245
- lines.push(indentedTrace);
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 ? ` ${pc.dim(`(${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
- lines.push(`${indent}${pc.yellow("○")} ${pc.dim(test.name)}`);
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: toolName || "wp-tester",
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 ? this.getOrCreateFile(fileId) : this.getOrCreateFile("__global__");
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 ? this.state.files.get(fileId) : this.state.files.get("__global__");
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 ? this.getOrCreateFile(fileId) : this.getOrCreateFile("__global__");
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
- suite.tests.push({
400
- name,
401
- suiteName,
402
- status: "running",
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 ? this.getOrCreateFile(fileId) : this.getOrCreateFile("__global__");
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 - try exact match first, then fallback to name-only match
418
- let test = suite.tests.find(t => t.name === name && t.suiteName === suiteName);
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 to find by name only (handles cases where suiteName might differ)
421
- test = suite.tests.find(t => t.name === name && t.status === "running");
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 ? this.getOrCreateFile(fileId) : this.getOrCreateFile("__global__");
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 - try exact match first, then fallback to name-only match
451
- let test = suite.tests.find(t => t.name === name && t.suiteName === suiteName);
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 to find by name only (handles cases where suiteName might differ)
454
- test = suite.tests.find(t => t.name === name && t.status === "running");
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 ? this.getOrCreateFile(fileId) : this.getOrCreateFile("__global__");
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 - try exact match first, then fallback to name-only match
488
- let test = suite.tests.find(t => t.name === name && t.suiteName === suiteName);
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 to find by name only (handles cases where suiteName might differ)
491
- test = suite.tests.find(t => t.name === name && t.status === "running");
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 ? this.getOrCreateFile(fileId) : this.getOrCreateFile("__global__");
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 ? `${test.suiteName}::${test.name}` : test.name,
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: 0,
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
  }