@molecule/api-ai-tools 1.9.15 → 1.9.16
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 +71 -9
- package/dist/trace-artifacts.d.ts +78 -5
- package/dist/trace-artifacts.d.ts.map +1 -1
- package/dist/trace-artifacts.js +461 -99
- package/package.json +1 -1
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-
|
|
6
|
+
Generated: 2026-10-18T12:49:51.606Z
|
|
7
7
|
-->
|
|
8
8
|
|
|
9
9
|
# @molecule/api-ai-tools
|
|
@@ -426,6 +426,14 @@ interface TraceRequest {
|
|
|
426
426
|
* credential-less request from one whose credential is a cookie.
|
|
427
427
|
*/
|
|
428
428
|
sentCookie?: boolean
|
|
429
|
+
/**
|
|
430
|
+
* The trace member this request came from — the browser context Playwright numbered (`1`, `3`).
|
|
431
|
+
* A test that opens a second browser context writes one trace member per context, and a test's
|
|
432
|
+
* request is only evidence about the page it was made on (xr-molecule-flash-m160).
|
|
433
|
+
*/
|
|
434
|
+
context?: string
|
|
435
|
+
/** The page the request was made on, as the HAR entry's `pageref` records it. */
|
|
436
|
+
pageId?: string
|
|
429
437
|
}
|
|
430
438
|
```
|
|
431
439
|
|
|
@@ -472,6 +480,35 @@ interface TraceAction {
|
|
|
472
480
|
start: number
|
|
473
481
|
/** When it finished, or null when the run ended before it did. */
|
|
474
482
|
end: number | null
|
|
483
|
+
/** The browser context the action was recorded in (`1`, `3`), when the trace names one. */
|
|
484
|
+
context?: string
|
|
485
|
+
/** The page the action ran on, from the frame snapshot the trace paired with it. */
|
|
486
|
+
pageId?: string
|
|
487
|
+
/** The frame the action ran in, from the same snapshot. */
|
|
488
|
+
frameId?: string
|
|
489
|
+
}
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
#### `TraceContextEvidence`
|
|
493
|
+
|
|
494
|
+
What a browser context OTHER than the failing page's recorded — the second context a
|
|
495
|
+
two-context test opens for its own setup.
|
|
496
|
+
|
|
497
|
+
Its actions and requests belong to a different page: a request it sent is not the failing
|
|
498
|
+
action's effect, and the URL it left the page on is not where the assertion's page went
|
|
499
|
+
(xr-molecule-flash-m160: the helper context created an article in an editor page while the
|
|
500
|
+
primary page moved to the profile the assertion then failed on, and the note called the
|
|
501
|
+
editor page the failing page).
|
|
502
|
+
|
|
503
|
+
```typescript
|
|
504
|
+
interface TraceContextEvidence {
|
|
505
|
+
/** The trace member's context id (`3`). */
|
|
506
|
+
context: string
|
|
507
|
+
/**
|
|
508
|
+
* The app requests that context made, bounded — failures first, then the answered ones — each
|
|
509
|
+
* labelled by the note as set-up work rather than the failing page's.
|
|
510
|
+
*/
|
|
511
|
+
requests: TraceRequest[]
|
|
475
512
|
}
|
|
476
513
|
```
|
|
477
514
|
|
|
@@ -506,19 +543,44 @@ interface TraceArtifact {
|
|
|
506
543
|
*/
|
|
507
544
|
console: string[]
|
|
508
545
|
/**
|
|
509
|
-
* The test's LAST user action before the failing assertion
|
|
510
|
-
* row leads with, when the trace carries the action times
|
|
546
|
+
* The test's LAST user action before the failing assertion ON THE PAGE THE ASSERTION RAN ON, and
|
|
547
|
+
* the requests it caused — what the row leads with, when the trace carries the action times
|
|
548
|
+
* ({@link TraceLastAction}).
|
|
511
549
|
*
|
|
512
|
-
*
|
|
513
|
-
*
|
|
550
|
+
* A test that opens a second browser context writes one trace member per context, and the
|
|
551
|
+
* archive does not order them by time: the last action of the concatenation may belong to the
|
|
552
|
+
* helper's page while the assertion failed on another (xr-molecule-flash-m160). The action is
|
|
553
|
+
* therefore the failing page's own, and its `pageAfter` is that page's last frame snapshot.
|
|
554
|
+
*
|
|
555
|
+
* Absent on traces read before this field existed, on traces whose network entries recorded no
|
|
556
|
+
* clock, and on a trace with more than one used context whose failing page cannot be identified
|
|
557
|
+
* ({@link TraceArtifact.unattributed}) — the row then falls back to {@link lead}, or to no
|
|
558
|
+
* causal claim at all.
|
|
514
559
|
*/
|
|
515
560
|
lastAction?: TraceLastAction
|
|
516
561
|
/**
|
|
517
562
|
* The page's own actions in the order the trace recorded them, bounded — what a test that ran
|
|
518
563
|
* out of a per-test budget spends its time on. A test that repeats one action prints it once per
|
|
519
|
-
* repeat, so the gap between two starts is one repeat's measured cost.
|
|
564
|
+
* repeat, so the gap between two starts is one repeat's measured cost. Scoped to the page the
|
|
565
|
+
* failing assertion ran on; a second browser context's actions are not this test's page.
|
|
520
566
|
*/
|
|
521
567
|
actions: TraceAction[]
|
|
568
|
+
/**
|
|
569
|
+
* True when the trace held more than one browser context and none of them could be tied to the
|
|
570
|
+
* failing assertion's page. The note then makes no causal claim — no "the test's last action",
|
|
571
|
+
* no "the page stayed at …" — rather than attributing one context's click and URL to another
|
|
572
|
+
* context's assertion (xr-molecule-flash-m160).
|
|
573
|
+
*/
|
|
574
|
+
unattributed?: true
|
|
575
|
+
/**
|
|
576
|
+
* The browser contexts the test opened BESIDES the failing assertion's page, with the app
|
|
577
|
+
* requests each made. Kept apart so the note can name them as set-up work instead of printing
|
|
578
|
+
* their requests as the failing action's effects (xr-molecule-flash-m160).
|
|
579
|
+
*
|
|
580
|
+
* Absent when the trace recorded one context only, when every other context made no request,
|
|
581
|
+
* and on traces read before this field existed.
|
|
582
|
+
*/
|
|
583
|
+
otherContexts?: TraceContextEvidence[]
|
|
522
584
|
}
|
|
523
585
|
```
|
|
524
586
|
|
|
@@ -1905,9 +1967,9 @@ over anything else when it is the page talking to the app (`fetch`/`xhr`/`docume
|
|
|
1905
1967
|
test's trace holds the whole page load. A request with no response and NO error text was aborted
|
|
1906
1968
|
or still in flight when the test ended; it is kept too, but as what it is, not as a failure.
|
|
1907
1969
|
|
|
1908
|
-
A
|
|
1909
|
-
|
|
1910
|
-
|
|
1970
|
+
A trace.zip holds ONE trace member per browser context the test used (beside the runner's own
|
|
1971
|
+
`test.trace`), and the archive's member order is not the test's chronology. Each member is read
|
|
1972
|
+
as its own context: actions and requests keep their context id, requests and frame snapshots
|
|
1911
1973
|
|
|
1912
1974
|
_(description truncated — read the full text in `node_modules/@molecule/api-ai-tools/dist/trace-artifacts.d.ts`.)_
|
|
1913
1975
|
|
|
@@ -89,6 +89,14 @@ export interface TraceRequest {
|
|
|
89
89
|
* credential-less request from one whose credential is a cookie.
|
|
90
90
|
*/
|
|
91
91
|
sentCookie?: boolean;
|
|
92
|
+
/**
|
|
93
|
+
* The trace member this request came from — the browser context Playwright numbered (`1`, `3`).
|
|
94
|
+
* A test that opens a second browser context writes one trace member per context, and a test's
|
|
95
|
+
* request is only evidence about the page it was made on (xr-molecule-flash-m160).
|
|
96
|
+
*/
|
|
97
|
+
context?: string;
|
|
98
|
+
/** The page the request was made on, as the HAR entry's `pageref` records it. */
|
|
99
|
+
pageId?: string;
|
|
92
100
|
}
|
|
93
101
|
/**
|
|
94
102
|
* The test's LAST user action before the failing assertion, and the requests it caused.
|
|
@@ -125,6 +133,31 @@ export interface TraceAction {
|
|
|
125
133
|
start: number;
|
|
126
134
|
/** When it finished, or null when the run ended before it did. */
|
|
127
135
|
end: number | null;
|
|
136
|
+
/** The browser context the action was recorded in (`1`, `3`), when the trace names one. */
|
|
137
|
+
context?: string;
|
|
138
|
+
/** The page the action ran on, from the frame snapshot the trace paired with it. */
|
|
139
|
+
pageId?: string;
|
|
140
|
+
/** The frame the action ran in, from the same snapshot. */
|
|
141
|
+
frameId?: string;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* What a browser context OTHER than the failing page's recorded — the second context a
|
|
145
|
+
* two-context test opens for its own setup.
|
|
146
|
+
*
|
|
147
|
+
* Its actions and requests belong to a different page: a request it sent is not the failing
|
|
148
|
+
* action's effect, and the URL it left the page on is not where the assertion's page went
|
|
149
|
+
* (xr-molecule-flash-m160: the helper context created an article in an editor page while the
|
|
150
|
+
* primary page moved to the profile the assertion then failed on, and the note called the
|
|
151
|
+
* editor page the failing page).
|
|
152
|
+
*/
|
|
153
|
+
export interface TraceContextEvidence {
|
|
154
|
+
/** The trace member's context id (`3`). */
|
|
155
|
+
context: string;
|
|
156
|
+
/**
|
|
157
|
+
* The app requests that context made, bounded — failures first, then the answered ones — each
|
|
158
|
+
* labelled by the note as set-up work rather than the failing page's.
|
|
159
|
+
*/
|
|
160
|
+
requests: TraceRequest[];
|
|
128
161
|
}
|
|
129
162
|
/** What one trace says: the test it belongs to, the requests it made, and its console lines. */
|
|
130
163
|
export interface TraceArtifact {
|
|
@@ -153,19 +186,44 @@ export interface TraceArtifact {
|
|
|
153
186
|
*/
|
|
154
187
|
console: string[];
|
|
155
188
|
/**
|
|
156
|
-
* The test's LAST user action before the failing assertion
|
|
157
|
-
* row leads with, when the trace carries the action times
|
|
189
|
+
* The test's LAST user action before the failing assertion ON THE PAGE THE ASSERTION RAN ON, and
|
|
190
|
+
* the requests it caused — what the row leads with, when the trace carries the action times
|
|
191
|
+
* ({@link TraceLastAction}).
|
|
158
192
|
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
193
|
+
* A test that opens a second browser context writes one trace member per context, and the
|
|
194
|
+
* archive does not order them by time: the last action of the concatenation may belong to the
|
|
195
|
+
* helper's page while the assertion failed on another (xr-molecule-flash-m160). The action is
|
|
196
|
+
* therefore the failing page's own, and its `pageAfter` is that page's last frame snapshot.
|
|
197
|
+
*
|
|
198
|
+
* Absent on traces read before this field existed, on traces whose network entries recorded no
|
|
199
|
+
* clock, and on a trace with more than one used context whose failing page cannot be identified
|
|
200
|
+
* ({@link TraceArtifact.unattributed}) — the row then falls back to {@link lead}, or to no
|
|
201
|
+
* causal claim at all.
|
|
161
202
|
*/
|
|
162
203
|
lastAction?: TraceLastAction;
|
|
163
204
|
/**
|
|
164
205
|
* The page's own actions in the order the trace recorded them, bounded — what a test that ran
|
|
165
206
|
* out of a per-test budget spends its time on. A test that repeats one action prints it once per
|
|
166
|
-
* repeat, so the gap between two starts is one repeat's measured cost.
|
|
207
|
+
* repeat, so the gap between two starts is one repeat's measured cost. Scoped to the page the
|
|
208
|
+
* failing assertion ran on; a second browser context's actions are not this test's page.
|
|
167
209
|
*/
|
|
168
210
|
actions: TraceAction[];
|
|
211
|
+
/**
|
|
212
|
+
* True when the trace held more than one browser context and none of them could be tied to the
|
|
213
|
+
* failing assertion's page. The note then makes no causal claim — no "the test's last action",
|
|
214
|
+
* no "the page stayed at …" — rather than attributing one context's click and URL to another
|
|
215
|
+
* context's assertion (xr-molecule-flash-m160).
|
|
216
|
+
*/
|
|
217
|
+
unattributed?: true;
|
|
218
|
+
/**
|
|
219
|
+
* The browser contexts the test opened BESIDES the failing assertion's page, with the app
|
|
220
|
+
* requests each made. Kept apart so the note can name them as set-up work instead of printing
|
|
221
|
+
* their requests as the failing action's effects (xr-molecule-flash-m160).
|
|
222
|
+
*
|
|
223
|
+
* Absent when the trace recorded one context only, when every other context made no request,
|
|
224
|
+
* and on traces read before this field existed.
|
|
225
|
+
*/
|
|
226
|
+
otherContexts?: TraceContextEvidence[];
|
|
169
227
|
}
|
|
170
228
|
/** How many traces one read reports, newest first. */
|
|
171
229
|
export declare const MAX_TRACE_ARTIFACTS = 12;
|
|
@@ -213,6 +271,13 @@ export declare const TRACE_NOTE_LEAD_GOT_CHARS = 200;
|
|
|
213
271
|
* test's trace holds the whole page load. A request with no response and NO error text was aborted
|
|
214
272
|
* or still in flight when the test ended; it is kept too, but as what it is, not as a failure.
|
|
215
273
|
*
|
|
274
|
+
* A trace.zip holds ONE trace member per browser context the test used (beside the runner's own
|
|
275
|
+
* `test.trace`), and the archive's member order is not the test's chronology. Each member is read
|
|
276
|
+
* as its own context: actions and requests keep their context id, requests and frame snapshots
|
|
277
|
+
* keep the pageId Playwright records, and the failing assertion's page — the page its own step id
|
|
278
|
+
* ran on — decides which actions, snapshots and requests become the test's row. What the test's
|
|
279
|
+
* other browser contexts recorded is reported apart, as `otherContexts` (xr-molecule-flash-m160).
|
|
280
|
+
*
|
|
216
281
|
* A request answered 4xx by the app's OWN API also keeps what the page SENT and the first of what
|
|
217
282
|
* came back: Playwright stores either inline (`text`) or in a resource entry (`_file`), and both
|
|
218
283
|
* are read. That is where a wrapped body shows: xr-molecule-flash-m111's helper passed
|
|
@@ -346,6 +411,14 @@ export interface TraceRunReport {
|
|
|
346
411
|
* (answered 201, the page still rendering signed-out) never appeared. With no action clock, the
|
|
347
412
|
* row falls back to {@link TraceArtifact.lead}: the page's last answered fetch/xhr.
|
|
348
413
|
*
|
|
414
|
+
* The action, the URL and the requests are the FAILING ASSERTION'S PAGE's ({@link
|
|
415
|
+
* TraceArtifact.lastAction}), never the archive's last context: a test that creates its data in a
|
|
416
|
+
* second browser context writes that context's click and `/editor` snapshot into the same zip, and
|
|
417
|
+
* reading the concatenation made them the failing page (xr-molecule-flash-m160). The other
|
|
418
|
+
* contexts' requests are named separately as set-up work. When the trace holds more than one used
|
|
419
|
+
* context and the assertion's page cannot be identified, the note lists the requests and makes NO
|
|
420
|
+
* causal claim — no "the test's last action", no "the page stayed at …" — instead of merging them.
|
|
421
|
+
*
|
|
349
422
|
* WHICH rows it spends is evidence, not artifact order (xr-molecule-flash-m112 +53:46): a row
|
|
350
423
|
* needs a recorded response, a real failure or a console line ({@link traceRowEvidence}), rows are
|
|
351
424
|
* deduplicated by the test's location, and they are ordered real failure → repeated from an
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"trace-artifacts.d.ts","sourceRoot":"","sources":["../src/trace-artifacts.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAMH,iEAAiE;AACjE,MAAM,WAAW,YAAY;IAC3B,gCAAgC;IAChC,MAAM,EAAE,MAAM,CAAA;IACd,iGAAiG;IACjG,IAAI,EAAE,MAAM,CAAA;IACZ;;;;OAIG;IACH,MAAM,EAAE,MAAM,CAAA;IACd;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB;;;;;OAKG;IACH,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,oFAAoF;IACpF,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ;;;;OAIG;IACH,IAAI,CAAC,EAAE,MAAM,CAAA;IACb;;;;;OAKG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;;;;;;OAQG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAA;IAC3B;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB;;;;OAIG;IACH,UAAU,CAAC,EAAE,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"trace-artifacts.d.ts","sourceRoot":"","sources":["../src/trace-artifacts.ts"],"names":[],"mappings":"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"}
|
package/dist/trace-artifacts.js
CHANGED
|
@@ -84,6 +84,13 @@ const TRACE_NOTE_FAILURE_CHARS = 120;
|
|
|
84
84
|
* test's trace holds the whole page load. A request with no response and NO error text was aborted
|
|
85
85
|
* or still in flight when the test ended; it is kept too, but as what it is, not as a failure.
|
|
86
86
|
*
|
|
87
|
+
* A trace.zip holds ONE trace member per browser context the test used (beside the runner's own
|
|
88
|
+
* `test.trace`), and the archive's member order is not the test's chronology. Each member is read
|
|
89
|
+
* as its own context: actions and requests keep their context id, requests and frame snapshots
|
|
90
|
+
* keep the pageId Playwright records, and the failing assertion's page — the page its own step id
|
|
91
|
+
* ran on — decides which actions, snapshots and requests become the test's row. What the test's
|
|
92
|
+
* other browser contexts recorded is reported apart, as `otherContexts` (xr-molecule-flash-m160).
|
|
93
|
+
*
|
|
87
94
|
* A request answered 4xx by the app's OWN API also keeps what the page SENT and the first of what
|
|
88
95
|
* came back: Playwright stores either inline (`text`) or in a resource entry (`_file`), and both
|
|
89
96
|
* are read. That is where a wrapped body shows: xr-molecule-flash-m111's helper passed
|
|
@@ -276,26 +283,69 @@ const found = []
|
|
|
276
283
|
walk(ROOT, 5, found)
|
|
277
284
|
found.sort(function (a, b) { return b.mtime - a.mtime })
|
|
278
285
|
|
|
286
|
+
// A trace.zip holds ONE trace member per browser context the test used (1-trace.trace,
|
|
287
|
+
// 3-trace.trace, …) beside the test runner's own test.trace. The archive ORDER of those members
|
|
288
|
+
// is not the test's chronology: xr-molecule-flash-m160's helper context was written after the
|
|
289
|
+
// primary page's, so the concatenation's last user action was the helper's editor click and the
|
|
290
|
+
// last frame snapshot was /editor, while the assertion had failed on the primary page at
|
|
291
|
+
// /profile/:username. Each member is read as its own context, and everything below attributes a
|
|
292
|
+
// request, an action and a frame snapshot to the page and context that recorded it.
|
|
293
|
+
function traceContextOf(name) {
|
|
294
|
+
if (name === 'test.trace') return 'test'
|
|
295
|
+
if (name === 'trace.trace') return '0'
|
|
296
|
+
const m = /^(.+)-trace\\.trace$/.exec(name)
|
|
297
|
+
return m === null ? null : m[1]
|
|
298
|
+
}
|
|
299
|
+
function networkContextOf(name) {
|
|
300
|
+
if (name === 'trace.network') return '0'
|
|
301
|
+
const m = /^(.+)-trace\\.network$/.exec(name)
|
|
302
|
+
return m === null ? null : m[1]
|
|
303
|
+
}
|
|
304
|
+
|
|
279
305
|
for (const one of found.slice(0, MAX_TRACES)) {
|
|
280
306
|
let buf
|
|
281
307
|
try { buf = fs.readFileSync(one.file) } catch (e) { continue }
|
|
282
308
|
let entries
|
|
283
309
|
try { entries = zipEntries(buf) } catch (e) { continue }
|
|
284
|
-
const
|
|
285
|
-
const
|
|
310
|
+
const runner = []
|
|
311
|
+
const contexts = []
|
|
312
|
+
const contextById = {}
|
|
313
|
+
function contextFor(id) {
|
|
314
|
+
let ctx = contextById[id]
|
|
315
|
+
if (!ctx) {
|
|
316
|
+
ctx = { id: id, trace: [], network: [], requests: [], actions: [], frames: [] }
|
|
317
|
+
contextById[id] = ctx
|
|
318
|
+
contexts.push(ctx)
|
|
319
|
+
}
|
|
320
|
+
return ctx
|
|
321
|
+
}
|
|
286
322
|
for (const entry of entries) {
|
|
287
|
-
|
|
288
|
-
|
|
323
|
+
const traceContext = traceContextOf(entry.name)
|
|
324
|
+
if (traceContext === 'test') {
|
|
325
|
+
runner.push.apply(runner, ndjson(zipData(buf, entry)))
|
|
326
|
+
continue
|
|
327
|
+
}
|
|
328
|
+
if (traceContext !== null) {
|
|
329
|
+
const ctx = contextFor(traceContext)
|
|
330
|
+
ctx.trace.push.apply(ctx.trace, ndjson(zipData(buf, entry)))
|
|
331
|
+
continue
|
|
332
|
+
}
|
|
333
|
+
const networkContext = networkContextOf(entry.name)
|
|
334
|
+
if (networkContext !== null) {
|
|
335
|
+
const ctx = contextFor(networkContext)
|
|
336
|
+
ctx.network.push.apply(ctx.network, ndjson(zipData(buf, entry)))
|
|
337
|
+
}
|
|
289
338
|
}
|
|
290
|
-
const
|
|
291
|
-
for (const
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
339
|
+
const requestsAll = []
|
|
340
|
+
for (const ctx of contexts) {
|
|
341
|
+
for (const line of ctx.network) {
|
|
342
|
+
if (!line || line.type !== 'resource-snapshot') continue
|
|
343
|
+
const snapshot = line.snapshot || {}
|
|
344
|
+
const request = snapshot.request || {}
|
|
345
|
+
if (!request.url || !request.method) continue
|
|
346
|
+
const response = snapshot.response || {}
|
|
347
|
+
const type = String(snapshot._resourceType || '')
|
|
348
|
+
if (!origin) origin = type === 'document' ? originOf(String(request.url)) : ''
|
|
299
349
|
// What the request carried for CREDENTIALS — never the secret itself. A 401 on the app's own
|
|
300
350
|
// API cannot be read without this: m148's favorite POST answered 401 with no Authorization
|
|
301
351
|
// header at all, so the client never attached the token, and the note sent the run to the
|
|
@@ -312,51 +362,213 @@ for (const one of found.slice(0, MAX_TRACES)) {
|
|
|
312
362
|
const authSpace = authValue.search(/\\s/)
|
|
313
363
|
const authWord = authSpace > 0 ? authValue.slice(0, authSpace) : ''
|
|
314
364
|
const authScheme = /^[A-Za-z][A-Za-z0-9._~+/-]{0,23}$/.test(authWord) ? authWord : undefined
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
365
|
+
const sentCookie = headerList.some(function (h) {
|
|
366
|
+
return h && typeof h.name === 'string' && /^cookie$/i.test(h.name)
|
|
367
|
+
})
|
|
368
|
+
const parsed = {
|
|
369
|
+
method: String(request.method).toUpperCase(),
|
|
370
|
+
path: String(request.url).slice(0, MAX_URL),
|
|
371
|
+
status: Number(response.status) || 0,
|
|
372
|
+
failure: typeof response._failureText === 'string' ? response._failureText : undefined,
|
|
373
|
+
type: type,
|
|
374
|
+
sentAuthorization: authHeader !== undefined,
|
|
375
|
+
authorization: authScheme,
|
|
376
|
+
sentCookie: sentCookie,
|
|
377
|
+
// The HAR entry carries the monotonic clock the trace's action entries use, so a request
|
|
378
|
+
// can be attributed to the action that caused it (see \`lastAction\` below).
|
|
379
|
+
startedAt: typeof snapshot._monotonicTime === 'number' ? snapshot._monotonicTime : undefined,
|
|
380
|
+
// The context member and the page ref the HAR entry records: a request made by the test's
|
|
381
|
+
// helper context is not the failing page's, however late the archive wrote it (m160).
|
|
382
|
+
context: ctx.id,
|
|
383
|
+
pageId: typeof snapshot.pageref === 'string' ? snapshot.pageref : undefined,
|
|
384
|
+
postData: request.postData,
|
|
385
|
+
content: response.content,
|
|
386
|
+
}
|
|
387
|
+
ctx.requests.push(parsed)
|
|
388
|
+
requestsAll.push(parsed)
|
|
389
|
+
}
|
|
333
390
|
}
|
|
334
391
|
if (!origin) {
|
|
335
|
-
const first =
|
|
392
|
+
const first = requestsAll.find(function (r) { return r.type === 'document' }) || requestsAll[0]
|
|
336
393
|
origin = first ? originOf(first.path) : ''
|
|
337
394
|
}
|
|
338
|
-
for (const r of
|
|
395
|
+
for (const r of requestsAll) r.path = pathOf(r.path).slice(0, MAX_URL)
|
|
339
396
|
// The page's own actions, timed. before/after pair on the call id, and only the LIBRARY
|
|
340
397
|
// traces carry a class and a method (Frame.goto, Locator.fill); the test runner's own
|
|
341
|
-
// trace times its hooks and fixtures with class Test, which is not a page action.
|
|
342
|
-
// the
|
|
343
|
-
//
|
|
344
|
-
const
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
398
|
+
// trace times its hooks and fixtures with class Test, which is not a page action. Each action
|
|
399
|
+
// keeps the context it came from and the page/frame its frame snapshot names, so the failing
|
|
400
|
+
// assertion's own page can be selected below (m160).
|
|
401
|
+
const actionsAll = []
|
|
402
|
+
const framesAll = []
|
|
403
|
+
for (const ctx of contexts) {
|
|
404
|
+
const ended = {}
|
|
405
|
+
for (const entry of ctx.trace) {
|
|
406
|
+
if (entry && entry.type === 'after' && typeof entry.callId === 'string' && typeof entry.endTime === 'number')
|
|
407
|
+
ended[entry.callId] = entry.endTime
|
|
408
|
+
}
|
|
409
|
+
const pageByCall = {}
|
|
410
|
+
for (const entry of ctx.trace) {
|
|
411
|
+
if (!entry || entry.type !== 'frame-snapshot') continue
|
|
412
|
+
const snap = entry.snapshot || {}
|
|
413
|
+
if (typeof snap.callId === 'string' && typeof snap.pageId === 'string' && pageByCall[snap.callId] === undefined)
|
|
414
|
+
pageByCall[snap.callId] = snap.pageId
|
|
415
|
+
if (snap.isMainFrame === false) continue
|
|
416
|
+
if (typeof snap.frameUrl !== 'string' || typeof snap.timestamp !== 'number') continue
|
|
417
|
+
ctx.frames.push({
|
|
418
|
+
url: pathOf(snap.frameUrl).slice(0, MAX_URL),
|
|
419
|
+
at: snap.timestamp,
|
|
420
|
+
context: ctx.id,
|
|
421
|
+
pageId: typeof snap.pageId === 'string' ? snap.pageId : undefined,
|
|
422
|
+
frameId: typeof snap.frameId === 'string' ? snap.frameId : undefined,
|
|
423
|
+
})
|
|
424
|
+
}
|
|
425
|
+
for (const entry of ctx.trace) {
|
|
426
|
+
if (!entry || entry.type !== 'before') continue
|
|
427
|
+
if (typeof entry.class !== 'string' || typeof entry.method !== 'string') continue
|
|
428
|
+
if (entry.class === 'Test' || typeof entry.startTime !== 'number') continue
|
|
429
|
+
const action = {
|
|
430
|
+
api: entry.class + '.' + entry.method,
|
|
431
|
+
start: entry.startTime,
|
|
432
|
+
end: typeof ended[entry.callId] === 'number' ? ended[entry.callId] : null,
|
|
433
|
+
context: ctx.id,
|
|
434
|
+
pageId: typeof entry.callId === 'string' ? pageByCall[entry.callId] : undefined,
|
|
435
|
+
frameId: undefined,
|
|
436
|
+
stepId: typeof entry.stepId === 'string' ? entry.stepId : undefined,
|
|
437
|
+
}
|
|
438
|
+
if (action.pageId !== undefined) {
|
|
439
|
+
for (const frame of ctx.frames) {
|
|
440
|
+
if (frame.pageId !== action.pageId) continue
|
|
441
|
+
action.frameId = frame.frameId
|
|
442
|
+
break
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
ctx.actions.push(action)
|
|
446
|
+
}
|
|
447
|
+
// Monotonic time, not archive order: two contexts interleave in the zip, and one context's
|
|
448
|
+
// own actions still have to be read in the order the page performed them.
|
|
449
|
+
ctx.actions.sort(function (a, b) { return a.start - b.start })
|
|
450
|
+
ctx.frames.sort(function (a, b) { return a.at - b.at })
|
|
451
|
+
for (const action of ctx.actions) actionsAll.push(action)
|
|
452
|
+
for (const frame of ctx.frames) framesAll.push(frame)
|
|
348
453
|
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
454
|
+
// The step the runner itself reports as failed, and the location it names. The runner writes
|
|
455
|
+
// the error on the failed step's \`after\` entry; taking the LAST step with a spec stack instead
|
|
456
|
+
// named the afterEach's \`context.close()\` (m160's social.spec.ts:13) rather than the strict-mode
|
|
457
|
+
// assertion at :90. The first error is the test's own failure — an afterEach that also fails
|
|
458
|
+
// runs after it.
|
|
459
|
+
let failedStepId = null
|
|
460
|
+
let failedStep = null
|
|
461
|
+
let failedEnd = null
|
|
462
|
+
let failureSpec = null
|
|
463
|
+
let failureLine = 0
|
|
464
|
+
let lastSpec = null
|
|
465
|
+
let lastSpecLine = 0
|
|
466
|
+
for (const entry of runner) {
|
|
467
|
+
if (!entry) continue
|
|
468
|
+
if (entry.type === 'after' && entry.error && typeof entry.callId === 'string' && failedStepId === null) {
|
|
469
|
+
failedStepId = entry.callId
|
|
470
|
+
failedEnd = typeof entry.endTime === 'number' ? entry.endTime : null
|
|
471
|
+
}
|
|
359
472
|
}
|
|
473
|
+
// The fallback location — the last step that names a spec — is read from every member, runner
|
|
474
|
+
// and library alike: a trace written without a test.trace (a synthetic fixture, an older
|
|
475
|
+
// Playwright) still names its spec on the library entries, and dropping that would leave the
|
|
476
|
+
// row placeless.
|
|
477
|
+
for (const traceEntries of [runner].concat(contexts.map(function (ctx) { return ctx.trace }))) {
|
|
478
|
+
for (const entry of traceEntries) {
|
|
479
|
+
if (!entry || entry.type !== 'before' || !Array.isArray(entry.stack)) continue
|
|
480
|
+
for (const frame of entry.stack) {
|
|
481
|
+
if (typeof frame.file !== 'string' || !/\\.(?:spec|test)\\.[cm]?[jt]sx?$/.test(frame.file)) continue
|
|
482
|
+
lastSpec = specPathOf(frame.file)
|
|
483
|
+
lastSpecLine = Number(frame.line) || 0
|
|
484
|
+
break
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
if (failedStepId !== null) {
|
|
489
|
+
for (const entry of runner) {
|
|
490
|
+
if (!entry || entry.type !== 'before' || entry.callId !== failedStepId) continue
|
|
491
|
+
failedStep = entry
|
|
492
|
+
if (!Array.isArray(entry.stack)) break
|
|
493
|
+
for (const frame of entry.stack) {
|
|
494
|
+
if (typeof frame.file !== 'string' || !/\\.(?:spec|test)\\.[cm]?[jt]sx?$/.test(frame.file)) continue
|
|
495
|
+
failureSpec = specPathOf(frame.file)
|
|
496
|
+
failureLine = Number(frame.line) || 0
|
|
497
|
+
break
|
|
498
|
+
}
|
|
499
|
+
break
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
const spec = failureSpec !== null ? failureSpec : lastSpec
|
|
503
|
+
const line = failureSpec !== null ? failureLine : lastSpecLine
|
|
504
|
+
// The page the failing step ran on. The library trace records the runner's step id on every
|
|
505
|
+
// action it performed for that step, and each action's frame snapshot carries its pageId — so
|
|
506
|
+
// the failing assertion's page is the step's own page, never the last page any context left
|
|
507
|
+
// behind (m160: the helper context's /editor won because its member came later in the zip).
|
|
508
|
+
let failedPageIds = []
|
|
509
|
+
if (failedStepId !== null) {
|
|
510
|
+
for (const action of actionsAll) {
|
|
511
|
+
if (action.stepId !== failedStepId || action.pageId === undefined) continue
|
|
512
|
+
if (failedPageIds.indexOf(action.pageId) === -1) failedPageIds.push(action.pageId)
|
|
513
|
+
}
|
|
514
|
+
// A step with no library action of its own (a plain \`page.waitForTimeout\`, a runner-only
|
|
515
|
+
// step) falls back to the actions that ran inside its own interval.
|
|
516
|
+
if (failedPageIds.length === 0 && failedStep !== null && typeof failedStep.startTime === 'number') {
|
|
517
|
+
for (const action of actionsAll) {
|
|
518
|
+
if (action.pageId === undefined) continue
|
|
519
|
+
if (action.start < failedStep.startTime - 1) continue
|
|
520
|
+
if (failedEnd !== null && action.start > failedEnd + 1) continue
|
|
521
|
+
if (failedPageIds.indexOf(action.pageId) === -1) failedPageIds.push(action.pageId)
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
let failedPage = failedPageIds.length === 1 ? failedPageIds[0] : null
|
|
526
|
+
// A member with no action, request or frame is not a context the test used: Playwright writes
|
|
527
|
+
// a trace entry for a context it opened and closed without a page, and counting that as a
|
|
528
|
+
// second context would make every ordinary trace unattributable.
|
|
529
|
+
const usedContexts = contexts.filter(function (ctx) {
|
|
530
|
+
return ctx.actions.length > 0 || ctx.requests.length > 0 || ctx.frames.length > 0
|
|
531
|
+
})
|
|
532
|
+
// One context, one page: nothing to attribute, and no second context to merge.
|
|
533
|
+
if (failedPage === null && usedContexts.length === 1) {
|
|
534
|
+
const pages = []
|
|
535
|
+
for (const frame of framesAll) {
|
|
536
|
+
if (frame.pageId !== undefined && pages.indexOf(frame.pageId) === -1) pages.push(frame.pageId)
|
|
537
|
+
}
|
|
538
|
+
if (pages.length === 1) failedPage = pages[0]
|
|
539
|
+
}
|
|
540
|
+
let failedContext = null
|
|
541
|
+
if (failedPage !== null) {
|
|
542
|
+
for (const frame of framesAll) {
|
|
543
|
+
if (frame.pageId !== failedPage) continue
|
|
544
|
+
failedContext = frame.context
|
|
545
|
+
break
|
|
546
|
+
}
|
|
547
|
+
if (failedContext === null) {
|
|
548
|
+
for (const action of actionsAll) {
|
|
549
|
+
if (action.pageId !== failedPage) continue
|
|
550
|
+
failedContext = action.context
|
|
551
|
+
break
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
if (failedContext === null) {
|
|
555
|
+
for (const r of requestsAll) {
|
|
556
|
+
if (r.pageId !== failedPage) continue
|
|
557
|
+
failedContext = r.context
|
|
558
|
+
break
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
// Two used contexts and no identified page: the note makes no causal claim rather than
|
|
563
|
+
// merging them (m160's rule). With one used context everything in it belongs to the test's page.
|
|
564
|
+
const unattributed = failedPage === null && usedContexts.length > 1
|
|
565
|
+
const actions = failedPage === null ? actionsAll : actionsAll.filter(function (a) { return a.pageId === failedPage })
|
|
566
|
+
const frames = failedPage === null ? (usedContexts.length === 1 ? framesAll : []) : framesAll.filter(function (f) { return f.pageId === failedPage })
|
|
567
|
+
let requests = failedPage === null ? requestsAll : requestsAll.filter(function (r) { return r.pageId === failedPage })
|
|
568
|
+
// A trace whose HAR entries carry no page ref: the failing context's own requests are still
|
|
569
|
+
// its page's, because a context in a test with one page has nowhere else to send them.
|
|
570
|
+
if (failedPage !== null && requests.length === 0 && failedContext !== null)
|
|
571
|
+
requests = requestsAll.filter(function (r) { return r.context === failedContext })
|
|
360
572
|
// A \`page.goto\` that has no end time: the page never finished loading. When that is the
|
|
361
573
|
// failure, a document subresource with no response is the reason — the navigation waits for
|
|
362
574
|
// its stylesheets and scripts — and it is evidence, not the aborted noise a finished
|
|
@@ -446,7 +658,9 @@ for (const one of found.slice(0, MAX_TRACES)) {
|
|
|
446
658
|
// then rendering signed-out) leads instead of the Home page's own fetch.
|
|
447
659
|
const USER_ACTION = /\\.(?:click|dblclick|tap|fill|press|pressSequentially|selectOption|check|uncheck|setInputFiles|goto|type)$/
|
|
448
660
|
const userActions = actions.filter(function (a) { return USER_ACTION.test(a.api) })
|
|
449
|
-
|
|
661
|
+
// With two contexts and no identified page there is no causal claim to make: the last action
|
|
662
|
+
// of the concatenation could belong to the helper's page (m160).
|
|
663
|
+
const lastAction = unattributed || userActions.length === 0 ? null : userActions[userActions.length - 1]
|
|
450
664
|
// A request the action caused started after it (a millisecond of clock slack) and before the
|
|
451
665
|
// assertion — which is everything later in the trace. A request that started before it is the
|
|
452
666
|
// page's own load, not the action's; the page shell's files are never the step's, except a
|
|
@@ -511,19 +725,13 @@ for (const one of found.slice(0, MAX_TRACES)) {
|
|
|
511
725
|
...(r.sentAuthorization ? { sentAuthorization: true } : {}),
|
|
512
726
|
...(r.authorization ? { authorization: r.authorization } : {}),
|
|
513
727
|
...(r.sentCookie ? { sentCookie: true } : {}),
|
|
728
|
+
...(r.context ? { context: r.context } : {}),
|
|
729
|
+
...(r.pageId ? { pageId: r.pageId } : {}),
|
|
514
730
|
}
|
|
515
731
|
}
|
|
516
|
-
// The
|
|
517
|
-
//
|
|
518
|
-
//
|
|
519
|
-
const frames = []
|
|
520
|
-
for (const entry of trace) {
|
|
521
|
-
if (!entry || entry.type !== 'frame-snapshot') continue
|
|
522
|
-
const snap = entry.snapshot || {}
|
|
523
|
-
if (snap.isMainFrame === false) continue
|
|
524
|
-
if (typeof snap.frameUrl !== 'string' || typeof snap.timestamp !== 'number') continue
|
|
525
|
-
frames.push({ url: pathOf(snap.frameUrl).slice(0, MAX_URL), at: snap.timestamp })
|
|
526
|
-
}
|
|
732
|
+
// The failing page's own frame snapshots, timed: what the page showed when the action ran, and
|
|
733
|
+
// where it ended up. They are the snapshots of the assertion's page only — another context's
|
|
734
|
+
// editor page is not where this page went (m160) — and they are ordered by the trace's clock.
|
|
527
735
|
let last = null
|
|
528
736
|
if (lastAction !== null) {
|
|
529
737
|
let pageBefore
|
|
@@ -547,8 +755,9 @@ for (const one of found.slice(0, MAX_TRACES)) {
|
|
|
547
755
|
}
|
|
548
756
|
// The fallback lead stays for traces with no action clock. With an action, only a request it
|
|
549
757
|
// caused is the lead; an action that sent none leaves the page's own load to the row's
|
|
550
|
-
// "also answered" bucket, not to a lead that would hide it.
|
|
551
|
-
|
|
758
|
+
// "also answered" bucket, not to a lead that would hide it. With two contexts and no identified
|
|
759
|
+
// page there is no causal lead at all (m160).
|
|
760
|
+
if (!unattributed && lead === null && lastAction === null) {
|
|
552
761
|
let leadIndex = -1
|
|
553
762
|
for (let i = requests.length - 1; i >= 0; i--) {
|
|
554
763
|
const r = requests[i]
|
|
@@ -608,24 +817,49 @@ for (const one of found.slice(0, MAX_TRACES)) {
|
|
|
608
817
|
const consoleErrors = []
|
|
609
818
|
const consoleSignals = []
|
|
610
819
|
const consoleWarnings = []
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
820
|
+
// The console lines of the page the assertion ran on; with no identified page, of every context.
|
|
821
|
+
const consoleTraces =
|
|
822
|
+
failedContext === null
|
|
823
|
+
? contexts.map(function (ctx) { return ctx.trace })
|
|
824
|
+
: contexts.filter(function (ctx) { return ctx.id === failedContext }).map(function (ctx) { return ctx.trace })
|
|
825
|
+
for (const traceEntries of consoleTraces) {
|
|
826
|
+
for (const entry of traceEntries) {
|
|
827
|
+
if (!entry) continue
|
|
828
|
+
if (entry.type === 'console' && typeof entry.text === 'string') {
|
|
829
|
+
if (entry.messageType === 'error') consoleErrors.push(entry.text)
|
|
830
|
+
else if (consoleSignal(entry.text)) consoleSignals.push(entry.text)
|
|
831
|
+
else if (entry.messageType === 'warning' || entry.messageType === 'warn')
|
|
832
|
+
consoleWarnings.push(entry.text)
|
|
833
|
+
} else if (entry.type === 'event' && entry.method === 'pageError') {
|
|
834
|
+
const message = messageOf(entry.params)
|
|
835
|
+
if (message) consoleErrors.push(message)
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
// Evidence from the OTHER browser contexts the test opened — kept apart from the failing page's
|
|
840
|
+
// requests and labelled as set-up work, never as the failing action's effect (m160).
|
|
841
|
+
const otherContexts = []
|
|
842
|
+
if (failedContext !== null) {
|
|
843
|
+
for (const ctx of usedContexts) {
|
|
844
|
+
if (ctx.id === failedContext) continue
|
|
845
|
+
const picked = []
|
|
846
|
+
for (const r of ctx.requests) {
|
|
847
|
+
if (shellRequest(r)) continue
|
|
848
|
+
const sent = bodyText(buf, entries, r.postData, MAX_BODY)
|
|
849
|
+
const got = gotText(buf, entries, r)
|
|
850
|
+
if (sent) r.sent = sent
|
|
851
|
+
if (got) r.got = got
|
|
852
|
+
picked.push(r)
|
|
628
853
|
}
|
|
854
|
+
if (picked.length === 0 && ctx.actions.length === 0) continue
|
|
855
|
+
// Failures first, then by the trace's clock: a 401 from the helper context is worth naming
|
|
856
|
+
// even when its later answered calls would fill the bound.
|
|
857
|
+
picked.sort(function (a, b) {
|
|
858
|
+
const left = failedRequest(a) ? 0 : 1
|
|
859
|
+
const right = failedRequest(b) ? 0 : 1
|
|
860
|
+
return left === right ? (a.startedAt || 0) - (b.startedAt || 0) : left - right
|
|
861
|
+
})
|
|
862
|
+
otherContexts.push({ context: ctx.id, requests: picked.slice(0, MAX_NOTE).map(wire) })
|
|
629
863
|
}
|
|
630
864
|
}
|
|
631
865
|
// Chromium reports every failed request as a console error too; where the page logged something
|
|
@@ -649,6 +883,8 @@ for (const one of found.slice(0, MAX_TRACES)) {
|
|
|
649
883
|
requests: chosen.slice(0, MAX_REQUESTS),
|
|
650
884
|
console: keptConsole.slice(0, MAX_CONSOLE).map(function (text) { return text.slice(0, 300) }),
|
|
651
885
|
actions: actions.slice(-MAX_ACTIONS),
|
|
886
|
+
...(unattributed ? { unattributed: true } : {}),
|
|
887
|
+
...(otherContexts.length > 0 ? { otherContexts: otherContexts } : {}),
|
|
652
888
|
}) + '\\n')
|
|
653
889
|
}
|
|
654
890
|
`;
|
|
@@ -677,6 +913,8 @@ function parseTraceRequest(value) {
|
|
|
677
913
|
? { authorization: r.authorization }
|
|
678
914
|
: {}),
|
|
679
915
|
...(r.sentCookie === true ? { sentCookie: true } : {}),
|
|
916
|
+
...(typeof r.context === 'string' && r.context ? { context: r.context } : {}),
|
|
917
|
+
...(typeof r.pageId === 'string' && r.pageId ? { pageId: r.pageId } : {}),
|
|
680
918
|
};
|
|
681
919
|
}
|
|
682
920
|
/**
|
|
@@ -724,6 +962,17 @@ export function parseTraceArtifacts(stdout) {
|
|
|
724
962
|
const actions = Array.isArray(raw.actions) ? raw.actions : [];
|
|
725
963
|
const lead = parseTraceRequest(raw.lead);
|
|
726
964
|
const lastAction = parseLastAction(raw.lastAction);
|
|
965
|
+
const otherContexts = Array.isArray(raw.otherContexts)
|
|
966
|
+
? raw.otherContexts
|
|
967
|
+
.filter((c) => typeof c === 'object' && c !== null)
|
|
968
|
+
.filter((c) => typeof c.context === 'string')
|
|
969
|
+
.map((c) => ({
|
|
970
|
+
context: String(c.context),
|
|
971
|
+
requests: (Array.isArray(c.requests) ? c.requests : [])
|
|
972
|
+
.map(parseTraceRequest)
|
|
973
|
+
.filter((request) => request !== null),
|
|
974
|
+
}))
|
|
975
|
+
: [];
|
|
727
976
|
out.push({
|
|
728
977
|
spec: typeof raw.spec === 'string' ? raw.spec : null,
|
|
729
978
|
line: typeof raw.line === 'number' ? raw.line : 0,
|
|
@@ -740,7 +989,12 @@ export function parseTraceArtifacts(stdout) {
|
|
|
740
989
|
api: String(a.api),
|
|
741
990
|
start: Number(a.start),
|
|
742
991
|
end: a.end === null || a.end === undefined ? null : Number(a.end),
|
|
992
|
+
...(typeof a.context === 'string' ? { context: a.context } : {}),
|
|
993
|
+
...(typeof a.pageId === 'string' ? { pageId: a.pageId } : {}),
|
|
994
|
+
...(typeof a.frameId === 'string' ? { frameId: a.frameId } : {}),
|
|
743
995
|
})),
|
|
996
|
+
...(otherContexts.length === 0 ? {} : { otherContexts }),
|
|
997
|
+
...(raw.unattributed === true ? { unattributed: true } : {}),
|
|
744
998
|
});
|
|
745
999
|
}
|
|
746
1000
|
catch (_error) {
|
|
@@ -1062,6 +1316,36 @@ function runFailureOf(item, failures) {
|
|
|
1062
1316
|
}
|
|
1063
1317
|
return undefined;
|
|
1064
1318
|
}
|
|
1319
|
+
/**
|
|
1320
|
+
* Where the row names this failing test.
|
|
1321
|
+
*
|
|
1322
|
+
* The trace names the line of the step it recorded — the failing assertion when the runner wrote
|
|
1323
|
+
* its error on that step, and the suite's `afterEach` (`context.close()`) when it did not. The
|
|
1324
|
+
* runner's own summary names the test's DECLARATION line, and that is the location the result
|
|
1325
|
+
* quotes and the line a re-run command takes, so it wins whenever the run reported one for this
|
|
1326
|
+
* file: matched by the trace's own line inside the test body ({@link runFailureOf}), and otherwise
|
|
1327
|
+
* by the file alone when that file has exactly ONE reported failure — an `afterEach` line sits
|
|
1328
|
+
* BEFORE the declaration, so the line window cannot match it (xr-molecule-flash-m163's row said
|
|
1329
|
+
* `navigation.spec.ts:12`, the suite's `afterEach`, for the test declared at `:200`).
|
|
1330
|
+
*
|
|
1331
|
+
* @param item - One traced failure.
|
|
1332
|
+
* @param failures - The runner's report, one entry per failing test.
|
|
1333
|
+
* @returns The `file:line` the row names.
|
|
1334
|
+
*/
|
|
1335
|
+
function failureWhereOf(item, failures) {
|
|
1336
|
+
const spec = item.spec;
|
|
1337
|
+
if (spec === null)
|
|
1338
|
+
return whereOf(item);
|
|
1339
|
+
let reported = runFailureOf(item, failures);
|
|
1340
|
+
if (reported === undefined) {
|
|
1341
|
+
const sameFile = failures.filter((failure) => sameLocationFile(spec, failure.spec));
|
|
1342
|
+
if (sameFile.length === 1)
|
|
1343
|
+
reported = sameFile[0];
|
|
1344
|
+
}
|
|
1345
|
+
if (reported === undefined)
|
|
1346
|
+
return whereOf(item);
|
|
1347
|
+
return reported.line > 0 ? `${reported.spec}:${reported.line}` : reported.spec;
|
|
1348
|
+
}
|
|
1065
1349
|
/**
|
|
1066
1350
|
* Whether a pending request's interval covers a navigation's wait: it started while the
|
|
1067
1351
|
* navigation was loading (at or after the wait began) and it has no recorded response, so it was
|
|
@@ -1473,6 +1757,14 @@ function missingApiPrefixSentence(request) {
|
|
|
1473
1757
|
* (answered 201, the page still rendering signed-out) never appeared. With no action clock, the
|
|
1474
1758
|
* row falls back to {@link TraceArtifact.lead}: the page's last answered fetch/xhr.
|
|
1475
1759
|
*
|
|
1760
|
+
* The action, the URL and the requests are the FAILING ASSERTION'S PAGE's ({@link
|
|
1761
|
+
* TraceArtifact.lastAction}), never the archive's last context: a test that creates its data in a
|
|
1762
|
+
* second browser context writes that context's click and `/editor` snapshot into the same zip, and
|
|
1763
|
+
* reading the concatenation made them the failing page (xr-molecule-flash-m160). The other
|
|
1764
|
+
* contexts' requests are named separately as set-up work. When the trace holds more than one used
|
|
1765
|
+
* context and the assertion's page cannot be identified, the note lists the requests and makes NO
|
|
1766
|
+
* causal claim — no "the test's last action", no "the page stayed at …" — instead of merging them.
|
|
1767
|
+
*
|
|
1476
1768
|
* WHICH rows it spends is evidence, not artifact order (xr-molecule-flash-m112 +53:46): a row
|
|
1477
1769
|
* needs a recorded response, a real failure or a console line ({@link traceRowEvidence}), rows are
|
|
1478
1770
|
* deduplicated by the test's location, and they are ordered real failure → repeated from an
|
|
@@ -1527,6 +1819,7 @@ function missingApiPrefixSentence(request) {
|
|
|
1527
1819
|
* error.
|
|
1528
1820
|
*/
|
|
1529
1821
|
export function failureTraceNote(items, target, earlierFailures = [], apiPrefixes = [], report = {}) {
|
|
1822
|
+
const failures = report.failures ?? [];
|
|
1530
1823
|
const candidates = items.filter((item) => traceRowEvidence(item, report));
|
|
1531
1824
|
if (candidates.length === 0)
|
|
1532
1825
|
return null;
|
|
@@ -1539,7 +1832,7 @@ export function failureTraceNote(items, target, earlierFailures = [], apiPrefixe
|
|
|
1539
1832
|
const seen = new Set();
|
|
1540
1833
|
for (const { item, index } of ranked) {
|
|
1541
1834
|
// A trace that names no spec cannot be placed; it is never collapsed onto another one.
|
|
1542
|
-
const where = item.spec === null ? `#${index}` :
|
|
1835
|
+
const where = item.spec === null ? `#${index}` : failureWhereOf(item, failures);
|
|
1543
1836
|
if (seen.has(where))
|
|
1544
1837
|
continue;
|
|
1545
1838
|
seen.add(where);
|
|
@@ -1593,6 +1886,9 @@ export function failureTraceNote(items, target, earlierFailures = [], apiPrefixe
|
|
|
1593
1886
|
];
|
|
1594
1887
|
const anyStalled = stalls.some((stall) => stall !== null);
|
|
1595
1888
|
const rows = [];
|
|
1889
|
+
// Rows whose failing action sent nothing and left the page on a new route: the page is still
|
|
1890
|
+
// showing the previous route's data. Counted so the note can say it once for the whole run.
|
|
1891
|
+
let staleRouteRows = 0;
|
|
1596
1892
|
for (const [index, item] of traced.entries()) {
|
|
1597
1893
|
// The test's LAST action before the failing assertion leads the row (m148): the requests it
|
|
1598
1894
|
// caused and what came back explain a page that never showed the signed-in state, while the
|
|
@@ -1630,7 +1926,26 @@ export function failureTraceNote(items, target, earlierFailures = [], apiPrefixe
|
|
|
1630
1926
|
const parts = [];
|
|
1631
1927
|
if (step !== undefined && !stalledNav) {
|
|
1632
1928
|
const head = `the test's last action was ${step.api} at ${actionClock(item, step.start)}`;
|
|
1633
|
-
|
|
1929
|
+
const pageBefore = step.pageBefore;
|
|
1930
|
+
const pageAfter = step.pageAfter;
|
|
1931
|
+
// The action asked the API for nothing and the page ended up on another route: the page is
|
|
1932
|
+
// still showing the data it fetched for the route before, which is why the new route's list
|
|
1933
|
+
// is wrong (xr-molecule-flash-m163 +10:00 — the note printed "it sent no request" beside
|
|
1934
|
+
// "the page then navigated to …", then pointed at a response body that never existed, and
|
|
1935
|
+
// the run spent six minutes on markup and CSS).
|
|
1936
|
+
const staleRoute = stepRequests.length === 0 &&
|
|
1937
|
+
pageBefore !== undefined &&
|
|
1938
|
+
pageAfter !== undefined &&
|
|
1939
|
+
pageBefore !== pageAfter;
|
|
1940
|
+
if (staleRoute) {
|
|
1941
|
+
staleRouteRows += 1;
|
|
1942
|
+
parts.push(`${head}, and it sent no request — the page moved from ${pageBefore} to ${pageAfter}. ` +
|
|
1943
|
+
`The page changed route but fetched nothing new, so it is still showing the previous ` +
|
|
1944
|
+
`route's data: check how the page reads the route (a param the route does not define ` +
|
|
1945
|
+
`is always undefined; a missing effect dependency on the route means the fetch never ` +
|
|
1946
|
+
`re-runs), not the markup or the CSS`);
|
|
1947
|
+
}
|
|
1948
|
+
else if (stepRequests.length === 0) {
|
|
1634
1949
|
parts.push(`${head}, and it sent no request`);
|
|
1635
1950
|
}
|
|
1636
1951
|
else {
|
|
@@ -1642,10 +1957,10 @@ export function failureTraceNote(items, target, earlierFailures = [], apiPrefixe
|
|
|
1642
1957
|
}
|
|
1643
1958
|
// What the page did next: a navigation shows in the main frame's URL even when the app moved
|
|
1644
1959
|
// client-side and the network log recorded no document request.
|
|
1645
|
-
if (
|
|
1646
|
-
parts.push(
|
|
1647
|
-
? `the page then navigated to ${
|
|
1648
|
-
: `the page stayed at ${
|
|
1960
|
+
if (!staleRoute && pageAfter !== undefined) {
|
|
1961
|
+
parts.push(pageBefore !== undefined && pageBefore !== pageAfter
|
|
1962
|
+
? `the page then navigated to ${pageAfter}`
|
|
1963
|
+
: `the page stayed at ${pageAfter}`);
|
|
1649
1964
|
}
|
|
1650
1965
|
// A 4xx on the logout endpoint with the page unmoved is not a backend failure: the auth
|
|
1651
1966
|
// client swallows the call and clears the session, and the URL can only move if the page
|
|
@@ -1702,14 +2017,29 @@ export function failureTraceNote(items, target, earlierFailures = [], apiPrefixe
|
|
|
1702
2017
|
.slice(0, 2)
|
|
1703
2018
|
.map((text) => text.replace(/\s+/g, ' ').trim().slice(0, 160))
|
|
1704
2019
|
.join(' | ')}`);
|
|
1705
|
-
|
|
2020
|
+
// A request from another browser context the test opened is not the failing action's effect:
|
|
2021
|
+
// it is set-up work on a different page, and it is named as such rather than merged into the
|
|
2022
|
+
// row (xr-molecule-flash-m160: the helper context's POST /api/articles was printed as the
|
|
2023
|
+
// click's effect while the assertion had failed on the profile page).
|
|
2024
|
+
const others = (item.otherContexts ?? []).filter((context) => context.requests.length > 0);
|
|
2025
|
+
for (const [otherIndex, context] of others.slice(0, 2).entries()) {
|
|
2026
|
+
const label = otherIndex === 0
|
|
2027
|
+
? 'a second browser context, opened by the test for set-up and not the page the assertion ran on'
|
|
2028
|
+
: 'another browser context the test opened';
|
|
2029
|
+
parts.push(`${label}: ${context.requests.slice(0, 2).map(requestLine).join('; ')}`);
|
|
2030
|
+
}
|
|
2031
|
+
rows.push(` ${index + 1}. ${failureWhereOf(item, failures)} — ${parts.join(' — ')}`);
|
|
1706
2032
|
}
|
|
1707
2033
|
// Distinct locations the note did not name: the repeats collapse, so the count is what is left
|
|
1708
2034
|
// to look at, not how many zips Playwright wrote.
|
|
1709
|
-
const distinct = new Set(candidates.map((item, index) =>
|
|
2035
|
+
const distinct = new Set(candidates.map((item, index) => item.spec === null ? `#${index}` : failureWhereOf(item, failures)));
|
|
1710
2036
|
const more = distinct.size > traced.length
|
|
1711
2037
|
? `\n … and ${distinct.size - traced.length} more traced failures`
|
|
1712
2038
|
: '';
|
|
2039
|
+
// Every row came from a trace with more than one browser context and no page to tie the
|
|
2040
|
+
// assertion to: the note may list the requests, and may not say what action sent them or where
|
|
2041
|
+
// "the page" went (m160).
|
|
2042
|
+
const allUnattributed = traced.every((item) => item.unattributed === true);
|
|
1713
2043
|
// A run whose pages never loaded has one cause, and it is not in the app: every traced failure is
|
|
1714
2044
|
// a `page.goto` that timed out on a document the browser could not load. The transport error is
|
|
1715
2045
|
// the first line the executor needs — m118 (+37:09) read 14 goto timeouts as app failures while
|
|
@@ -1752,12 +2082,33 @@ export function failureTraceNote(items, target, earlierFailures = [], apiPrefixe
|
|
|
1752
2082
|
`and if the stall repeats measure that exact fetch ` +
|
|
1753
2083
|
`(\`curl -o /dev/null -w '%{time_starttransfer} %{time_total}' <url>\`) before ` +
|
|
1754
2084
|
`treating it as the app's.\n\n`);
|
|
2085
|
+
// Every row is a route change that fetched nothing: the cause and the next read are the same
|
|
2086
|
+
// for the whole run, and the generic "read the response body" paragraph would point at a
|
|
2087
|
+
// response that never existed (xr-molecule-flash-m163 +10:00).
|
|
2088
|
+
const everyStaleRoute = staleRouteRows > 0 &&
|
|
2089
|
+
staleRouteRows === traced.length &&
|
|
2090
|
+
!anyFailed &&
|
|
2091
|
+
!anyStalled &&
|
|
2092
|
+
!allUnattributed &&
|
|
2093
|
+
transport === null;
|
|
2094
|
+
const staleRouteLead = everyStaleRoute
|
|
2095
|
+
? `Every failure this run traced is the same shape: the failing action asked the API for ` +
|
|
2096
|
+
`nothing and the page changed route, so the page is still showing the data it fetched for ` +
|
|
2097
|
+
`the route before. Read how each page derives its query from the route — a param the route ` +
|
|
2098
|
+
`does not define is always undefined, and a missing effect dependency on the route means ` +
|
|
2099
|
+
`the fetch never re-runs — before touching the markup or the CSS.\n\n`
|
|
2100
|
+
: '';
|
|
1755
2101
|
return (transportLead +
|
|
1756
2102
|
stalledLead +
|
|
2103
|
+
staleRouteLead +
|
|
1757
2104
|
`The platform read the failing tests' own Playwright traces from the run against ${target} ` +
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
2105
|
+
(allUnattributed
|
|
2106
|
+
? `(what each page sent and got — the requests their contexts recorded, with no claim about ` +
|
|
2107
|
+
`which action caused them, then the ones that failed, the ones that got no response, and ` +
|
|
2108
|
+
`the last answered ones):\n`
|
|
2109
|
+
: `(what each page sent and got — first what the test's last action sent and where the page ` +
|
|
2110
|
+
`went next, then the requests that failed, the ones that got no response, and its last ` +
|
|
2111
|
+
`answered ones):\n`) +
|
|
1761
2112
|
`${rows.join('\n')}${more}\n` +
|
|
1762
2113
|
(anyFailed
|
|
1763
2114
|
? everyFailureLacksPrefix
|
|
@@ -1787,13 +2138,24 @@ export function failureTraceNote(items, target, earlierFailures = [], apiPrefixe
|
|
|
1787
2138
|
'not finish loading because its own subresource never answered. Do not read this as a ' +
|
|
1788
2139
|
'page-state bug: re-run these tests against the same URL first, and change the app only ' +
|
|
1789
2140
|
'if the same navigation times out again with no stalled asset in the trace.'
|
|
1790
|
-
:
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
2141
|
+
: allUnattributed
|
|
2142
|
+
? 'No listed request failed — none got a 4xx/5xx or a network error. The trace held more ' +
|
|
2143
|
+
'than one browser context and nothing ties a page to the failing assertion, so no ' +
|
|
2144
|
+
'action or URL is attributed here: read the requests above together with the runner’s ' +
|
|
2145
|
+
'own failure text to tell which page the test asserted on, and read that page’s state, ' +
|
|
2146
|
+
'not the selector.'
|
|
2147
|
+
: everyStaleRoute
|
|
2148
|
+
? 'No listed request failed — none got a 4xx/5xx or a network error — and the failing ' +
|
|
2149
|
+
'action sent no request at all, so there is no response to read. The page changed ' +
|
|
2150
|
+
'route and fetched nothing, which is why it still shows the previous route’s data: ' +
|
|
2151
|
+
'fix how the page reads the route, not the selector.'
|
|
2152
|
+
: 'No listed request failed — none got a 4xx/5xx or a network error — so the cause is in the ' +
|
|
2153
|
+
"page, not the API. Start with what the test's last action sent (the first line of each " +
|
|
2154
|
+
'row) and what the page did next: a request that answered (2xx included) while the page ' +
|
|
2155
|
+
'still did not show the state the assertion waited on is the fact to read — its status ' +
|
|
2156
|
+
'and response body, not the selector. If the request is not the one the test meant — an ' +
|
|
2157
|
+
'unfollow sent where the test clicked follow — the state the action read was stale ' +
|
|
2158
|
+
'before it.') +
|
|
1797
2159
|
prefixAdvice +
|
|
1798
2160
|
(wrapped
|
|
1799
2161
|
? ' The API received a body wrapped in `data`: app-http’s `post(url, data, config)` / ' +
|