@molecule/api-ai-tools 1.10.10 → 1.11.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/README.md CHANGED
@@ -3,7 +3,7 @@ AUTO-GENERATED — DO NOT EDIT THIS FILE.
3
3
  Generated by `mlcl sync-docs` from the package's src/index.ts JSDoc + mlcl/registry.json.
4
4
  Edits here are overwritten on the next commit (molecule's pre-commit hook regenerates).
5
5
  To change this document, edit the module-level JSDoc in src/index.ts.
6
- Generated: 2026-10-20T14:43:37.900Z
6
+ Generated: 2026-10-20T18:19:55.686Z
7
7
  -->
8
8
 
9
9
  # @molecule/api-ai-tools
@@ -705,6 +705,81 @@ interface TraceHiddenBox {
705
705
  }
706
706
  ```
707
707
 
708
+ #### `TraceCountMatch`
709
+
710
+ One element a failing locator count matched, read from the trace's own failure snapshot.
711
+
712
+ A count assertion reports only `Expected: 3 / Received: 4`; the element that made it 4 is not
713
+ in the error. Playwright's trace does hold it — either marked `__playwright_target__` in the
714
+ failure snapshot (`expect(locator).toHaveCount(n)`), or recoverable by replaying the recorded
715
+ selector against the snapshot's DOM in a browser (`locator.count()` then `expect(n).toBe(k)`) —
716
+ so each match is named by what it is and where it sits, never by a guess.
717
+
718
+ ```typescript
719
+ interface TraceCountMatch {
720
+ /** The element's tag, lowercased (`div`). */
721
+ tag: string
722
+ /** Its `class` attribute, bounded (`card-block`). */
723
+ classes?: string
724
+ /** Its own text, whitespace-collapsed and bounded. */
725
+ text?: string
726
+ /** Its ancestors, outermost first (`body > div.comment-form > div.card`), bounded. */
727
+ path?: string
728
+ /** True when a `<form>` is one of its ancestors — the m321 extra match's shape. */
729
+ inForm?: true
730
+ }
731
+ ```
732
+
733
+ #### `TraceCountMismatch`
734
+
735
+ A failing assertion that compared a LOCATOR COUNT (`expect(await locator.count()).toBe(n)` or
736
+ `expect(locator).toHaveCount(n)`), with the locator the trace recorded and, when the failure
737
+ snapshot can supply them, the elements it matched.
738
+
739
+ xr-molecule-flash-m321's `comments.spec.ts:86` sat on `Expected: 3 / Received: 4` for ~30
740
+ minutes and three whole suite walks: the helper counted `.card:not(.comment-form) .card-block`,
741
+ the app's comment form was a `.comment-form` wrapper around a plain `.card`, and the form's own
742
+ `.card-block` made the fourth. The trace had the selector (its `Frame.queryCount` entry) and the
743
+ four elements (its failure snapshot); the note printed neither. This reading prints both, and
744
+ fails closed — a selector outside the browser's own CSS support, a missing browser, or a replay
745
+ that disagrees with the recorded count names no elements rather than inventing them.
746
+
747
+ ```typescript
748
+ interface TraceCountMismatch {
749
+ /** The locator as the trace or the assertion printed it (`locator('.card:not(…')`). */
750
+ locator: string
751
+ /** The selector inside that locator, when the trace recorded one. */
752
+ selector?: string
753
+ /** The number the assertion expected. */
754
+ expected: number
755
+ /** The number the page matched. */
756
+ received: number
757
+ /**
758
+ * Where the locator came from: the assertion's own message, or the `locator.count()` entry the
759
+ * trace recorded just before the failing assertion.
760
+ */
761
+ source: 'assertion' | 'locator-count'
762
+ /** The matched elements, bounded to {@link MAX_COUNT_MATCHES} in document order. */
763
+ matched?: TraceCountMatch[]
764
+ /** How many elements matched in total, before the display bound. */
765
+ matchedTotal?: number
766
+ /**
767
+ * How the elements were read: Playwright's own `__playwright_target__` marks in the failure
768
+ * snapshot, or the recorded selector replayed against that snapshot in a browser.
769
+ */
770
+ matchedFrom?: 'snapshot-targets' | 'selector-replay'
771
+ /**
772
+ * Why no elements are named, when they are not. `unsupported-selector` is a selector the
773
+ * browser's `querySelectorAll` rejects (a `getByRole`/`getByText` engine, `>>` chaining);
774
+ * `no-snapshot` is a trace with no readable DOM snapshot at the failure; `count-mismatch` is a
775
+ * replay that disagreed with the recorded count, so the snapshot is not evidence for this
776
+ * assertion; `browser-unavailable` is a project whose Playwright module or recorded engine
777
+ * executable could not be resolved, so the selector could not be replayed at all.
778
+ */
779
+ unreadable?: 'unsupported-selector' | 'no-snapshot' | 'count-mismatch' | 'browser-unavailable'
780
+ }
781
+ ```
782
+
708
783
  #### `TraceArtifact`
709
784
 
710
785
  What one trace says: the test it belongs to, the requests it made, and its console lines.
@@ -801,6 +876,13 @@ interface TraceArtifact {
801
876
  * leaving the run to probe computed styles (xr-molecule-flash-m197).
802
877
  */
803
878
  hiddenBox?: TraceHiddenBox
879
+ /**
880
+ * A failing assertion that compared a locator count, with the locator the trace recorded and the
881
+ * elements its failure snapshot matched ({@link TraceCountMismatch}). The row prints them, so a
882
+ * count that is off by one names the extra element instead of leaving the run to probe for it
883
+ * (xr-molecule-flash-m321's 30-minute `Expected: 3 / Received: 4`).
884
+ */
885
+ countMismatch?: TraceCountMismatch
804
886
  /**
805
887
  * The method+path groups this test's page sent {@link TRACE_NOTE_LOOP_REPEATS} times or more,
806
888
  * most-repeated first — a render loop the note leads with (xr-molecule-flash-m215: 2,106
@@ -2671,6 +2753,37 @@ function hiddenBoxNote(items: readonly TraceArtifact[]): string | null
2671
2753
 
2672
2754
  **Returns:** The sentence, or null when no failure carries a hidden-box reading.
2673
2755
 
2756
+ #### `countMismatchSentence(mismatch)`
2757
+
2758
+ The row's sentence for a failing count assertion: the locator, both numbers, and the elements
2759
+ the failure snapshot held.
2760
+
2761
+ xr-molecule-flash-m321's `comments.spec.ts:86` printed only `Expected: 3 / Received: 4`. The
2762
+ helper counted `.card:not(.comment-form) .card-block`, and the app's comment form was a
2763
+ `.comment-form` wrapper around a plain `.card`, so the form's own `.card-block` was the fourth
2764
+ match. The trace held the selector and the four elements; the note held neither.
2765
+
2766
+ ```typescript
2767
+ function countMismatchSentence(mismatch: TraceCountMismatch): string
2768
+ ```
2769
+
2770
+ - `mismatch` — The trace's count reading ({@link TraceCountMismatch}).
2771
+
2772
+ **Returns:** The sentence.
2773
+
2774
+ #### `countMismatchNote(items)`
2775
+
2776
+ The sentence a count assertion earns when every traced failure is one — the next read before
2777
+ the API or the request is changed.
2778
+
2779
+ ```typescript
2780
+ function countMismatchNote(items: readonly TraceArtifact[]): string | null
2781
+ ```
2782
+
2783
+ - `items` — The traces {@link parseTraceArtifacts} read.
2784
+
2785
+ **Returns:** The sentence, or null when not every traced failure is a count mismatch.
2786
+
2674
2787
  #### `failureTraceNote(items, target, earlierFailures, apiPrefixes, report)`
2675
2788
 
2676
2789
  The note a failed suite run's result carries: what each failing test's page actually sent, and
@@ -18,11 +18,14 @@
18
18
  * 401`. So the platform reads it (bounded, per test) and puts it in the run's result
19
19
  * ({@link failureTraceNote}), instead of leaving the executor to build its own browser to find out.
20
20
  *
21
- * Everything here is text in, text out: the sandbox runs {@link traceArtifactsScript} (Node, no
22
- * dependencies — Playwright's zip reader is not importable from a project) and prints one
23
- * `TRACE <json>` line per trace; the platform parses those lines ({@link parseTraceArtifacts}) and
24
- * writes the note ({@link failureTraceNote}). Both halves are covered by a test that runs the
25
- * script against a `trace.zip` produced by a real Playwright run, in the sandbox's own shape.
21
+ * Everything here is text in, text out: the sandbox runs {@link traceArtifactsScript} (plain Node —
22
+ * the zip is read without a dependency) and prints one `TRACE <json>` line per trace; the platform
23
+ * parses those lines ({@link parseTraceArtifacts}) and writes the note ({@link failureTraceNote}).
24
+ * The one exception is the selector replay behind a `locator.count()` reading: it resolves the
25
+ * project's own installed Playwright, replays the recorded selector against the recorded snapshot
26
+ * in that browser's DOM, and reports `browser-unavailable` (naming no elements) when it cannot.
27
+ * Both halves are covered by a test that runs the script against a `trace.zip` produced by a real
28
+ * Playwright run, in the sandbox's own shape.
26
29
  *
27
30
  * @module
28
31
  */
@@ -263,6 +266,73 @@ export interface TraceHiddenBox {
263
266
  /** True when the element has no text and no element children at all. */
264
267
  empty?: true;
265
268
  }
269
+ /**
270
+ * One element a failing locator count matched, read from the trace's own failure snapshot.
271
+ *
272
+ * A count assertion reports only `Expected: 3 / Received: 4`; the element that made it 4 is not
273
+ * in the error. Playwright's trace does hold it — either marked `__playwright_target__` in the
274
+ * failure snapshot (`expect(locator).toHaveCount(n)`), or recoverable by replaying the recorded
275
+ * selector against the snapshot's DOM in a browser (`locator.count()` then `expect(n).toBe(k)`) —
276
+ * so each match is named by what it is and where it sits, never by a guess.
277
+ */
278
+ export interface TraceCountMatch {
279
+ /** The element's tag, lowercased (`div`). */
280
+ tag: string;
281
+ /** Its `class` attribute, bounded (`card-block`). */
282
+ classes?: string;
283
+ /** Its own text, whitespace-collapsed and bounded. */
284
+ text?: string;
285
+ /** Its ancestors, outermost first (`body > div.comment-form > div.card`), bounded. */
286
+ path?: string;
287
+ /** True when a `<form>` is one of its ancestors — the m321 extra match's shape. */
288
+ inForm?: true;
289
+ }
290
+ /**
291
+ * A failing assertion that compared a LOCATOR COUNT (`expect(await locator.count()).toBe(n)` or
292
+ * `expect(locator).toHaveCount(n)`), with the locator the trace recorded and, when the failure
293
+ * snapshot can supply them, the elements it matched.
294
+ *
295
+ * xr-molecule-flash-m321's `comments.spec.ts:86` sat on `Expected: 3 / Received: 4` for ~30
296
+ * minutes and three whole suite walks: the helper counted `.card:not(.comment-form) .card-block`,
297
+ * the app's comment form was a `.comment-form` wrapper around a plain `.card`, and the form's own
298
+ * `.card-block` made the fourth. The trace had the selector (its `Frame.queryCount` entry) and the
299
+ * four elements (its failure snapshot); the note printed neither. This reading prints both, and
300
+ * fails closed — a selector outside the browser's own CSS support, a missing browser, or a replay
301
+ * that disagrees with the recorded count names no elements rather than inventing them.
302
+ */
303
+ export interface TraceCountMismatch {
304
+ /** The locator as the trace or the assertion printed it (`locator('.card:not(…')`). */
305
+ locator: string;
306
+ /** The selector inside that locator, when the trace recorded one. */
307
+ selector?: string;
308
+ /** The number the assertion expected. */
309
+ expected: number;
310
+ /** The number the page matched. */
311
+ received: number;
312
+ /**
313
+ * Where the locator came from: the assertion's own message, or the `locator.count()` entry the
314
+ * trace recorded just before the failing assertion.
315
+ */
316
+ source: 'assertion' | 'locator-count';
317
+ /** The matched elements, bounded to {@link MAX_COUNT_MATCHES} in document order. */
318
+ matched?: TraceCountMatch[];
319
+ /** How many elements matched in total, before the display bound. */
320
+ matchedTotal?: number;
321
+ /**
322
+ * How the elements were read: Playwright's own `__playwright_target__` marks in the failure
323
+ * snapshot, or the recorded selector replayed against that snapshot in a browser.
324
+ */
325
+ matchedFrom?: 'snapshot-targets' | 'selector-replay';
326
+ /**
327
+ * Why no elements are named, when they are not. `unsupported-selector` is a selector the
328
+ * browser's `querySelectorAll` rejects (a `getByRole`/`getByText` engine, `>>` chaining);
329
+ * `no-snapshot` is a trace with no readable DOM snapshot at the failure; `count-mismatch` is a
330
+ * replay that disagreed with the recorded count, so the snapshot is not evidence for this
331
+ * assertion; `browser-unavailable` is a project whose Playwright module or recorded engine
332
+ * executable could not be resolved, so the selector could not be replayed at all.
333
+ */
334
+ unreadable?: 'unsupported-selector' | 'no-snapshot' | 'count-mismatch' | 'browser-unavailable';
335
+ }
266
336
  /**
267
337
  * One method+path a test's page sent many times — the trace of a render loop.
268
338
  *
@@ -376,6 +446,13 @@ export interface TraceArtifact {
376
446
  * leaving the run to probe computed styles (xr-molecule-flash-m197).
377
447
  */
378
448
  hiddenBox?: TraceHiddenBox;
449
+ /**
450
+ * A failing assertion that compared a locator count, with the locator the trace recorded and the
451
+ * elements its failure snapshot matched ({@link TraceCountMismatch}). The row prints them, so a
452
+ * count that is off by one names the extra element instead of leaving the run to probe for it
453
+ * (xr-molecule-flash-m321's 30-minute `Expected: 3 / Received: 4`).
454
+ */
455
+ countMismatch?: TraceCountMismatch;
379
456
  /**
380
457
  * The method+path groups this test's page sent {@link TRACE_NOTE_LOOP_REPEATS} times or more,
381
458
  * most-repeated first — a render loop the note leads with (xr-molecule-flash-m215: 2,106
@@ -601,6 +678,27 @@ export declare function isCredentialExchangePath(path: string, authEndpoints?: r
601
678
  * @returns The sentence, or null when no failure carries a hidden-box reading.
602
679
  */
603
680
  export declare function hiddenBoxNote(items: readonly TraceArtifact[]): string | null;
681
+ /**
682
+ * The row's sentence for a failing count assertion: the locator, both numbers, and the elements
683
+ * the failure snapshot held.
684
+ *
685
+ * xr-molecule-flash-m321's `comments.spec.ts:86` printed only `Expected: 3 / Received: 4`. The
686
+ * helper counted `.card:not(.comment-form) .card-block`, and the app's comment form was a
687
+ * `.comment-form` wrapper around a plain `.card`, so the form's own `.card-block` was the fourth
688
+ * match. The trace held the selector and the four elements; the note held neither.
689
+ *
690
+ * @param mismatch - The trace's count reading ({@link TraceCountMismatch}).
691
+ * @returns The sentence.
692
+ */
693
+ export declare function countMismatchSentence(mismatch: TraceCountMismatch): string;
694
+ /**
695
+ * The sentence a count assertion earns when every traced failure is one — the next read before
696
+ * the API or the request is changed.
697
+ *
698
+ * @param items - The traces {@link parseTraceArtifacts} read.
699
+ * @returns The sentence, or null when not every traced failure is a count mismatch.
700
+ */
701
+ export declare function countMismatchNote(items: readonly TraceArtifact[]): string | null;
604
702
  /**
605
703
  * The note a failed suite run's result carries: what each failing test's page actually sent, and
606
704
  * what came back.
@@ -1 +1 @@
1
- {"version":3,"file":"trace-artifacts.d.ts","sourceRoot":"","sources":["../src/trace-artifacts.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"trace-artifacts.d.ts","sourceRoot":"","sources":["../src/trace-artifacts.ts"],"names":[],"mappings":"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"}
@@ -18,11 +18,14 @@
18
18
  * 401`. So the platform reads it (bounded, per test) and puts it in the run's result
19
19
  * ({@link failureTraceNote}), instead of leaving the executor to build its own browser to find out.
20
20
  *
21
- * Everything here is text in, text out: the sandbox runs {@link traceArtifactsScript} (Node, no
22
- * dependencies — Playwright's zip reader is not importable from a project) and prints one
23
- * `TRACE <json>` line per trace; the platform parses those lines ({@link parseTraceArtifacts}) and
24
- * writes the note ({@link failureTraceNote}). Both halves are covered by a test that runs the
25
- * script against a `trace.zip` produced by a real Playwright run, in the sandbox's own shape.
21
+ * Everything here is text in, text out: the sandbox runs {@link traceArtifactsScript} (plain Node —
22
+ * the zip is read without a dependency) and prints one `TRACE <json>` line per trace; the platform
23
+ * parses those lines ({@link parseTraceArtifacts}) and writes the note ({@link failureTraceNote}).
24
+ * The one exception is the selector replay behind a `locator.count()` reading: it resolves the
25
+ * project's own installed Playwright, replays the recorded selector against the recorded snapshot
26
+ * in that browser's DOM, and reports `browser-unavailable` (naming no elements) when it cannot.
27
+ * Both halves are covered by a test that runs the script against a `trace.zip` produced by a real
28
+ * Playwright run, in the sandbox's own shape.
26
29
  *
27
30
  * @module
28
31
  */
@@ -50,6 +53,12 @@ export const MAX_TRACED_BODY_CHARS = 300;
50
53
  const MAX_TRACED_WEBSOCKET_CHARS = 600;
51
54
  /** How many tests the note in a run's result names. */
52
55
  export const MAX_TRACE_NOTE_TESTS = 5;
56
+ /** How many elements a count mismatch's row names, in document order. */
57
+ const MAX_COUNT_MATCHES = 6;
58
+ /** How much of one matched element's text the trace read keeps. */
59
+ const MAX_COUNT_TEXT_CHARS = 90;
60
+ /** How deep one matched element's ancestor chain is named (it and up to three ancestors). */
61
+ const MAX_COUNT_PATH_DEPTH = 4;
53
62
  /** How many requests the note names per test. */
54
63
  export const MAX_TRACE_NOTE_REQUESTS = 4;
55
64
  /**
@@ -123,6 +132,16 @@ const MAX_ACTIONS = ${MAX_TRACED_ACTIONS}
123
132
  const MAX_BODY = ${MAX_TRACED_BODY_CHARS}
124
133
  const MAX_WS = ${MAX_TRACED_WEBSOCKET_CHARS}
125
134
  const LOOP_REPEATS = ${TRACE_NOTE_LOOP_REPEATS}
135
+ const MAX_COUNT_MATCHES = ${MAX_COUNT_MATCHES}
136
+ const MAX_COUNT_TEXT = ${MAX_COUNT_TEXT_CHARS}
137
+ const MAX_COUNT_PATH_DEPTH = ${MAX_COUNT_PATH_DEPTH}
138
+ // The serialized failure snapshot is capped: a page whose DOM exceeds this is not replayed, and the
139
+ // reading reports no readable snapshot instead of a partial page.
140
+ const MAX_SNAPSHOT_HTML = 4000000
141
+ // A locator.count() older than this before the failing assertion is not paired with it: the value
142
+ // match in countMismatchOf() is the strong evidence, this only bounds a trace whose count recorded
143
+ // no result.
144
+ const COUNT_LOOKBACK_MS = 180000
126
145
  const MAX_URL = 200
127
146
  // A console line that is not an error but still has to reach the note (m148): one a molecule
128
147
  // package wrote — it names the package, the \`[molecule/…]\` prefix, or the \`[scope]\` prefix
@@ -408,6 +427,438 @@ function hiddenBoxOf(message, frames) {
408
427
  return reading
409
428
  }
410
429
 
430
+ // --- count assertions (xr-molecule-flash-m321) -------------------------------
431
+ // A count assertion reports only numbers ("Expected: 3 / Received: 4"). The trace holds the rest:
432
+ // \`locator.count()\` is a Frame.queryCount entry carrying params.selector and the result value,
433
+ // \`expect(locator).toHaveCount(n)\` is a Frame.expect entry carrying the selector and the expected
434
+ // number, and the failure snapshot is the page's DOM at that moment. Two things name the matched
435
+ // elements: the \`__playwright_target__\` marks Playwright leaves on an assertion's own matches, and
436
+ // a replay of the recorded selector in the browser's own DOM against the snapshot (resolved from
437
+ // the project's Playwright; no hand-written CSS grammar). Everything else fails closed — a selector
438
+ // the browser rejects, a missing browser, an unreadable snapshot, or a replay whose count disagrees
439
+ // with the recorded one names no elements rather than inventing them.
440
+
441
+ function stripAnsi(text) {
442
+ return String(text || '').replace(/\\u001b\\[[0-9;]*m/g, '')
443
+ }
444
+
445
+ // The two numbers a count assertion prints, when the failure is one. A string assertion's
446
+ // \`Expected: "…"\` and a comparison's \`Expected: > 3\` both parse to null.
447
+ function countNumbersOf(message) {
448
+ const text = stripAnsi(message)
449
+ const expected = /Expected:\\s*(\\d+)\\s*(?:\\n|$)/.exec(text)
450
+ const received = /Received:\\s*(\\d+)\\s*(?:\\n|$)/.exec(text)
451
+ if (expected === null || received === null) return null
452
+ return { expected: Number(expected[1]), received: Number(received[1]) }
453
+ }
454
+
455
+ // The locator a count assertion printed (\`Locator: locator('.card…')\`), and its inner selector.
456
+ function locatorInError(message) {
457
+ const text = stripAnsi(message)
458
+ const named = /Locator:\\s*(locator\\([^\\n]*?\\))\\s*(?:\\n|$)/.exec(text)
459
+ if (named === null) return null
460
+ const inner = /^locator\\(\\s*(['"\`])([\\s\\S]*)\\1\\s*\\)$/.exec(named[1])
461
+ if (inner === null) return { locator: named[1], selector: null }
462
+ return { locator: named[1], selector: inner[2] }
463
+ }
464
+
465
+ function cssClassesOf(node) {
466
+ return String(node.attrs.class || '').split(/\\s+/).filter(Boolean)
467
+ }
468
+
469
+ // The post-order node list Playwright's own \`snapshotNodes\` builds: text nodes in place, each
470
+ // element pushed after its children. A subtree reference is not a node and is not followed here.
471
+ function snapshotNodesOf(value) {
472
+ const nodes = []
473
+ const visit = function (n) {
474
+ if (typeof n === 'string') { nodes.push(n); return }
475
+ if (!Array.isArray(n) || typeof n[0] !== 'string') return
476
+ for (let i = 2; i < n.length; i++) visit(n[i])
477
+ nodes.push(n)
478
+ }
479
+ visit(value)
480
+ return nodes
481
+ }
482
+
483
+ // The frame's own snapshots, in recording order — the list Playwright's subtree references index
484
+ // into. Main frames only (the reader drops subframe snapshots), matched by frameId when present.
485
+ function frameSnapshotsOf(frame, allFrames) {
486
+ if (typeof frame.frameId === 'string' && frame.frameId !== '') {
487
+ const same = allFrames.filter(function (f) { return f.frameId === frame.frameId })
488
+ if (same.length > 0) return same
489
+ }
490
+ return allFrames.filter(function (f) { return f.context === frame.context && f.pageId === frame.pageId })
491
+ }
492
+
493
+ // One snapshot's DOM as { tag, attrs, children, parent } nodes, with subtree references expanded
494
+ // against the frame's earlier snapshots (reference \`[delta, nodeIndex]\` = the snapshot \`at - delta\`,
495
+ // exactly as the trace viewer resolves it). Null when any reference cannot be resolved or the tree
496
+ // is too large: an incomplete snapshot is not evidence about the page.
497
+ function resolvedSnapshotTree(frame, allFrames) {
498
+ if (frame === undefined || frame === null) return null
499
+ const tree = frame.snapshot
500
+ if (tree === undefined || tree === null) return null
501
+ const shown = frameSnapshotsOf(frame, allFrames)
502
+ const index = shown.indexOf(frame)
503
+ if (index < 0) return null
504
+ let budget = 60000
505
+ let readable = true
506
+ const build = function (value, at, depth) {
507
+ if (!readable) return null
508
+ if (budget-- < 0 || depth > 80) { readable = false; return null }
509
+ if (typeof value === 'string') return { tag: null, text: value, attrs: {}, children: [], parent: null }
510
+ if (!Array.isArray(value)) { readable = false; return null }
511
+ if (Array.isArray(value[0])) {
512
+ const delta = typeof value[0][0] === 'number' ? value[0][0] : -1
513
+ const nodeIndex = typeof value[0][1] === 'number' ? value[0][1] : -1
514
+ const refAt = at - delta
515
+ if (delta < 0 || refAt < 0 || refAt >= shown.length || nodeIndex < 0) { readable = false; return null }
516
+ const earlier = shown[refAt].snapshot
517
+ const nodes = earlier === undefined || earlier === null ? [] : snapshotNodesOf(earlier)
518
+ if (nodeIndex >= nodes.length) { readable = false; return null }
519
+ return build(nodes[nodeIndex], refAt, depth + 1)
520
+ }
521
+ if (typeof value[0] !== 'string') { readable = false; return null }
522
+ const node = {
523
+ tag: String(value[0]).toLowerCase(),
524
+ attrs: value[1] && typeof value[1] === 'object' ? value[1] : {},
525
+ children: [],
526
+ parent: null,
527
+ }
528
+ for (let i = 2; i < value.length; i++) {
529
+ const child = build(value[i], at, depth + 1)
530
+ if (child === null) { readable = false; return null }
531
+ child.parent = node
532
+ node.children.push(child)
533
+ }
534
+ return node
535
+ }
536
+ const root = build(tree, index, 0)
537
+ return readable ? root : null
538
+ }
539
+
540
+ // An element's own text, whitespace-collapsed and bounded. Script and style bodies are skipped, as
541
+ // the trace viewer skips them: code is not what the page showed.
542
+ function elementTextOf(node) {
543
+ let out = ''
544
+ const visit = function (n) {
545
+ if (out.length > MAX_COUNT_TEXT) return
546
+ if (typeof n.text === 'string') { out += n.text; return }
547
+ if (n.tag === 'script' || n.tag === 'style') return
548
+ for (const child of n.children) visit(child)
549
+ }
550
+ visit(node)
551
+ return out.replace(/\\s+/g, ' ').trim().slice(0, MAX_COUNT_TEXT)
552
+ }
553
+
554
+ // The element's ancestors, outermost first, for the row's "where it sits" (\`body >
555
+ // section.comments > div.card\`).
556
+ function elementPathOf(node) {
557
+ const parts = []
558
+ let at = node.parent
559
+ while (at !== null && at.tag !== null && parts.length < MAX_COUNT_PATH_DEPTH) {
560
+ const classes = cssClassesOf(at)
561
+ parts.unshift(at.tag + (classes.length > 0 ? '.' + classes[0] : ''))
562
+ at = at.parent
563
+ }
564
+ return parts.join(' > ').slice(0, 160)
565
+ }
566
+
567
+ function elementInForm(node) {
568
+ let at = node.parent
569
+ while (at !== null) {
570
+ if (at.tag === 'form') return true
571
+ at = at.parent
572
+ }
573
+ return false
574
+ }
575
+
576
+ function countMatchOf(node) {
577
+ const match = { tag: node.tag }
578
+ const classes = String(node.attrs.class || '').trim()
579
+ if (classes) match.classes = classes.slice(0, 120)
580
+ const text = elementTextOf(node)
581
+ if (text) match.text = text
582
+ const path = elementPathOf(node)
583
+ if (path) match.path = path
584
+ if (elementInForm(node)) match.inForm = true
585
+ return match
586
+ }
587
+
588
+ // Every element carrying Playwright's own assertion-target mark, in document order.
589
+ function targetElementsOf(root) {
590
+ const out = []
591
+ const visit = function (node) {
592
+ if (node.tag === null) return
593
+ if (Object.prototype.hasOwnProperty.call(node.attrs, '__playwright_target__')) out.push(node)
594
+ for (const child of node.children) visit(child)
595
+ }
596
+ visit(root)
597
+ return out
598
+ }
599
+
600
+ // The failure snapshot as HTML, so a real browser can parse it and answer the selector with the
601
+ // standard DOMParser/querySelectorAll instead of a hand-written grammar. Each element carries its
602
+ // node index, so a match maps back to the RECORDED element (text, classes, ancestors) rather than a
603
+ // re-derivation. Bounded, and every unsupported shape fails the serialization instead of producing
604
+ // a partial page: an incomplete DOM is not evidence about the page.
605
+ const SNAPSHOT_NODE_ATTR = 'data-mol-snapshot-node'
606
+ const SNAPSHOT_TAG = /^[a-z][a-z0-9-]*$/i
607
+ const SNAPSHOT_ATTR = /^[-_A-Za-z][-_A-Za-z0-9:.]*$/
608
+ const SNAPSHOT_VOID = {
609
+ area: 1, base: 1, br: 1, col: 1, embed: 1, hr: 1, img: 1, input: 1, link: 1,
610
+ meta: 1, param: 1, source: 1, track: 1, wbr: 1
611
+ }
612
+ function snapshotHtmlOf(root) {
613
+ const nodes = []
614
+ let html = ''
615
+ let failed = false
616
+ function append(text) {
617
+ if (failed) return
618
+ if (html.length + text.length > MAX_SNAPSHOT_HTML) { failed = true; return }
619
+ html += text
620
+ }
621
+ function escapeText(text) {
622
+ return String(text).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
623
+ }
624
+ function escapeAttr(text) {
625
+ return String(text).replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;')
626
+ }
627
+ function visit(node) {
628
+ if (failed) return
629
+ if (node.tag === null) { append(escapeText(node.text === undefined ? '' : node.text)); return }
630
+ if (!SNAPSHOT_TAG.test(node.tag)) { failed = true; return }
631
+ const index = nodes.length
632
+ nodes.push(node)
633
+ append('<' + node.tag)
634
+ for (const name of Object.keys(node.attrs)) {
635
+ if (!SNAPSHOT_ATTR.test(name) || name.toLowerCase() === SNAPSHOT_NODE_ATTR) continue
636
+ const value = node.attrs[name]
637
+ if (typeof value !== 'string') continue
638
+ // A meta refresh is a navigation, not rendered content. The replay only needs the DOM the
639
+ // failure snapshot shows, so it is dropped and the isolated context stays inert.
640
+ if (node.tag === 'meta' && name.toLowerCase() === 'http-equiv' && /refresh/i.test(value)) continue
641
+ append(' ' + name + '="' + escapeAttr(value) + '"')
642
+ }
643
+ append(' ' + SNAPSHOT_NODE_ATTR + '="' + index + '">')
644
+ if (SNAPSHOT_VOID[node.tag]) {
645
+ if (node.children.length > 0) { failed = true; return }
646
+ return
647
+ }
648
+ for (const child of node.children) visit(child)
649
+ append('</' + node.tag + '>')
650
+ }
651
+ visit(root)
652
+ if (failed || html === '') return null
653
+ // A snapshot rooted at <html> IS the document; anything else is body content.
654
+ const documentHtml =
655
+ root.tag === 'html' ? '<!doctype html>' + html
656
+ : '<!doctype html><html><head></head><body>' + html + '</body></html>'
657
+ return { nodes: nodes, documentHtml: documentHtml }
658
+ }
659
+
660
+ // Playwright, resolved from the project whose traces these are (the sandbox installs it for the e2e
661
+ // run the trace came from). The trace reader itself stays dependency-free: when the module or the
662
+ // recorded browser's executable cannot be resolved, the replay says so and names no elements.
663
+ let playwrightModulePromise = null
664
+ function playwrightModule() {
665
+ if (playwrightModulePromise === null) {
666
+ playwrightModulePromise = Promise.resolve().then(function () {
667
+ const paths = []
668
+ if (ROOT) paths.push(ROOT)
669
+ paths.push(process.cwd())
670
+ for (const name of ['playwright', '@playwright/test']) {
671
+ try {
672
+ return require(require.resolve(name, { paths: paths }))
673
+ } catch (_error) {
674
+ // Not installed under this name at the project root: try the next module.
675
+ }
676
+ }
677
+ return null
678
+ })
679
+ }
680
+ return playwrightModulePromise
681
+ }
682
+
683
+ // One browser per recorded engine, launched on the first selector replay and reused. A resolution
684
+ // or launch failure is cached as null, so a project without the browser does not pay for it once per
685
+ // trace.
686
+ const replayBrowsers = {}
687
+ function replayBrowserFor(name) {
688
+ const engineName = name || 'chromium'
689
+ if (replayBrowsers[engineName] === undefined) {
690
+ replayBrowsers[engineName] = playwrightModule().then(function (pw) {
691
+ if (pw === null) return null
692
+ const engine = pw[engineName]
693
+ if (!engine || typeof engine.launch !== 'function') return null
694
+ try {
695
+ return engine.launch({ headless: true, timeout: 10000 }).catch(function (_error) {
696
+ // No executable for this engine: the replay reports unavailable, never a guess.
697
+ return null
698
+ })
699
+ } catch (_error) {
700
+ return null
701
+ }
702
+ })
703
+ }
704
+ return replayBrowsers[engineName]
705
+ }
706
+ function closeReplayBrowsers() {
707
+ return Promise.all(Object.keys(replayBrowsers).map(function (name) {
708
+ return replayBrowsers[name].then(function (browser) {
709
+ if (browser === null) return null
710
+ return browser.close().catch(function (_error) { return null })
711
+ })
712
+ }))
713
+ }
714
+
715
+ // Replay one selector against the recorded snapshot with the browser's own DOM. The context is
716
+ // isolated and has JavaScript disabled (nothing in the snapshot can execute), is offline, and
717
+ // aborts every request (nothing it references can be fetched). null = no browser available;
718
+ // { unsupported: true } = the browser rejected the selector (a Playwright engine, chaining).
719
+ async function replaySelector(selector, snapshot, browserName) {
720
+ const browser = await replayBrowserFor(browserName)
721
+ if (browser === null) return null
722
+ let context = null
723
+ try {
724
+ context = await browser.newContext({
725
+ javaScriptEnabled: false,
726
+ offline: true,
727
+ serviceWorkers: 'block',
728
+ })
729
+ await context.route('**/*', function (route) { return route.abort() })
730
+ const page = await context.newPage()
731
+ await page.setContent(snapshot.documentHtml, { waitUntil: 'domcontentloaded', timeout: 10000 })
732
+ const found = await page.evaluate(function (args) {
733
+ try {
734
+ const elements = document.querySelectorAll(args.selector)
735
+ const indices = []
736
+ for (let i = 0; i < elements.length; i++) {
737
+ indices.push(elements[i].getAttribute(args.marker))
738
+ }
739
+ return { ok: true, indices: indices }
740
+ } catch (_error) {
741
+ return { ok: false }
742
+ }
743
+ }, { selector: selector, marker: SNAPSHOT_NODE_ATTR })
744
+ if (found === null || found === undefined || found.ok !== true || !Array.isArray(found.indices))
745
+ return { unsupported: true }
746
+ return { indices: found.indices }
747
+ } catch (_error) {
748
+ // The context could not hold the snapshot (launch failure, a malformed serialization): the
749
+ // replay is unavailable for this assertion, and no element is named.
750
+ return null
751
+ } finally {
752
+ if (context !== null) await context.close().catch(function (_error) { return null })
753
+ }
754
+ }
755
+
756
+ // The count assertion this failure is, with the locator the trace recorded and the elements its
757
+ // failure snapshot matched. Null when the failure is not a count, when no locator can be attributed
758
+ // to it, or when both numbers cannot be read.
759
+ async function countMismatchOf(failedError, frames, allFrames, queryCounts, frameExpects, failedStep, browserName) {
760
+ const numbers = countNumbersOf(failedError)
761
+ if (numbers === null) return null
762
+ const assertionStart =
763
+ failedStep && typeof failedStep.startTime === 'number' ? failedStep.startTime : null
764
+ const recent = function (entry) {
765
+ if (assertionStart === null) return true
766
+ return entry.start <= assertionStart + 1000 && assertionStart - entry.start <= COUNT_LOOKBACK_MS
767
+ }
768
+ const named = locatorInError(failedError)
769
+ let locator = null
770
+ let selector = null
771
+ let source = null
772
+ if (named !== null && named.selector !== null) {
773
+ locator = named.locator
774
+ selector = named.selector
775
+ source = 'assertion'
776
+ }
777
+ // expect(locator).toHaveCount(n): the trace records the selector and the expected number.
778
+ if (locator === null) {
779
+ const wanted = frameExpects
780
+ .filter(function (entry) { return entry.expected === numbers.expected && recent(entry) })
781
+ .sort(function (a, b) { return b.start - a.start })[0]
782
+ if (wanted !== undefined) {
783
+ locator = wanted.locator
784
+ selector = wanted.selector
785
+ source = 'assertion'
786
+ }
787
+ }
788
+ // locator.count() then expect(count).toBe(n): the value the trace recorded IS the received one.
789
+ if (locator === null) {
790
+ const wanted = queryCounts
791
+ .filter(function (entry) { return entry.value === numbers.received && recent(entry) })
792
+ .sort(function (a, b) { return b.start - a.start })[0]
793
+ if (wanted !== undefined) {
794
+ locator = wanted.locator
795
+ selector = wanted.selector
796
+ source = 'locator-count'
797
+ }
798
+ }
799
+ if (locator === null || source === null) return null
800
+ const reading = {
801
+ locator: locator.slice(0, 200),
802
+ ...(selector === null ? {} : { selector: selector.slice(0, 200) }),
803
+ expected: numbers.expected,
804
+ received: numbers.received,
805
+ source: source,
806
+ }
807
+ const tree = resolvedSnapshotTree(frames.length > 0 ? frames[frames.length - 1] : null, allFrames)
808
+ if (tree === null) {
809
+ reading.unreadable = 'no-snapshot'
810
+ return reading
811
+ }
812
+ // Playwright marks an assertion's own matches in the snapshot recorded with it. Only a mark set
813
+ // whose count IS the received count belongs to this assertion.
814
+ const targets = targetElementsOf(tree)
815
+ if (targets.length === numbers.received) {
816
+ reading.matched = targets.slice(0, MAX_COUNT_MATCHES).map(countMatchOf)
817
+ reading.matchedTotal = targets.length
818
+ reading.matchedFrom = 'snapshot-targets'
819
+ return reading
820
+ }
821
+ if (selector === null) {
822
+ reading.unreadable = 'unsupported-selector'
823
+ return reading
824
+ }
825
+ // The recorded selector is replayed against the recorded snapshot by the browser's own DOM: no
826
+ // hand-written CSS grammar, and every standard selector the engine the test ran in supports.
827
+ const snapshot = snapshotHtmlOf(tree)
828
+ if (snapshot === null) {
829
+ reading.unreadable = 'no-snapshot'
830
+ return reading
831
+ }
832
+ const replay = await replaySelector(selector, snapshot, browserName)
833
+ if (replay === null) {
834
+ reading.unreadable = 'browser-unavailable'
835
+ return reading
836
+ }
837
+ if (replay.unsupported) {
838
+ reading.unreadable = 'unsupported-selector'
839
+ return reading
840
+ }
841
+ const elements = []
842
+ let broken = false
843
+ for (const index of replay.indices) {
844
+ const at = typeof index === 'string' ? Number(index) : NaN
845
+ const node =
846
+ Number.isInteger(at) && at >= 0 && at < snapshot.nodes.length ? snapshot.nodes[at] : null
847
+ if (node === null || node.tag === null) { broken = true; break }
848
+ elements.push(node)
849
+ }
850
+ // The replay is evidence only when it agrees, element for element, with the count the run
851
+ // recorded; a stale or partial snapshot names nothing.
852
+ if (broken || elements.length !== numbers.received) {
853
+ reading.unreadable = 'count-mismatch'
854
+ return reading
855
+ }
856
+ reading.matched = elements.slice(0, MAX_COUNT_MATCHES).map(countMatchOf)
857
+ reading.matchedTotal = elements.length
858
+ reading.matchedFrom = 'selector-replay'
859
+ return reading
860
+ }
861
+
411
862
  // A request/response body exactly as Playwright stored it: inline on the value, or in a resource
