@e2e-dev/web 0.15.5-nightly-20261105030216 → 0.15.5
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 +1 -1
- package/dist/.build.json +1 -1
- package/dist/actions.d.ts +1 -1
- package/dist/actions.js +17 -6
- package/dist/browser-connection.d.ts +2 -3
- package/dist/browser-connection.js +18 -5
- package/dist/browser.js +35 -25
- package/dist/element-list.d.ts +28 -0
- package/dist/element-list.js +73 -0
- package/dist/evaluation.d.ts +20 -12
- package/dist/evaluation.js +107 -10
- package/dist/in-page/read-semantics.js +3 -0
- package/dist/install.d.ts +2 -2
- package/dist/install.js +5 -5
- package/dist/observation-capture.js +3 -3
- package/dist/observation.d.ts +12 -8
- package/dist/observation.js +37 -44
- package/dist/read-node.d.ts +5 -0
- package/dist/refs.d.ts +17 -8
- package/dist/refs.js +20 -10
- package/dist/support.d.ts +16 -3
- package/dist/support.js +31 -0
- package/dist/surface.d.ts +21 -4
- package/dist/surface.js +195 -97
- package/package.json +3 -3
package/dist/surface.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { mkdirSync } from 'node:fs';
|
|
10
10
|
import path from 'node:path';
|
|
11
|
-
import { EngineError, obj, raceAbort, TestError, withinCleanupBudget, } from 'e2e/engine';
|
|
11
|
+
import { Deadline, EngineError, obj, raceAbort, TestError, withinCleanupBudget, } from 'e2e/engine';
|
|
12
12
|
import { matchesText } from 'e2e/engine';
|
|
13
13
|
import { classifyActionError, classifyInputError, dispatchLocatorAction, dispatchPointerAction } from './actions.js';
|
|
14
14
|
import { BrowserConnection, connectCdp } from './browser-connection.js';
|
|
@@ -19,7 +19,7 @@ import { TraceFeed } from './trace-feed.js';
|
|
|
19
19
|
import { DialogRouter } from './dialogs.js';
|
|
20
20
|
import { browserCheck, ensureBrowsersInstalled } from './install.js';
|
|
21
21
|
import { applyPostSteps, frameSelectors, projectExpression } from './locators.js';
|
|
22
|
-
import { contentBox, frameContentInset,
|
|
22
|
+
import { contentBox, frameContentInset, placeBoxInFrame, ROOT_NODE_ID, toSemanticNode } from './observation.js';
|
|
23
23
|
import { captureObservation } from './observation-capture.js';
|
|
24
24
|
import { maskOptions, secureFieldMasks } from './observe.js';
|
|
25
25
|
import { SHOWN_SELECTOR } from './shown-selector.js';
|
|
@@ -31,7 +31,7 @@ import { SECURE_FIELD_SELECTOR } from './read-node.js';
|
|
|
31
31
|
import { readSelector, takeReadsFunction } from './read-selector.js';
|
|
32
32
|
import { httpCredentials, installSiteHeaders, lowercaseNames, siteHeadersFor } from './protected-app.js';
|
|
33
33
|
import { RefRegistry } from './refs.js';
|
|
34
|
-
import { cancelled, DEFAULT_VIEWPORT, ErrorLatch, invalidState, message, navigationStaleOr, performViewportSwipe, sanitizeFilename, staleOr, translatePwError, } from './support.js';
|
|
34
|
+
import { cancelled, DEFAULT_VIEWPORT, ErrorLatch, invalidState, isClassified, isPwTimeout, message, navigationStaleOr, performViewportSwipe, sanitizeFilename, pageScriptFailure, staleOr, translatePwError, } from './support.js';
|
|
35
35
|
/** Browser launch budget when the harness init budget is not otherwise expressed. */
|
|
36
36
|
const BROWSER_LAUNCH_TIMEOUT_MS = 60_000;
|
|
37
37
|
const STATE_FORMAT = 'playwright-storage-state';
|
|
@@ -80,6 +80,8 @@ export class PlaywrightSurface {
|
|
|
80
80
|
artifactsDir = '';
|
|
81
81
|
/** Navigations the engine has in flight, which the app log leaves to the step that started them. */
|
|
82
82
|
ownNavigations = 0;
|
|
83
|
+
/** One entry per navigation in flight, set to the URL of a document request a test route aborted during it. */
|
|
84
|
+
navigationAborts = new Set();
|
|
83
85
|
/** What the attempt tells its trace beside the app log; absent between attempts. */
|
|
84
86
|
trace;
|
|
85
87
|
artifactCounter = 0;
|
|
@@ -132,7 +134,7 @@ export class PlaywrightSurface {
|
|
|
132
134
|
return [...scripts, { id: 'browser', status: 'skip', message: `browsers come from the "${this.leases.name}" provider` }];
|
|
133
135
|
if (this.connect !== undefined)
|
|
134
136
|
return [...scripts, { id: 'browser', status: 'skip', message: 'the browser is attached over CDP' }];
|
|
135
|
-
return [...scripts, ...browserCheck(this.browserName, info.env, info.headed)];
|
|
137
|
+
return [...scripts, ...(await browserCheck(this.browserName, info.env, info.headed))];
|
|
136
138
|
}
|
|
137
139
|
/** Releases the browsers `prepare` leased; a local launch or a `connect` has nothing to release. */
|
|
138
140
|
async finish(info) {
|
|
@@ -463,6 +465,9 @@ export class PlaywrightSurface {
|
|
|
463
465
|
: await this.session.run(operation, label, fn, bound);
|
|
464
466
|
}
|
|
465
467
|
catch (cause) {
|
|
468
|
+
// A handler error latched while the operation ran is its cause: a route
|
|
469
|
+
// that refused its own arguments aborts the request the step waited on.
|
|
470
|
+
this.latch.throwPending();
|
|
466
471
|
throw translate(cause, label);
|
|
467
472
|
}
|
|
468
473
|
}
|
|
@@ -483,18 +488,44 @@ export class PlaywrightSurface {
|
|
|
483
488
|
resize(size, operation) {
|
|
484
489
|
return this.acted(operation, this.guard(operation, 'resize', () => this.setViewport(size)));
|
|
485
490
|
}
|
|
486
|
-
/**
|
|
491
|
+
/**
|
|
492
|
+
* Runs one navigation the engine itself makes, so the app log leaves it to
|
|
493
|
+
* the step that asked for it. A navigation that fails after a test route
|
|
494
|
+
* aborted its document request failed because the test aborted it: an
|
|
495
|
+
* action failure, not an engine fault.
|
|
496
|
+
*/
|
|
487
497
|
async navigating(work) {
|
|
488
498
|
this.ownNavigations += 1;
|
|
499
|
+
const watch = {};
|
|
500
|
+
this.navigationAborts.add(watch);
|
|
489
501
|
try {
|
|
490
502
|
const result = await work();
|
|
491
503
|
this.trace?.tellEnvironment();
|
|
492
504
|
return result;
|
|
493
505
|
}
|
|
506
|
+
catch (cause) {
|
|
507
|
+
if (watch.url === undefined || isClassified(cause) || isPwTimeout(cause))
|
|
508
|
+
throw cause;
|
|
509
|
+
throw new TestError('ACTION_FAILED', `navigation failed: a route aborted the request for ${watch.url}: ${message(cause)}`, { cause });
|
|
510
|
+
}
|
|
494
511
|
finally {
|
|
512
|
+
this.navigationAborts.delete(watch);
|
|
495
513
|
this.ownNavigations -= 1;
|
|
496
514
|
}
|
|
497
515
|
}
|
|
516
|
+
/**
|
|
517
|
+
* Records a request a test route is about to abort, when it loads the
|
|
518
|
+
* document of the attempt's page. Never throws: the abort must follow.
|
|
519
|
+
*/
|
|
520
|
+
noteRouteAbort(request) {
|
|
521
|
+
if (this.navigationAborts.size === 0 || !request.isNavigationRequest())
|
|
522
|
+
return;
|
|
523
|
+
const main = this.openPage()?.mainFrame();
|
|
524
|
+
if (main === undefined || frameOf(request) !== main)
|
|
525
|
+
return;
|
|
526
|
+
for (const watch of this.navigationAborts)
|
|
527
|
+
watch.url ??= request.url();
|
|
528
|
+
}
|
|
498
529
|
/** Resolves with what an action resolved with once the trace has the screen it left (`TraceFeed.screenAfterAction`); an action that failed leaves none. */
|
|
499
530
|
async acted(operation, work) {
|
|
500
531
|
const result = await work;
|
|
@@ -525,88 +556,104 @@ export class PlaywrightSurface {
|
|
|
525
556
|
*/
|
|
526
557
|
locate(expression, operation) {
|
|
527
558
|
return this.guard(operation, 'locate', async (currentOperation) => {
|
|
528
|
-
const session = this.requireSession();
|
|
529
|
-
const token = session.token();
|
|
530
|
-
const refs = session.refs;
|
|
531
|
-
const page = this.requirePage();
|
|
532
|
-
const frame = await this.frameOf(expression);
|
|
533
|
-
const projected = projectExpression(page, expression, { testIdAttribute: this.testIdAttribute, frameHidden: frame.hidden });
|
|
534
|
-
const { displayValue, name, steps } = projected;
|
|
535
|
-
// Every match is read by the `e2e-read` selector engine in the task that finds it, so a
|
|
536
|
-
// page that replaces it a frame later cannot detach it before the read. A
|
|
537
|
-
// predicate-filtered match is one element among many candidates (every input with a
|
|
538
|
-
// value, every control the label engine kept). Re-resolving it by position at action
|
|
539
|
-
// time would act on a neighbor whenever the page inserted or removed an element in
|
|
540
|
-
// between, so such matches are pinned to the handles of the elements the query returned,
|
|
541
|
-
// and their reads are taken back from those very handles: what was read and what is
|
|
542
|
-
// acted on are one set of elements by construction.
|
|
543
|
-
const read = readSelector({ testIdAttribute: this.testIdAttribute, secureFieldSelector: SECURE_FIELD_SELECTOR });
|
|
544
|
-
const reading = projected.locator.locator(read.selector);
|
|
545
|
-
const handles = displayValue !== null || name !== null
|
|
546
|
-
? (await reading.elementHandles())
|
|
547
|
-
: null;
|
|
548
|
-
/** Disposes every handle this locate took, on the paths that hand none of them out. */
|
|
549
|
-
const releaseHandles = () => {
|
|
550
|
-
for (const handle of handles ?? [])
|
|
551
|
-
void handle.dispose().catch(() => undefined);
|
|
552
|
-
};
|
|
553
|
-
const first = handles?.[0];
|
|
554
|
-
const taken = handles === null
|
|
555
|
-
? await reading.evaluateAll(takeReadsFunction, { token: read.token })
|
|
556
|
-
: first === undefined
|
|
557
|
-
? []
|
|
558
|
-
: await first.evaluate(takeReadsFunction, { token: read.token, elements: handles });
|
|
559
|
-
const reads = taken
|
|
560
|
-
.filter((raw) => raw !== null)
|
|
561
|
-
.map((raw) => (frame.hidden ? { ...raw, states: { ...raw.states, hidden: true } } : raw));
|
|
562
|
-
if (reads.length !== taken.length) {
|
|
563
|
-
releaseHandles();
|
|
564
|
-
throw new TestError('INVALID_LOCATOR', 'a selector that captures with * (*css=article >> text=Hello) is not supported: use filter({ has }) instead');
|
|
565
|
-
}
|
|
566
|
-
const candidates = reads.flatMap((raw, index) => (projected.visible && raw.states.hidden ? [] : [{ raw, index }]));
|
|
567
|
-
// An exact label query matches any of the control's labels as the engine's reader names
|
|
568
|
-
// them, so text a label marks aria-hidden (a required-field marker) never hides a field,
|
|
569
|
-
// and an aria-label override or a second label does not either.
|
|
570
|
-
const predicate = displayValue !== null
|
|
571
|
-
? (raw) => matchesText(raw.value ?? '', displayValue)
|
|
572
|
-
: name !== null
|
|
573
|
-
? (raw) => raw.labels !== null && raw.labels.some((label) => matchesText(label, name))
|
|
574
|
-
: null;
|
|
575
|
-
const matches = predicate === null
|
|
576
|
-
? candidates
|
|
577
|
-
: await applyPostSteps(candidates.filter(({ raw }) => predicate(raw)), steps,
|
|
578
|
-
// A filter after a position runs on that one element alone.
|
|
579
|
-
async ({ index }, options) => (await projected.locator.nth(index).filter(options).count()) > 0);
|
|
580
|
-
if (currentOperation.signal.aborted) {
|
|
581
|
-
releaseHandles();
|
|
582
|
-
throw cancelled('locate cancelled');
|
|
583
|
-
}
|
|
584
559
|
try {
|
|
585
|
-
|
|
560
|
+
return await this.locateNow(expression, currentOperation);
|
|
586
561
|
}
|
|
587
562
|
catch (cause) {
|
|
588
|
-
|
|
589
|
-
throw cause;
|
|
590
|
-
}
|
|
591
|
-
if (handles !== null) {
|
|
592
|
-
// Only the matches keep their handles; the rest would otherwise live until the page goes.
|
|
593
|
-
const kept = new Set(matches.map(({ index }) => index));
|
|
594
|
-
for (const [index, handle] of handles.entries()) {
|
|
595
|
-
if (!kept.has(index))
|
|
596
|
-
void handle.dispose().catch(() => undefined);
|
|
597
|
-
}
|
|
563
|
+
throw (await pageScriptFailure(this.frameScope(expression), cause, 'locate', currentOperation.timeoutMs)) ?? cause;
|
|
598
564
|
}
|
|
599
|
-
return matches.map(({ raw, index }) => {
|
|
600
|
-
const pinned = handles?.[index];
|
|
601
|
-
// A single match keeps the strict locator, so a ref that turns
|
|
602
|
-
// ambiguous between locate and perform is stale instead of acting
|
|
603
|
-
// on whichever element is first.
|
|
604
|
-
const locator = reads.length === 1 ? projected.locator : projected.locator.nth(index);
|
|
605
|
-
const id = refs.storeLocated(pinned === undefined ? { kind: 'locator', locator } : { kind: 'element', element: pinned });
|
|
606
|
-
return inFrame(toSemanticNode({ id, revision: '' }, raw), frame.box);
|
|
607
|
-
});
|
|
608
565
|
}, staleOr);
|
|
609
566
|
}
|
|
567
|
+
/** The body of `locate`, inside its guard. */
|
|
568
|
+
async locateNow(expression, currentOperation) {
|
|
569
|
+
const session = this.requireSession();
|
|
570
|
+
const token = session.token();
|
|
571
|
+
const refs = session.refs;
|
|
572
|
+
const page = this.requirePage();
|
|
573
|
+
const frame = await this.frameOf(expression);
|
|
574
|
+
const projected = projectExpression(page, expression, { testIdAttribute: this.testIdAttribute, frameHidden: frame.hidden });
|
|
575
|
+
const { displayValue, name, steps } = projected;
|
|
576
|
+
// Every match is read by the `e2e-read` selector engine in the task that finds it, so a
|
|
577
|
+
// page that replaces it a frame later cannot detach it before the read. A
|
|
578
|
+
// predicate-filtered match is one element among many candidates (every input with a
|
|
579
|
+
// value, every control the label engine kept). Re-resolving it by position at action
|
|
580
|
+
// time would act on a neighbor whenever the page inserted or removed an element in
|
|
581
|
+
// between, so such matches are pinned to the handles of the elements the query returned,
|
|
582
|
+
// and their reads are taken back from those very handles: what was read and what is
|
|
583
|
+
// acted on are one set of elements by construction.
|
|
584
|
+
const read = readSelector({ testIdAttribute: this.testIdAttribute, secureFieldSelector: SECURE_FIELD_SELECTOR });
|
|
585
|
+
const reading = projected.locator.locator(read.selector);
|
|
586
|
+
const handles = displayValue !== null || name !== null
|
|
587
|
+
? (await reading.elementHandles())
|
|
588
|
+
: null;
|
|
589
|
+
/** Disposes every handle this locate took, on the paths that hand none of them out. */
|
|
590
|
+
const releaseHandles = () => {
|
|
591
|
+
for (const handle of handles ?? [])
|
|
592
|
+
void handle.dispose().catch(() => undefined);
|
|
593
|
+
};
|
|
594
|
+
const first = handles?.[0];
|
|
595
|
+
const taken = handles === null
|
|
596
|
+
? await reading.evaluateAll(takeReadsFunction, { token: read.token })
|
|
597
|
+
: first === undefined
|
|
598
|
+
? []
|
|
599
|
+
: await first.evaluate(takeReadsFunction, { token: read.token, elements: handles });
|
|
600
|
+
const reads = taken
|
|
601
|
+
.filter((raw) => raw !== null)
|
|
602
|
+
.map((raw) => (frame.hidden ? { ...raw, states: { ...raw.states, hidden: true } } : raw));
|
|
603
|
+
if (reads.length !== taken.length) {
|
|
604
|
+
releaseHandles();
|
|
605
|
+
throw new TestError('INVALID_LOCATOR', 'a selector that captures with * (*css=article >> text=Hello) is not supported: use filter({ has }) instead');
|
|
606
|
+
}
|
|
607
|
+
const candidates = reads.flatMap((raw, index) => (projected.visible && raw.states.hidden ? [] : [{ raw, index }]));
|
|
608
|
+
// An exact label query matches any of the control's labels as the engine's reader names
|
|
609
|
+
// them, so text a label marks aria-hidden (a required-field marker) never hides a field,
|
|
610
|
+
// and an aria-label override or a second label does not either.
|
|
611
|
+
const predicate = displayValue !== null
|
|
612
|
+
? (raw) => matchesText(raw.value ?? '', displayValue)
|
|
613
|
+
: name !== null
|
|
614
|
+
? (raw) => raw.labels !== null && raw.labels.some((label) => matchesText(label, name))
|
|
615
|
+
: null;
|
|
616
|
+
const matches = predicate === null
|
|
617
|
+
? candidates
|
|
618
|
+
: await applyPostSteps(candidates.filter(({ raw }) => predicate(raw)), steps,
|
|
619
|
+
// A filter after a position runs on that one element alone.
|
|
620
|
+
async ({ index }, options) => (await projected.locator.nth(index).filter(options).count()) > 0);
|
|
621
|
+
if (currentOperation.signal.aborted) {
|
|
622
|
+
releaseHandles();
|
|
623
|
+
throw cancelled('locate cancelled');
|
|
624
|
+
}
|
|
625
|
+
try {
|
|
626
|
+
session.check(token);
|
|
627
|
+
}
|
|
628
|
+
catch (cause) {
|
|
629
|
+
releaseHandles();
|
|
630
|
+
throw cause;
|
|
631
|
+
}
|
|
632
|
+
if (handles !== null) {
|
|
633
|
+
// Only the matches keep their handles; the rest would otherwise live until the page goes.
|
|
634
|
+
const kept = new Set(matches.map(({ index }) => index));
|
|
635
|
+
for (const [index, handle] of handles.entries()) {
|
|
636
|
+
if (!kept.has(index))
|
|
637
|
+
void handle.dispose().catch(() => undefined);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
return matches.map(({ raw, index }) => {
|
|
641
|
+
const pinned = handles?.[index];
|
|
642
|
+
// A single match keeps the strict locator, so a ref that turns
|
|
643
|
+
// ambiguous between locate and perform is stale instead of acting
|
|
644
|
+
// on whichever element is first.
|
|
645
|
+
const locator = reads.length === 1 ? projected.locator : projected.locator.nth(index);
|
|
646
|
+
const id = refs.storeLocated(pinned === undefined ? { kind: 'locator', locator } : { kind: 'element', element: pinned });
|
|
647
|
+
return inFrame(toSemanticNode({ id, revision: '' }, raw), frame.box);
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
/** The page or frame an expression's frame selectors lead to, unresolved. */
|
|
651
|
+
frameScope(expression) {
|
|
652
|
+
let scope = this.requirePage();
|
|
653
|
+
for (const selector of frameSelectors(expression))
|
|
654
|
+
scope = scope.frameLocator(selector);
|
|
655
|
+
return scope;
|
|
656
|
+
}
|
|
610
657
|
/**
|
|
611
658
|
* One action on a located or observed node. A `swipe` on the observation
|
|
612
659
|
* root is the viewport swipe: a wheel gesture sized by the viewport, with no
|
|
@@ -620,7 +667,11 @@ export class PlaywrightSurface {
|
|
|
620
667
|
this.requireSession().requireObservation();
|
|
621
668
|
return performViewportSwipe(page, action.direction, action.momentum ?? 'none');
|
|
622
669
|
}
|
|
623
|
-
|
|
670
|
+
const { refs } = this.requireSession();
|
|
671
|
+
return refs.lookup(ref).then((target) => {
|
|
672
|
+
currentOperation.signal.throwIfAborted();
|
|
673
|
+
return dispatchLocatorAction(target, action, currentOperation, (other) => refs.lookup(other));
|
|
674
|
+
});
|
|
624
675
|
}, (cause) => classifyActionError(cause, action)));
|
|
625
676
|
}
|
|
626
677
|
/** One pointer action at a viewport point in CSS pixels, with nothing resolved behind it; see `dispatchPointerAction`. */
|
|
@@ -770,17 +821,39 @@ export class PlaywrightSurface {
|
|
|
770
821
|
/** Redacted at the source: secure fields in every frame are masked, and the caret is hidden without editing styles, as in observation pixels. */
|
|
771
822
|
screenshot(label, operation) {
|
|
772
823
|
return this.guard(operation, 'screenshot', async (currentOperation) => {
|
|
773
|
-
const page = this.requirePage();
|
|
774
824
|
const { relative, absolute } = this.artifactPath('screenshots', label, '.png');
|
|
775
|
-
const deadline =
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
825
|
+
const deadline = new Deadline(currentOperation.timeoutMs);
|
|
826
|
+
// The masks are resolved in the document showing when the capture
|
|
827
|
+
// starts. A capture that fails while the page replaces that document
|
|
828
|
+
// is taken of the replacing one, masked by the same rule, within the
|
|
829
|
+
// step's own deadline.
|
|
830
|
+
for (;;) {
|
|
831
|
+
const page = this.requirePage();
|
|
832
|
+
const documents = watchDocuments(page);
|
|
833
|
+
try {
|
|
834
|
+
const masks = await secureFieldMasks(page);
|
|
835
|
+
await withHiddenCaret(page, () => page.screenshot({
|
|
836
|
+
path: absolute,
|
|
837
|
+
timeout: Math.max(1, deadline.remaining()),
|
|
838
|
+
caret: 'initial',
|
|
839
|
+
...maskOptions(masks),
|
|
840
|
+
}));
|
|
841
|
+
return relative;
|
|
842
|
+
}
|
|
843
|
+
catch (cause) {
|
|
844
|
+
if (!documents.replaced() || isPwTimeout(cause) || currentOperation.signal.aborted)
|
|
845
|
+
throw cause;
|
|
846
|
+
if (deadline.expired()) {
|
|
847
|
+
throw new EngineError('OPERATION_TIMEOUT', `screenshot timed out: the page replaced its document during every capture for ${currentOperation.timeoutMs}ms`, {
|
|
848
|
+
retryable: false,
|
|
849
|
+
cause,
|
|
850
|
+
});
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
finally {
|
|
854
|
+
documents.stop();
|
|
855
|
+
}
|
|
856
|
+
}
|
|
784
857
|
});
|
|
785
858
|
}
|
|
786
859
|
/** Starts the attempt's video. */
|
|
@@ -817,8 +890,9 @@ export class PlaywrightSurface {
|
|
|
817
890
|
// --- observation ---
|
|
818
891
|
/**
|
|
819
892
|
* Captures one atomic semantic observation. Secure fields are masked in the
|
|
820
|
-
* page before the tree leaves the engine, and every node
|
|
821
|
-
* element
|
|
893
|
+
* page before the tree leaves the engine, and every node stays reachable
|
|
894
|
+
* through its document's element list until the next observation replaces
|
|
895
|
+
* the generation; an action makes the node's handle on first use.
|
|
822
896
|
*
|
|
823
897
|
* With `options.pixels`, masked viewport pixels are captured alongside the
|
|
824
898
|
* tree rather than after it, so the image and the node geometry describe the
|
|
@@ -862,6 +936,31 @@ export class PlaywrightSurface {
|
|
|
862
936
|
}
|
|
863
937
|
}
|
|
864
938
|
}
|
|
939
|
+
/** Watches `page` until `stop` for a frame that commits a new document or detaches, either of which replaces a document a capture is reading. */
|
|
940
|
+
function watchDocuments(page) {
|
|
941
|
+
let replaced = false;
|
|
942
|
+
const mark = () => {
|
|
943
|
+
replaced = true;
|
|
944
|
+
};
|
|
945
|
+
page.on('framenavigated', mark);
|
|
946
|
+
page.on('framedetached', mark);
|
|
947
|
+
return {
|
|
948
|
+
replaced: () => replaced,
|
|
949
|
+
stop: () => {
|
|
950
|
+
page.off('framenavigated', mark);
|
|
951
|
+
page.off('framedetached', mark);
|
|
952
|
+
},
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
/** The frame a request belongs to; undefined for a popup's first navigation, which Playwright issues before the popup's frame exists. */
|
|
956
|
+
function frameOf(request) {
|
|
957
|
+
try {
|
|
958
|
+
return request.frame();
|
|
959
|
+
}
|
|
960
|
+
catch {
|
|
961
|
+
return undefined;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
865
964
|
/**
|
|
866
965
|
* A located node in the top-level viewport's space, as the observation tree
|
|
867
966
|
* reports it: its box shifted by the frame's and clipped to it, and dropped
|
|
@@ -870,7 +969,6 @@ export class PlaywrightSurface {
|
|
|
870
969
|
function inFrame(node, frame) {
|
|
871
970
|
if (frame === undefined || node.rect === undefined)
|
|
872
971
|
return node;
|
|
873
|
-
const { rect, ...rest } = node;
|
|
874
|
-
|
|
875
|
-
return placed === undefined ? rest : { ...rest, rect: placed };
|
|
972
|
+
const { rect, origin, ...rest } = node;
|
|
973
|
+
return frame === null ? rest : { ...rest, ...placeBoxInFrame(rect, origin, frame) };
|
|
876
974
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@e2e-dev/web",
|
|
3
|
-
"version": "0.15.5
|
|
3
|
+
"version": "0.15.5",
|
|
4
4
|
"description": "Web engine for e2e agentic end-to-end testing, built on Playwright",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"e2e",
|
|
@@ -50,12 +50,12 @@
|
|
|
50
50
|
"playwright-core": "1.64.1"
|
|
51
51
|
},
|
|
52
52
|
"peerDependencies": {
|
|
53
|
-
"e2e": "0.22.1
|
|
53
|
+
"e2e": ">=0.22.1 <1"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
56
|
"@types/node": "26.6.6",
|
|
57
57
|
"@types/pngjs": "6.0.5",
|
|
58
|
-
"e2e": "0.
|
|
58
|
+
"e2e": "0.23.0",
|
|
59
59
|
"pngjs": "7.0.0",
|
|
60
60
|
"typescript": "7.0.2",
|
|
61
61
|
"vitest": "5.1.0"
|