@specific.dev/spectest 0.50.0 → 0.51.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser.d.ts +1 -0
- package/dist/index.js +11 -2
- package/dist/locator.d.ts +10 -2
- package/dist/locator.js +63 -4
- package/dist/recorder.d.ts +9 -0
- package/package.json +1 -1
- package/src/browser.ts +1 -0
- package/src/index.ts +12 -2
- package/src/locator.ts +84 -8
- package/src/recorder.ts +9 -0
package/dist/browser.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -524,6 +524,15 @@ function buildLocatorMatchers(loc, negated, message) {
|
|
|
524
524
|
const started = Date.now();
|
|
525
525
|
const deadline = started + (timeout ?? DEFAULT_ACTION_TIMEOUT_MS);
|
|
526
526
|
let actual;
|
|
527
|
+
// Whether this matcher's subject is meant to be on screen, i.e. whether
|
|
528
|
+
// the step should carry a reveal target for the replay (see
|
|
529
|
+
// `LocatorProbe.settle`). Only the visibility matchers flip with `negated`
|
|
530
|
+
// — `not.toHaveText(...)` is still an element the reader should be looking
|
|
531
|
+
// at — and `toHaveCount(0)` is the third way of saying "expect nothing".
|
|
532
|
+
const visibility = matcher === "toBeVisible" || matcher === "toBeHidden";
|
|
533
|
+
const expectsGone = (visibility && (matcher === "toBeHidden") !== negated) ||
|
|
534
|
+
(matcher === "toHaveCount" && expected === 0);
|
|
535
|
+
const settleOpts = { reveal: !expectsGone };
|
|
527
536
|
for (;;) {
|
|
528
537
|
let satisfied;
|
|
529
538
|
try {
|
|
@@ -541,7 +550,7 @@ function buildLocatorMatchers(loc, negated, message) {
|
|
|
541
550
|
}
|
|
542
551
|
const passed = satisfied !== negated;
|
|
543
552
|
if (passed) {
|
|
544
|
-
const sourceSeq = await probe.settle(matcher, Date.now() - started);
|
|
553
|
+
const sourceSeq = await probe.settle(matcher, Date.now() - started, undefined, settleOpts);
|
|
545
554
|
recordAssertion({
|
|
546
555
|
matcher,
|
|
547
556
|
negated,
|
|
@@ -555,7 +564,7 @@ function buildLocatorMatchers(loc, negated, message) {
|
|
|
555
564
|
}
|
|
556
565
|
if (Date.now() >= deadline) {
|
|
557
566
|
const msg = describe(actual);
|
|
558
|
-
const sourceSeq = await probe.settle(matcher, Date.now() - started, msg);
|
|
567
|
+
const sourceSeq = await probe.settle(matcher, Date.now() - started, msg, settleOpts);
|
|
559
568
|
recordAssertion({
|
|
560
569
|
matcher,
|
|
561
570
|
negated,
|
package/dist/locator.d.ts
CHANGED
|
@@ -151,8 +151,16 @@ export interface LocatorProbe {
|
|
|
151
151
|
* locator's `expect(...)` matcher assertion nests under, and return its seq
|
|
152
152
|
* (provenance + replay seek). `action` is the matcher name, `waitedMs` the
|
|
153
153
|
* poll time, `error` marks the step failed on a timed-out matcher. Returns
|
|
154
|
-
* `undefined` when nothing is recording.
|
|
155
|
-
|
|
154
|
+
* `undefined` when nothing is recording.
|
|
155
|
+
*
|
|
156
|
+
* `opts.reveal` additionally stamps the element's rrweb node id for the
|
|
157
|
+
* replay viewer (see {@link stampRevealTarget}). The caller decides,
|
|
158
|
+
* because only it knows whether the matcher expects the element on screen:
|
|
159
|
+
* probing for one a passing `toBeHidden()` says is gone would do nothing
|
|
160
|
+
* but wait out the probe's own timeout. */
|
|
161
|
+
settle(action: string, waitedMs: number, error?: string, opts?: {
|
|
162
|
+
reveal?: boolean;
|
|
163
|
+
}): Promise<number | undefined>;
|
|
156
164
|
}
|
|
157
165
|
/** Silent (non-recorded) reads an `expect(browser)` matcher polls — the
|
|
158
166
|
* session twin of {@link LocatorProbe}. Lives here, next to it, so `index.ts`
|
package/dist/locator.js
CHANGED
|
@@ -244,9 +244,22 @@ async function stampActionPoint(loc, rec, position) {
|
|
|
244
244
|
const pt = await loc.evaluate((el, pos) => {
|
|
245
245
|
if (window.top !== window)
|
|
246
246
|
return null;
|
|
247
|
+
// rrweb's id for this element, in the same one round trip. It is what
|
|
248
|
+
// the replay actually needs: a coordinate is measured against the VM's
|
|
249
|
+
// layout a moment BEFORE playwright's own actionability wait, so an
|
|
250
|
+
// element still animating in (or one the dashboard's fonts lay out a
|
|
251
|
+
// little differently) leaves the recorded point beside the element
|
|
252
|
+
// rather than on it — measured 27px low on a modal with an entrance
|
|
253
|
+
// animation. The id lets the viewer place the cursor from the replay's
|
|
254
|
+
// own layout instead. Stamped even when the point below is not, since
|
|
255
|
+
// the viewer can scroll an off-screen element into view.
|
|
256
|
+
const w = window;
|
|
257
|
+
const getId = w.__spectestRec?.mirror?.getId;
|
|
258
|
+
const rawId = typeof getId === "function" ? getId.call(w.__spectestRec.mirror, el) : 0;
|
|
259
|
+
const nodeId = typeof rawId === "number" && rawId > 0 ? rawId : 0;
|
|
247
260
|
const r = el.getBoundingClientRect();
|
|
248
261
|
if (!r.width || !r.height)
|
|
249
|
-
return null;
|
|
262
|
+
return nodeId ? { nodeId } : null;
|
|
250
263
|
// Playwright clicks the element's centre unless the caller named a
|
|
251
264
|
// point — which it takes relative to the PADDING box, so an element
|
|
252
265
|
// with a border (a plain `<button>` has 2px of it) sits that far off
|
|
@@ -255,17 +268,57 @@ async function stampActionPoint(loc, rec, position) {
|
|
|
255
268
|
const x = cs ? r.left + parseFloat(cs.borderLeftWidth) + pos.x : r.left + r.width / 2;
|
|
256
269
|
const y = cs ? r.top + parseFloat(cs.borderTopWidth) + pos.y : r.top + r.height / 2;
|
|
257
270
|
const onScreen = x >= 0 && y >= 0 && x <= window.innerWidth && y <= window.innerHeight;
|
|
258
|
-
return onScreen ? { x, y } :
|
|
271
|
+
return onScreen ? { x, y, nodeId } : { nodeId };
|
|
259
272
|
}, position, { timeout: POINT_PROBE_MS });
|
|
260
|
-
if (pt) {
|
|
273
|
+
if (pt && pt.x !== undefined) {
|
|
261
274
|
rec.x = Math.round(pt.x);
|
|
262
275
|
rec.y = Math.round(pt.y);
|
|
263
276
|
}
|
|
277
|
+
if (pt && pt.nodeId)
|
|
278
|
+
rec.targetNodeId = pt.nodeId;
|
|
264
279
|
}
|
|
265
280
|
catch {
|
|
266
281
|
/* Element not ready / gone / strict violation — the action reports it. */
|
|
267
282
|
}
|
|
268
283
|
}
|
|
284
|
+
/** Stamp the rrweb node id of the element a settled `expect(locator)` step is
|
|
285
|
+
* about, so the dashboard can bring it into the replay's view.
|
|
286
|
+
*
|
|
287
|
+
* Playwright's idea of "visible" is a non-empty box that isn't hidden — it
|
|
288
|
+
* says nothing about the viewport, so an element below the fold passes
|
|
289
|
+
* `toBeVisible()`. The replay then shows the recorded viewport, which is a
|
|
290
|
+
* frame the asserted element isn't in: the reader sees a page that looks
|
|
291
|
+
* unrelated to the step. The id is the element's identity in the recording
|
|
292
|
+
* (rrweb's own mirror, the same id space its mutation events carry), so the
|
|
293
|
+
* viewer can find the node in the replayed DOM and scroll it into view —
|
|
294
|
+
* measured against the replay's real layout rather than a rect we recorded
|
|
295
|
+
* here, and correct for an element inside a scrollable container too.
|
|
296
|
+
*
|
|
297
|
+
* Best-effort like {@link stampActionPoint}, and only for the top-level
|
|
298
|
+
* document: a frame's recorder has its own mirror, whose ids mean nothing in
|
|
299
|
+
* the main frame's stream. An unserialized node (`getId` → -1) or no recorder
|
|
300
|
+
* leaves the step unstamped, which just means the viewer doesn't scroll. */
|
|
301
|
+
async function stampRevealTarget(loc, rec) {
|
|
302
|
+
try {
|
|
303
|
+
const id = await loc.evaluate((el) => {
|
|
304
|
+
if (window.top !== window)
|
|
305
|
+
return 0;
|
|
306
|
+
// The bootstrap stashes rrweb's `record` here; `mirror` is its
|
|
307
|
+
// node ↔ id map, shared by every snapshot it takes (see browser.ts).
|
|
308
|
+
const w = window;
|
|
309
|
+
const getId = w.__spectestRec?.mirror?.getId;
|
|
310
|
+
if (typeof getId !== "function")
|
|
311
|
+
return 0;
|
|
312
|
+
const id = getId.call(w.__spectestRec.mirror, el);
|
|
313
|
+
return typeof id === "number" && id > 0 ? id : 0;
|
|
314
|
+
}, undefined, { timeout: POINT_PROBE_MS });
|
|
315
|
+
if (id)
|
|
316
|
+
rec.targetNodeId = id;
|
|
317
|
+
}
|
|
318
|
+
catch {
|
|
319
|
+
/* Element gone / not attached / strict violation — no reveal target. */
|
|
320
|
+
}
|
|
321
|
+
}
|
|
269
322
|
/** Fold a {@link InputFiles} argument into the one playwright takes: repo
|
|
270
323
|
* paths resolved to their in-VM location (see project-files.ts), built files
|
|
271
324
|
* given a default mime type and a real `Buffer`. The names come back too —
|
|
@@ -320,7 +373,13 @@ export function makeLocator(backend, strategy, chain) {
|
|
|
320
373
|
count: () => backend.silentRead((page) => lower(page, chain).count()),
|
|
321
374
|
isEnabled: (timeout) => backend.silentRead((page) => lower(page, chain).isEnabled({ timeout })),
|
|
322
375
|
isChecked: (timeout) => backend.silentRead((page) => lower(page, chain).isChecked({ timeout })),
|
|
323
|
-
settle: (action, waitedMs, error) =>
|
|
376
|
+
settle: async (action, waitedMs, error, opts) => {
|
|
377
|
+
const fields = { selector: label };
|
|
378
|
+
if (opts && opts.reveal) {
|
|
379
|
+
await backend.silentRead((page) => stampRevealTarget(lower(page, chain), fields));
|
|
380
|
+
}
|
|
381
|
+
return backend.recordSettled(action, fields, waitedMs, error);
|
|
382
|
+
},
|
|
324
383
|
};
|
|
325
384
|
const loc = {
|
|
326
385
|
[LOCATOR_BRAND]: true,
|
package/dist/recorder.d.ts
CHANGED
|
@@ -418,6 +418,15 @@ export interface BrowserEvent extends BaseEvent {
|
|
|
418
418
|
dy?: number;
|
|
419
419
|
x?: number;
|
|
420
420
|
y?: number;
|
|
421
|
+
/**
|
|
422
|
+
* rrweb node id of the element a settled `expect(locator)` step asserted on
|
|
423
|
+
* — the element's identity in this session's recording. Playwright calls an
|
|
424
|
+
* element below the fold visible, so the frame the step seeks to need not
|
|
425
|
+
* contain it; with this the viewer finds the node in the replayed DOM and
|
|
426
|
+
* scrolls it into view. Absent when the SDK couldn't measure it (an element
|
|
427
|
+
* in a frame, one rrweb hasn't serialized, a matcher whose subject is gone).
|
|
428
|
+
*/
|
|
429
|
+
targetNodeId?: number;
|
|
421
430
|
/** Screenshot image format. */
|
|
422
431
|
format?: string;
|
|
423
432
|
/** For `waitFor`: how many times the predicate was polled. */
|
package/package.json
CHANGED
package/src/browser.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -2328,6 +2328,16 @@ function buildLocatorMatchers(
|
|
|
2328
2328
|
const started = Date.now();
|
|
2329
2329
|
const deadline = started + (timeout ?? DEFAULT_ACTION_TIMEOUT_MS);
|
|
2330
2330
|
let actual: unknown;
|
|
2331
|
+
// Whether this matcher's subject is meant to be on screen, i.e. whether
|
|
2332
|
+
// the step should carry a reveal target for the replay (see
|
|
2333
|
+
// `LocatorProbe.settle`). Only the visibility matchers flip with `negated`
|
|
2334
|
+
// — `not.toHaveText(...)` is still an element the reader should be looking
|
|
2335
|
+
// at — and `toHaveCount(0)` is the third way of saying "expect nothing".
|
|
2336
|
+
const visibility = matcher === "toBeVisible" || matcher === "toBeHidden";
|
|
2337
|
+
const expectsGone =
|
|
2338
|
+
(visibility && (matcher === "toBeHidden") !== negated) ||
|
|
2339
|
+
(matcher === "toHaveCount" && expected === 0);
|
|
2340
|
+
const settleOpts = { reveal: !expectsGone };
|
|
2331
2341
|
for (;;) {
|
|
2332
2342
|
let satisfied: boolean;
|
|
2333
2343
|
try {
|
|
@@ -2344,7 +2354,7 @@ function buildLocatorMatchers(
|
|
|
2344
2354
|
}
|
|
2345
2355
|
const passed = satisfied !== negated;
|
|
2346
2356
|
if (passed) {
|
|
2347
|
-
const sourceSeq = await probe.settle(matcher, Date.now() - started);
|
|
2357
|
+
const sourceSeq = await probe.settle(matcher, Date.now() - started, undefined, settleOpts);
|
|
2348
2358
|
recordAssertion({
|
|
2349
2359
|
matcher,
|
|
2350
2360
|
negated,
|
|
@@ -2358,7 +2368,7 @@ function buildLocatorMatchers(
|
|
|
2358
2368
|
}
|
|
2359
2369
|
if (Date.now() >= deadline) {
|
|
2360
2370
|
const msg = describe(actual);
|
|
2361
|
-
const sourceSeq = await probe.settle(matcher, Date.now() - started, msg);
|
|
2371
|
+
const sourceSeq = await probe.settle(matcher, Date.now() - started, msg, settleOpts);
|
|
2362
2372
|
recordAssertion({
|
|
2363
2373
|
matcher,
|
|
2364
2374
|
negated,
|
package/src/locator.ts
CHANGED
|
@@ -322,8 +322,19 @@ export interface LocatorProbe {
|
|
|
322
322
|
* locator's `expect(...)` matcher assertion nests under, and return its seq
|
|
323
323
|
* (provenance + replay seek). `action` is the matcher name, `waitedMs` the
|
|
324
324
|
* poll time, `error` marks the step failed on a timed-out matcher. Returns
|
|
325
|
-
* `undefined` when nothing is recording.
|
|
326
|
-
|
|
325
|
+
* `undefined` when nothing is recording.
|
|
326
|
+
*
|
|
327
|
+
* `opts.reveal` additionally stamps the element's rrweb node id for the
|
|
328
|
+
* replay viewer (see {@link stampRevealTarget}). The caller decides,
|
|
329
|
+
* because only it knows whether the matcher expects the element on screen:
|
|
330
|
+
* probing for one a passing `toBeHidden()` says is gone would do nothing
|
|
331
|
+
* but wait out the probe's own timeout. */
|
|
332
|
+
settle(
|
|
333
|
+
action: string,
|
|
334
|
+
waitedMs: number,
|
|
335
|
+
error?: string,
|
|
336
|
+
opts?: { reveal?: boolean },
|
|
337
|
+
): Promise<number | undefined>;
|
|
327
338
|
}
|
|
328
339
|
|
|
329
340
|
/** Silent (non-recorded) reads an `expect(browser)` matcher polls — the
|
|
@@ -446,8 +457,23 @@ async function stampActionPoint(
|
|
|
446
457
|
const pt = await loc.evaluate(
|
|
447
458
|
(el, pos) => {
|
|
448
459
|
if (window.top !== window) return null;
|
|
460
|
+
// rrweb's id for this element, in the same one round trip. It is what
|
|
461
|
+
// the replay actually needs: a coordinate is measured against the VM's
|
|
462
|
+
// layout a moment BEFORE playwright's own actionability wait, so an
|
|
463
|
+
// element still animating in (or one the dashboard's fonts lay out a
|
|
464
|
+
// little differently) leaves the recorded point beside the element
|
|
465
|
+
// rather than on it — measured 27px low on a modal with an entrance
|
|
466
|
+
// animation. The id lets the viewer place the cursor from the replay's
|
|
467
|
+
// own layout instead. Stamped even when the point below is not, since
|
|
468
|
+
// the viewer can scroll an off-screen element into view.
|
|
469
|
+
const w = window as unknown as {
|
|
470
|
+
__spectestRec?: { mirror?: { getId?: (n: Node) => number } };
|
|
471
|
+
};
|
|
472
|
+
const getId = w.__spectestRec?.mirror?.getId;
|
|
473
|
+
const rawId = typeof getId === "function" ? getId.call(w.__spectestRec!.mirror, el) : 0;
|
|
474
|
+
const nodeId = typeof rawId === "number" && rawId > 0 ? rawId : 0;
|
|
449
475
|
const r = el.getBoundingClientRect();
|
|
450
|
-
if (!r.width || !r.height) return null;
|
|
476
|
+
if (!r.width || !r.height) return nodeId ? { nodeId } : null;
|
|
451
477
|
// Playwright clicks the element's centre unless the caller named a
|
|
452
478
|
// point — which it takes relative to the PADDING box, so an element
|
|
453
479
|
// with a border (a plain `<button>` has 2px of it) sits that far off
|
|
@@ -457,20 +483,65 @@ async function stampActionPoint(
|
|
|
457
483
|
const y = cs ? r.top + parseFloat(cs.borderTopWidth) + pos!.y : r.top + r.height / 2;
|
|
458
484
|
const onScreen =
|
|
459
485
|
x >= 0 && y >= 0 && x <= window.innerWidth && y <= window.innerHeight;
|
|
460
|
-
return onScreen ? { x, y } :
|
|
486
|
+
return onScreen ? { x, y, nodeId } : { nodeId };
|
|
461
487
|
},
|
|
462
488
|
position,
|
|
463
489
|
{ timeout: POINT_PROBE_MS },
|
|
464
490
|
);
|
|
465
|
-
if (pt) {
|
|
491
|
+
if (pt && pt.x !== undefined) {
|
|
466
492
|
rec.x = Math.round(pt.x);
|
|
467
|
-
rec.y = Math.round(pt.y);
|
|
493
|
+
rec.y = Math.round(pt.y!);
|
|
468
494
|
}
|
|
495
|
+
if (pt && pt.nodeId) rec.targetNodeId = pt.nodeId;
|
|
469
496
|
} catch {
|
|
470
497
|
/* Element not ready / gone / strict violation — the action reports it. */
|
|
471
498
|
}
|
|
472
499
|
}
|
|
473
500
|
|
|
501
|
+
/** Stamp the rrweb node id of the element a settled `expect(locator)` step is
|
|
502
|
+
* about, so the dashboard can bring it into the replay's view.
|
|
503
|
+
*
|
|
504
|
+
* Playwright's idea of "visible" is a non-empty box that isn't hidden — it
|
|
505
|
+
* says nothing about the viewport, so an element below the fold passes
|
|
506
|
+
* `toBeVisible()`. The replay then shows the recorded viewport, which is a
|
|
507
|
+
* frame the asserted element isn't in: the reader sees a page that looks
|
|
508
|
+
* unrelated to the step. The id is the element's identity in the recording
|
|
509
|
+
* (rrweb's own mirror, the same id space its mutation events carry), so the
|
|
510
|
+
* viewer can find the node in the replayed DOM and scroll it into view —
|
|
511
|
+
* measured against the replay's real layout rather than a rect we recorded
|
|
512
|
+
* here, and correct for an element inside a scrollable container too.
|
|
513
|
+
*
|
|
514
|
+
* Best-effort like {@link stampActionPoint}, and only for the top-level
|
|
515
|
+
* document: a frame's recorder has its own mirror, whose ids mean nothing in
|
|
516
|
+
* the main frame's stream. An unserialized node (`getId` → -1) or no recorder
|
|
517
|
+
* leaves the step unstamped, which just means the viewer doesn't scroll. */
|
|
518
|
+
async function stampRevealTarget(
|
|
519
|
+
loc: PWLocator,
|
|
520
|
+
rec: Partial<RecordableFields>,
|
|
521
|
+
): Promise<void> {
|
|
522
|
+
try {
|
|
523
|
+
const id = await loc.evaluate(
|
|
524
|
+
(el) => {
|
|
525
|
+
if (window.top !== window) return 0;
|
|
526
|
+
// The bootstrap stashes rrweb's `record` here; `mirror` is its
|
|
527
|
+
// node ↔ id map, shared by every snapshot it takes (see browser.ts).
|
|
528
|
+
const w = window as unknown as {
|
|
529
|
+
__spectestRec?: { mirror?: { getId?: (n: Node) => number } };
|
|
530
|
+
};
|
|
531
|
+
const getId = w.__spectestRec?.mirror?.getId;
|
|
532
|
+
if (typeof getId !== "function") return 0;
|
|
533
|
+
const id = getId.call(w.__spectestRec!.mirror, el);
|
|
534
|
+
return typeof id === "number" && id > 0 ? id : 0;
|
|
535
|
+
},
|
|
536
|
+
undefined,
|
|
537
|
+
{ timeout: POINT_PROBE_MS },
|
|
538
|
+
);
|
|
539
|
+
if (id) rec.targetNodeId = id;
|
|
540
|
+
} catch {
|
|
541
|
+
/* Element gone / not attached / strict violation — no reveal target. */
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
474
545
|
/** Fold a {@link InputFiles} argument into the one playwright takes: repo
|
|
475
546
|
* paths resolved to their in-VM location (see project-files.ts), built files
|
|
476
547
|
* given a default mime type and a real `Buffer`. The names come back too —
|
|
@@ -671,8 +742,13 @@ export function makeLocator(
|
|
|
671
742
|
count: () => backend.silentRead((page) => lower(page, chain).count()),
|
|
672
743
|
isEnabled: (timeout) => backend.silentRead((page) => lower(page, chain).isEnabled({ timeout })),
|
|
673
744
|
isChecked: (timeout) => backend.silentRead((page) => lower(page, chain).isChecked({ timeout })),
|
|
674
|
-
settle: (action, waitedMs, error) =>
|
|
675
|
-
|
|
745
|
+
settle: async (action, waitedMs, error, opts) => {
|
|
746
|
+
const fields: Partial<RecordableFields> = { selector: label };
|
|
747
|
+
if (opts && opts.reveal) {
|
|
748
|
+
await backend.silentRead((page) => stampRevealTarget(lower(page, chain), fields));
|
|
749
|
+
}
|
|
750
|
+
return backend.recordSettled(action, fields, waitedMs, error);
|
|
751
|
+
},
|
|
676
752
|
};
|
|
677
753
|
|
|
678
754
|
const loc: InternalLocator = {
|
package/src/recorder.ts
CHANGED
|
@@ -473,6 +473,15 @@ export interface BrowserEvent extends BaseEvent {
|
|
|
473
473
|
dy?: number;
|
|
474
474
|
x?: number;
|
|
475
475
|
y?: number;
|
|
476
|
+
/**
|
|
477
|
+
* rrweb node id of the element a settled `expect(locator)` step asserted on
|
|
478
|
+
* — the element's identity in this session's recording. Playwright calls an
|
|
479
|
+
* element below the fold visible, so the frame the step seeks to need not
|
|
480
|
+
* contain it; with this the viewer finds the node in the replayed DOM and
|
|
481
|
+
* scrolls it into view. Absent when the SDK couldn't measure it (an element
|
|
482
|
+
* in a frame, one rrweb hasn't serialized, a matcher whose subject is gone).
|
|
483
|
+
*/
|
|
484
|
+
targetNodeId?: number;
|
|
476
485
|
/** Screenshot image format. */
|
|
477
486
|
format?: string;
|
|
478
487
|
/** For `waitFor`: how many times the predicate was polled. */
|