412
863
  // entry the value names. Bounded, and binary is dropped — a JSON body is what this is for, and a
413
864
  // multipart upload would print bytes.
@@ -546,6 +997,8 @@ function networkContextOf(name) {
546
997
  return m === null ? null : m[1]
547
998
  }
548
999
 
1000
+ // The reading is async only for the browser selector replay; every other fact is plain Node.
1001
+ async function main() {
549
1002
  for (const one of found.slice(0, MAX_TRACES)) {
550
1003
  let buf
551
1004
  try { buf = fs.readFileSync(one.file) } catch (e) { continue }
@@ -580,6 +1033,19 @@ for (const one of found.slice(0, MAX_TRACES)) {
580
1033
  ctx.network.push.apply(ctx.network, ndjson(zipData(buf, entry)))
581
1034
  }
582
1035
  }
1036
+ // The browser the test ran in, from the trace's own context-options: a selector replay uses the
1037
+ // same engine's DOM rather than assuming one. A trace that records none defaults to chromium.
1038
+ let browserName = ''
1039
+ for (const ctx of contexts) {
1040
+ for (const entry of ctx.trace) {
1041
+ if (entry && entry.type === 'context-options' && typeof entry.browserName === 'string' &&
1042
+ entry.browserName) {
1043
+ browserName = entry.browserName
1044
+ break
1045
+ }
1046
+ }
1047
+ if (browserName) break
1048
+ }
583
1049
  const requestsAll = []
584
1050
  for (const ctx of contexts) {
585
1051
  for (const line of ctx.network) {
@@ -644,11 +1110,30 @@ for (const one of found.slice(0, MAX_TRACES)) {
644
1110
  // assertion's own page can be selected below (m160).
645
1111
  const actionsAll = []
646
1112
  const framesAll = []
1113
+ // The count entries a failing assertion can be paired with (m321): \`locator.count()\` and
1114
+ // \`expect(locator).toHaveCount(n)\` are the two ways a test compares a selector's match count.
1115
+ const queryCounts = []
1116
+ const frameExpects = []
1117
+ // The readable locator a runner step names (\`getByRole('button', { name: 'Post Comment' })\`),
1118
+ // keyed by the step id the library trace repeats on the entry it recorded for that step.
1119
+ const locatorByStep = {}
1120
+ for (const entry of runner) {
1121
+ if (!entry || entry.type !== 'before') continue
1122
+ const params = entry.params && typeof entry.params === 'object' ? entry.params : {}
1123
+ if (typeof entry.stepId === 'string' && typeof params.locator === 'string' && params.locator)
1124
+ locatorByStep[entry.stepId] = params.locator
1125
+ }
647
1126
  for (const ctx of contexts) {
648
1127
  const ended = {}
1128
+ const callValues = {}
649
1129
  for (const entry of ctx.trace) {
650
1130
  if (entry && entry.type === 'after' && typeof entry.callId === 'string' && typeof entry.endTime === 'number')
651
1131
  ended[entry.callId] = entry.endTime
1132
+ // A count's own answer, on the trace's clock: the \`locator.count()\` entry records
1133
+ // \`result.value\`, and that number is the "Received:" the failing assertion printed.
1134
+ if (entry && entry.type === 'after' && typeof entry.callId === 'string' &&
1135
+ entry.result && typeof entry.result.value === 'number')
1136
+ callValues[entry.callId] = entry.result.value
652
1137
  }
653
1138
  const pageByCall = {}
654
1139
  for (const entry of ctx.trace) {
@@ -697,6 +1182,39 @@ for (const one of found.slice(0, MAX_TRACES)) {
697
1182
  }
698
1183
  }
699
1184
  ctx.actions.push(action)
1185
+ // The entries a count assertion can be attributed to (m321). A \`getByRole\`-style locator is
1186
+ // readable only through the runner step's own \`params.locator\`; the library entry's
1187
+ // \`params.selector\` is the internal engine string, kept for the CSS replay.
1188
+ const locator =
1189
+ (typeof entry.stepId === 'string' ? locatorByStep[entry.stepId] : undefined) ||
1190
+ 'locator(' + JSON.stringify(params.selector) + ')'
1191
+ const end = typeof ended[entry.callId] === 'number' ? ended[entry.callId] : null
1192
+ if (api === 'Frame.queryCount' && typeof params.selector === 'string') {
1193
+ queryCounts.push({
1194
+ start: entry.startTime,
1195
+ end: end,
1196
+ selector: params.selector,
1197
+ value: typeof callValues[entry.callId] === 'number' ? callValues[entry.callId] : undefined,
1198
+ locator: locator,
1199
+ pageId: action.pageId,
1200
+ stepId: action.stepId,
1201
+ })
1202
+ }
1203
+ if (
1204
+ api === 'Frame.expect' &&
1205
+ /count/i.test(String(params.expression || '')) &&
1206
+ typeof params.selector === 'string'
1207
+ ) {
1208
+ frameExpects.push({
1209
+ start: entry.startTime,
1210
+ end: end,
1211
+ selector: params.selector,
1212
+ expected: typeof params.expectedNumber === 'number' ? params.expectedNumber : undefined,
1213
+ locator: locator,
1214
+ pageId: action.pageId,
1215
+ stepId: action.stepId,
1216
+ })
1217
+ }
700
1218
  }
701
1219
  // Monotonic time, not archive order: two contexts interleave in the zip, and one context's
702
1220
  // own actions still have to be read in the order the page performed them.
@@ -1026,27 +1544,17 @@ for (const one of found.slice(0, MAX_TRACES)) {
1026
1544
  }
1027
1545
  // The text a frame snapshot SHOWED, so a page that still carried the awaited value is never
1028
1546
  // described as having dropped it. Playwright serializes the DOM as strings (text nodes) and
1029
- // [tag, attrs, ...children] elements, with a subtree reference ([[snapshotIndex, nodeIndex]]) to
1030
- // a node an earlier snapshot of the same frame carried. \`script\`/\`style\` bodies are skipped,
1031
- // as the trace viewer skips them: code is not what the page showed. Null when the snapshot is
1032
- // absent or a reference cannot be resolved — an unreadable snapshot proves nothing.
1547
+ // [tag, attrs, ...children] elements, with a subtree reference ([delta, nodeIndex]) to a node an
1548
+ // EARLIER snapshot of the same frame carried — the reference is relative to this snapshot's own
1549
+ // index in the frame's list, which is how the trace viewer resolves it (\`snapshotIndex - delta\`).
1550
+ // \`script\`/\`style\` bodies are skipped, as the trace viewer skips them: code is not what the page
1551
+ // showed. Null when the snapshot is absent or a reference cannot be resolved — an unreadable
1552
+ // snapshot proves nothing.
1033
1553
  function snapshotShowed(frame) {
1034
1554
  const tree = frame.snapshot
1035
1555
  if (tree === undefined || tree === null) return null
1036
- const shown = framesAll.filter(function (f) {
1037
- return f.context === frame.context && f.pageId === frame.pageId
1038
- })
1039
- const nodesOf = function (value) {
1040
- const nodes = []
1041
- const collect = function (n) {
1042
- if (typeof n === 'string') { nodes.push(n); return }
1043
- if (!Array.isArray(n) || typeof n[0] !== 'string') return
1044
- for (let i = 2; i < n.length; i++) collect(n[i])
1045
- nodes.push(n)
1046
- }
1047
- collect(value)
1048
- return nodes
1049
- }
1556
+ const shown = frameSnapshotsOf(frame, framesAll)
1557
+ const index = shown.indexOf(frame)
1050
1558
  const out = []
1051
1559
  let visited = 0
1052
1560
  let readable = true
@@ -1058,8 +1566,9 @@ for (const one of found.slice(0, MAX_TRACES)) {
1058
1566
  const ref = n[0]
1059
1567
  const at = Array.isArray(ref) && typeof ref[0] === 'number' ? ref[0] : -1
1060
1568
  const node = Array.isArray(ref) && typeof ref[1] === 'number' ? ref[1] : -1
1061
- const earlier = at >= 0 && at < shown.length && shown[at].snapshot != null ? shown[at].snapshot : null
1062
- const nodes = earlier === null ? [] : nodesOf(earlier)
1569
+ const refAt = index - at
1570
+ const earlier = at >= 0 && refAt >= 0 && refAt < shown.length && shown[refAt].snapshot != null ? shown[refAt].snapshot : null
1571
+ const nodes = earlier === null ? [] : snapshotNodesOf(earlier)
1063
1572
  if (node < 0 || node >= nodes.length) { readable = false; return }
1064
1573
  visit(nodes[node])
1065
1574
  return
@@ -1132,6 +1641,11 @@ for (const one of found.slice(0, MAX_TRACES)) {
1132
1641
  // what the element is made of, so the note names the cause instead of sending the run at the
1133
1642
  // selector (m197 — an <img> with no CSS size collapsed inside its link).
1134
1643
  const hiddenBox = hiddenBoxOf(failedError, frames)
1644
+ // A count assertion whose locator and matched elements the trace itself holds (m321): the
1645
+ // locator from \`locator.count()\`/\`toHaveCount\`, the elements from the failure snapshot.
1646
+ const countMismatch = await countMismatchOf(
1647
+ failedError, frames, framesAll, queryCounts, frameExpects, failedStep, browserName
1648
+ )
1135
1649
  function wire(r) {
1136
1650
  return {
1137
1651
  method: r.method,
@@ -1356,7 +1870,8 @@ for (const one of found.slice(0, MAX_TRACES)) {
1356
1870
  chosen.length === 0 &&
1357
1871
  keptConsole.length === 0 &&
1358
1872
  actions.length === 0 &&
1359
- hiddenBox === null
1873
+ hiddenBox === null &&
1874
+ countMismatch === null
1360
1875
  )
1361
1876
  continue
1362
1877
  // Counted over the page's EVERY network entry, before the cap and dedupe above (m215).
@@ -1375,12 +1890,19 @@ for (const one of found.slice(0, MAX_TRACES)) {
1375
1890
  ...(missingText === null ? {} : { missingText: missingText }),
1376
1891
  ...(readContradiction === null ? {} : { readContradiction: readContradiction }),
1377
1892
  ...(hiddenBox === null ? {} : { hiddenBox: hiddenBox }),
1893
+ ...(countMismatch === null ? {} : { countMismatch: countMismatch }),
1378
1894
  console: keptConsole.slice(0, MAX_CONSOLE).map(function (text) { return text.slice(0, 300) }),
1379
1895
  actions: actions.slice(-MAX_ACTIONS),
1380
1896
  ...(unattributed ? { unattributed: true } : {}),
1381
1897
  ...(otherContexts.length > 0 ? { otherContexts: otherContexts } : {}),
1382
1898
  }) + '\\n')
1383
1899
  }
1900
+ }
1901
+
1902
+ main().catch(function (error) {
1903
+ process.stderr.write('trace read failed: ' +
1904
+ String(error && error.message ? error.message : error) + '\\n')
1905
+ }).then(closeReplayBrowsers)
1384
1906
  `;
1385
1907
  /**
1386
1908
  * One request out of a `TRACE` line's json, or null when the value is not an object.
@@ -1496,6 +2018,62 @@ function parseHiddenBox(value) {
1496
2018
  ...(raw.empty === true ? { empty: true } : {}),
1497
2019
  };
1498
2020
  }
2021
+ /**
2022
+ * One `countMismatch` out of a `TRACE` line's json, or null when the value is not one.
2023
+ *
2024
+ * The locator and both numbers are required; the matched elements are kept only when the script
2025
+ * wrote them, and each needs a tag. A reading whose `unreadable` reason is unknown is dropped —
2026
+ * the note may not present a reason it cannot explain.
2027
+ *
2028
+ * @param value - The line's `countMismatch`.
2029
+ * @returns The count assertion the trace read, or null.
2030
+ */
2031
+ function parseCountMismatch(value) {
2032
+ if (typeof value !== 'object' || value === null)
2033
+ return null;
2034
+ const raw = value;
2035
+ if (typeof raw.locator !== 'string' || raw.locator === '')
2036
+ return null;
2037
+ if (typeof raw.expected !== 'number' || !Number.isFinite(raw.expected))
2038
+ return null;
2039
+ if (typeof raw.received !== 'number' || !Number.isFinite(raw.received))
2040
+ return null;
2041
+ if (raw.source !== 'assertion' && raw.source !== 'locator-count')
2042
+ return null;
2043
+ const matched = Array.isArray(raw.matched)
2044
+ ? raw.matched
2045
+ .filter((entry) => typeof entry === 'object' && entry !== null)
2046
+ .filter((entry) => typeof entry.tag === 'string' && entry.tag !== '')
2047
+ .map((entry) => ({
2048
+ tag: String(entry.tag),
2049
+ ...(typeof entry.classes === 'string' && entry.classes ? { classes: entry.classes } : {}),
2050
+ ...(typeof entry.text === 'string' && entry.text ? { text: entry.text } : {}),
2051
+ ...(typeof entry.path === 'string' && entry.path ? { path: entry.path } : {}),
2052
+ ...(entry.inForm === true ? { inForm: true } : {}),
2053
+ }))
2054
+ : [];
2055
+ const unreadable = raw.unreadable === 'unsupported-selector' ||
2056
+ raw.unreadable === 'no-snapshot' ||
2057
+ raw.unreadable === 'count-mismatch' ||
2058
+ raw.unreadable === 'browser-unavailable'
2059
+ ? raw.unreadable
2060
+ : null;
2061
+ return {
2062
+ locator: raw.locator,
2063
+ ...(typeof raw.selector === 'string' && raw.selector ? { selector: raw.selector } : {}),
2064
+ expected: raw.expected,
2065
+ received: raw.received,
2066
+ source: raw.source,
2067
+ ...(matched.length === 0 ? {} : { matched }),
2068
+ ...(typeof raw.matchedTotal === 'number' && Number.isFinite(raw.matchedTotal)
2069
+ ? { matchedTotal: raw.matchedTotal }
2070
+ : {}),
2071
+ ...(raw.matchedFrom === 'snapshot-targets' || raw.matchedFrom === 'selector-replay'
2072
+ ? { matchedFrom: raw.matchedFrom }
2073
+ : {}),
2074
+ ...(unreadable === null ? {} : { unreadable }),
2075
+ };
2076
+ }
1499
2077
  /**
1500
2078
  * One `lastAction` out of a `TRACE` line's json, or null when the value is not one.
1501
2079
  *
@@ -1576,6 +2154,7 @@ export function parseTraceArtifacts(stdout) {
1576
2154
  const missingText = parseMissingText(raw.missingText);
1577
2155
  const readContradiction = parseReadContradiction(raw.readContradiction);
1578
2156
  const hiddenBox = parseHiddenBox(raw.hiddenBox);
2157
+ const countMismatch = parseCountMismatch(raw.countMismatch);
1579
2158
  const repeated = parseRepeated(raw.repeated);
1580
2159
  const otherContexts = Array.isArray(raw.otherContexts)
1581
2160
  ? raw.otherContexts
@@ -1615,6 +2194,7 @@ export function parseTraceArtifacts(stdout) {
1615
2194
  ...(missingText === null ? {} : { missingText }),
1616
2195
  ...(readContradiction === null ? {} : { readContradiction }),
1617
2196
  ...(hiddenBox === null ? {} : { hiddenBox }),
2197
+ ...(countMismatch === null ? {} : { countMismatch }),
1618
2198
  ...(repeated.length === 0 ? {} : { repeated }),
1619
2199
  });
1620
2200
  }
@@ -2150,6 +2730,9 @@ function traceRowEvidence(item, report) {
2150
2730
  // have resolved the locator into an empty box, and that is the whole cause (m197's shape on a
2151
2731
  // static page).
2152
2732
  item.hiddenBox !== undefined ||
2733
+ // A count assertion's own reading is evidence too: the trace names the locator and the
2734
+ // elements that made the count too large, even when every request answered (m321).
2735
+ item.countMismatch !== undefined ||
2153
2736
  item.console.length > 0 ||
2154
2737
  item.requests.some((request) => !isStaticAssetRequest(request) && !unansweredRequest(request)) ||
2155
2738
  // The one shape in which a subresource with no response IS the cause: the navigation the
@@ -3207,6 +3790,75 @@ export function hiddenBoxNote(items) {
3207
3790
  }
3208
3791
  return null;
3209
3792
  }
3793
+ /** One matched element as a count row writes it: tag.class, its own text, and where it sits. */
3794
+ function countMatchSentence(match) {
3795
+ const classes = match.classes === undefined ? '' : `.${match.classes.trim().split(/\s+/).join('.')}`;
3796
+ const text = match.text === undefined ? '' : ` "${match.text}"`;
3797
+ const where = [
3798
+ match.path === undefined ? '' : `in \`${match.path}\``,
3799
+ match.inForm === true ? 'inside a form' : '',
3800
+ ]
3801
+ .filter(Boolean)
3802
+ .join(', ');
3803
+ return `\`${match.tag}${classes}\`${text}${where === '' ? '' : ` (${where})`}`;
3804
+ }
3805
+ /**
3806
+ * The row's sentence for a failing count assertion: the locator, both numbers, and the elements
3807
+ * the failure snapshot held.
3808
+ *
3809
+ * xr-molecule-flash-m321's `comments.spec.ts:86` printed only `Expected: 3 / Received: 4`. The
3810
+ * helper counted `.card:not(.comment-form) .card-block`, and the app's comment form was a
3811
+ * `.comment-form` wrapper around a plain `.card`, so the form's own `.card-block` was the fourth
3812
+ * match. The trace held the selector and the four elements; the note held neither.
3813
+ *
3814
+ * @param mismatch - The trace's count reading ({@link TraceCountMismatch}).
3815
+ * @returns The sentence.
3816
+ */
3817
+ export function countMismatchSentence(mismatch) {
3818
+ const counted = `the failing assertion counted \`${mismatch.locator}\`: ${mismatch.received} matched where ` +
3819
+ `${mismatch.expected} ${mismatch.expected === 1 ? 'was' : 'were'} expected`;
3820
+ if (mismatch.matched !== undefined && mismatch.matchedTotal !== undefined) {
3821
+ const shown = mismatch.matchedTotal - mismatch.matched.length;
3822
+ const how = mismatch.matchedFrom === 'snapshot-targets'
3823
+ ? `the trace's failure snapshot marks the assertion's ${mismatch.matchedTotal} ` +
3824
+ `${mismatch.matchedTotal === 1 ? 'match' : 'matches'}`
3825
+ : `the selector replayed against the trace's failure snapshot matched ` +
3826
+ `${mismatch.matchedTotal}`;
3827
+ return (`${counted}; ${how}: ${mismatch.matched.map(countMatchSentence).join('; ')}` +
3828
+ (shown > 0 ? `; and ${shown} more` : ''));
3829
+ }
3830
+ if (mismatch.unreadable === 'unsupported-selector')
3831
+ return (`${counted}. The trace records the locator and the count but not the elements it selected ` +
3832
+ `(a browser's own CSS query cannot express that locator for the failure snapshot), ` +
3833
+ `so read that helper's selector against the page yourself`);
3834
+ if (mismatch.unreadable === 'browser-unavailable')
3835
+ return (`${counted}. The failure snapshot could not be replayed because the project's Playwright ` +
3836
+ `module or the recorded browser's executable was not available, so the matched elements ` +
3837
+ `are not named — read that helper's selector against the page yourself`);
3838
+ if (mismatch.unreadable === 'count-mismatch')
3839
+ return (`${counted}. The failure snapshot's DOM replayed to a different count, so it is not ` +
3840
+ `evidence for this assertion and the matched elements are not named`);
3841
+ return (`${counted}. The trace recorded no readable DOM snapshot at the failure, so the matched ` +
3842
+ `elements are not named`);
3843
+ }
3844
+ /**
3845
+ * The sentence a count assertion earns when every traced failure is one — the next read before
3846
+ * the API or the request is changed.
3847
+ *
3848
+ * @param items - The traces {@link parseTraceArtifacts} read.
3849
+ * @returns The sentence, or null when not every traced failure is a count mismatch.
3850
+ */
3851
+ export function countMismatchNote(items) {
3852
+ if (items.length === 0)
3853
+ return null;
3854
+ if (!items.every((item) => item.countMismatch !== undefined))
3855
+ return null;
3856
+ return ('No listed request failed — none got a 4xx/5xx or a network error. Every failure here is a ' +
3857
+ 'COUNT over a locator, and each row names the locator the trace recorded and the elements ' +
3858
+ 'its failure snapshot held. Read the extra element and the selector before changing the ' +
3859
+ 'request or the API: a match that sits inside another element (a form, a wrapper) is the ' +
3860
+ 'selector reaching it, not the list rendering it.');
3861
+ }
3210
3862
  /**
3211
3863
  * The note a failed suite run's result carries: what each failing test's page actually sent, and
3212
3864
  * what came back.
@@ -3353,6 +4005,9 @@ export function failureTraceNote(items, target, earlierFailures = [], apiPrefixe
3353
4005
  // "the cause is in the page" sentence, which m197 read as "go look at the page" and then spent
3354
4006
  // four minutes rasterising an SVG ({@link hiddenBoxNote}).
3355
4007
  const hiddenBox = hiddenBoxNote(traced);
4008
+ // Every traced failure a count assertion: the next read is the locator's matches, not a request
4009
+ // or the page's state (m321's 30-minute `Expected: 3 / Received: 4`).
4010
+ const everyCountMismatch = countMismatchNote(traced);
3356
4011
  // m145's shape: the app's HTTP client lost its base URL, so every API call reached the page
3357
4012
  // origin without the prefix the app mounts its routes under. The prefix is READ — the caller's
3358
4013
  // client config plus what the traces show — never assumed to be `/api`.
@@ -3449,6 +4104,10 @@ export function failureTraceNote(items, target, earlierFailures = [], apiPrefixe
3449
4104
  const budget = stall?.budgetMs ?? null;
3450
4105
  let room = MAX_TRACE_NOTE_REQUESTS;
3451
4106
  const parts = [];
4107
+ // The count reading leads the row: the locator and the extra element are the fact the run
4108
+ // needs first (m321), the requests beside it explain nothing about a count.
4109
+ if (item.countMismatch !== undefined)
4110
+ parts.push(countMismatchSentence(item.countMismatch));
3452
4111
  if (step !== undefined && !stalledNav) {
3453
4112
  const head = `the test's last action was ${step.api} at ${actionClock(item, step.start)}`;
3454
4113
  const pageBefore = step.pageBefore;
@@ -3762,17 +4421,21 @@ export function failureTraceNote(items, target, earlierFailures = [], apiPrefixe
3762
4421
  `${serverSilentReading(silentApiRequests, silentNavigations, false)} ` +
3763
4422
  `Re-run only these tests against ${target} (\`--last-failed\`) or inspect the app's ` +
3764
4423
  'runtime log for the same window to distinguish the cause.'
3765
- : hasPendingRequests
3766
- ? 'No listed request failed with a 4xx/5xx or a network error, but some requests have no ' +
3767
- 'recorded response. This does not establish whether the server stalled or the browser cancelled ' +
3768
- 'the request. Read the pending paths alongside the failed action and its timeout.'
3769
- : 'No listed request failed with a 4xx/5xx or a network error. That does not establish ' +
3770
- "that its response body or the page state was correct. Read what the test's last action sent (the first line of each " +
3771
- 'row) and what the page did next: a request that answered (2xx included) while the page ' +
3772
- 'still did not show the state the assertion waited on is the fact to read — its status ' +
3773
- 'and response body, not the selector. If the request is not the one the test meant — an ' +
3774
- 'unfollow sent where the test clicked follow — the state the action read was stale ' +
3775
- 'before it.') +
4424
+ : everyCountMismatch !== null
4425
+ ? // A count assertion's own reading outranks the generic "read the response body"
4426
+ // advice, which sends the run at the request the count never depended on (m321).
4427
+ everyCountMismatch
4428
+ : hasPendingRequests
4429
+ ? 'No listed request failed with a 4xx/5xx or a network error, but some requests have no ' +
4430
+ 'recorded response. This does not establish whether the server stalled or the browser cancelled ' +
4431
+ 'the request. Read the pending paths alongside the failed action and its timeout.'
4432
+ : 'No listed request failed with a 4xx/5xx or a network error. That does not establish ' +
4433
+ "that its response body or the page state was correct. Read what the test's last action sent (the first line of each " +
4434
+ 'row) and what the page did next: a request that answered (2xx included) while the page ' +
4435
+ 'still did not show the state the assertion waited on is the fact to read — its status ' +
4436
+ 'and response body, not the selector. If the request is not the one the test meant — an ' +
4437
+ 'unfollow sent where the test clicked follow — the state the action read was stale ' +
4438
+ 'before it.') +
3776
4439
  prefixAdvice +
3777
4440
  (wrapped
3778
4441
  ? ' The API received a body wrapped in `data`: app-http’s `post(url, data, config)` / ' +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@molecule/api-ai-tools",
3
- "version": "1.10.10",
3
+ "version": "1.11.0",
4
4
  "description": "Shared AI agent tools with backend abstraction for sandbox and local execution",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",