@mercuryo-ai/agentbrowse 0.2.60 → 0.2.63
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/CHANGELOG.md +33 -1
- package/README.md +132 -14
- package/dist/browser-session-state.d.ts +40 -10
- package/dist/browser-session-state.d.ts.map +1 -1
- package/dist/browser-session-state.js +63 -5
- package/dist/commands/act.d.ts.map +1 -1
- package/dist/commands/act.js +548 -535
- package/dist/commands/attach.d.ts +1 -3
- package/dist/commands/attach.d.ts.map +1 -1
- package/dist/commands/attach.js +5 -12
- package/dist/commands/browser-connection-failure.d.ts +9 -0
- package/dist/commands/browser-connection-failure.d.ts.map +1 -0
- package/dist/commands/browser-connection-failure.js +15 -0
- package/dist/commands/browser-status.d.ts +0 -2
- package/dist/commands/browser-status.d.ts.map +1 -1
- package/dist/commands/browser-status.js +27 -37
- package/dist/commands/close.d.ts.map +1 -1
- package/dist/commands/close.js +5 -0
- package/dist/commands/extract.d.ts.map +1 -1
- package/dist/commands/extract.js +147 -144
- package/dist/commands/interaction-kernel.d.ts +1 -1
- package/dist/commands/interaction-kernel.d.ts.map +1 -1
- package/dist/commands/interaction-kernel.js +1 -1
- package/dist/commands/launch.d.ts +0 -1
- package/dist/commands/launch.d.ts.map +1 -1
- package/dist/commands/launch.js +11 -12
- package/dist/commands/navigate.d.ts.map +1 -1
- package/dist/commands/navigate.js +79 -73
- package/dist/commands/observe-accessibility.d.ts.map +1 -1
- package/dist/commands/observe-accessibility.js +36 -2
- package/dist/commands/observe-inventory.d.ts +50 -7
- package/dist/commands/observe-inventory.d.ts.map +1 -1
- package/dist/commands/observe-inventory.js +822 -99
- package/dist/commands/observe-persistence.d.ts.map +1 -1
- package/dist/commands/observe-persistence.js +49 -6
- package/dist/commands/observe-projection.d.ts +6 -2
- package/dist/commands/observe-projection.d.ts.map +1 -1
- package/dist/commands/observe-projection.js +251 -27
- package/dist/commands/observe-semantics.d.ts +1 -0
- package/dist/commands/observe-semantics.d.ts.map +1 -1
- package/dist/commands/observe-semantics.js +541 -135
- package/dist/commands/observe-signals.d.ts +4 -4
- package/dist/commands/observe-signals.d.ts.map +1 -1
- package/dist/commands/observe-signals.js +2 -2
- package/dist/commands/observe-surfaces.d.ts +2 -1
- package/dist/commands/observe-surfaces.d.ts.map +1 -1
- package/dist/commands/observe-surfaces.js +143 -45
- package/dist/commands/observe.d.ts +5 -1
- package/dist/commands/observe.d.ts.map +1 -1
- package/dist/commands/observe.js +266 -274
- package/dist/commands/screenshot.d.ts.map +1 -1
- package/dist/commands/screenshot.js +50 -64
- package/dist/commands/semantic-observe.d.ts.map +1 -1
- package/dist/commands/semantic-observe.js +43 -0
- package/dist/library.d.ts +3 -1
- package/dist/library.d.ts.map +1 -1
- package/dist/library.js +3 -1
- package/dist/match-resolve-fill.d.ts +196 -0
- package/dist/match-resolve-fill.d.ts.map +1 -0
- package/dist/match-resolve-fill.js +700 -0
- package/dist/match-resolve-fill.test-support.d.ts +34 -0
- package/dist/match-resolve-fill.test-support.d.ts.map +1 -0
- package/dist/match-resolve-fill.test-support.js +81 -0
- package/dist/protected-fill.d.ts.map +1 -1
- package/dist/protected-fill.js +46 -7
- package/dist/runtime-protected-state.d.ts.map +1 -1
- package/dist/runtime-protected-state.js +12 -0
- package/dist/runtime-state.d.ts +6 -0
- package/dist/runtime-state.d.ts.map +1 -1
- package/dist/runtime-state.js +6 -0
- package/dist/secrets/form-matcher.d.ts.map +1 -1
- package/dist/secrets/form-matcher.js +76 -27
- package/dist/secrets/protected-exact-value-redaction.d.ts.map +1 -1
- package/dist/secrets/protected-exact-value-redaction.js +6 -0
- package/dist/secrets/protected-fill.js +3 -3
- package/dist/session.d.ts +3 -3
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +2 -2
- package/dist/solver/browser-launcher.d.ts.map +1 -1
- package/dist/solver/browser-launcher.js +2 -1
- package/dist/sticky-owner-host-entry.d.ts +2 -0
- package/dist/sticky-owner-host-entry.d.ts.map +1 -0
- package/dist/sticky-owner-host-entry.js +97 -0
- package/dist/sticky-owner.d.ts +15 -0
- package/dist/sticky-owner.d.ts.map +1 -0
- package/dist/sticky-owner.js +431 -0
- package/dist/testing.d.ts +1 -0
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +1 -0
- package/docs/README.md +28 -11
- package/docs/api-reference.md +311 -19
- package/docs/assistive-runtime.md +41 -16
- package/docs/configuration.md +36 -4
- package/docs/getting-started.md +73 -5
- package/docs/integration-checklist.md +32 -3
- package/docs/match-resolve-fill.md +699 -0
- package/docs/protected-fill.md +373 -91
- package/docs/testing.md +147 -15
- package/docs/troubleshooting.md +47 -6
- package/examples/README.md +7 -0
- package/examples/match-resolve-fill.ts +107 -0
- package/package.json +4 -2
- package/dist/protected-fill-browser.d.ts +0 -22
- package/dist/protected-fill-browser.d.ts.map +0 -1
- package/dist/protected-fill-browser.js +0 -52
package/dist/commands/act.js
CHANGED
|
@@ -21,8 +21,10 @@ import { isLocatorUserActionable } from './user-actionable.js';
|
|
|
21
21
|
import { resolveSurfaceScopeRoot } from './target-resolution.js';
|
|
22
22
|
import { buildProtectedArtifactsSuppressed } from '../secrets/protected-artifact-guard.js';
|
|
23
23
|
import { scrubProtectedExactValues } from '../secrets/protected-exact-value-redaction.js';
|
|
24
|
-
import {
|
|
24
|
+
import { listPages, resolvePageByRef, syncSessionPage } from '../playwright-runtime.js';
|
|
25
25
|
import { withApiTraceContext } from '../command-api-tracing.js';
|
|
26
|
+
import { describeBrowserConnectionFailure } from './browser-connection-failure.js';
|
|
27
|
+
import { withStickyOwnerBrowser } from '../sticky-owner.js';
|
|
26
28
|
function ensureValue(action, value) {
|
|
27
29
|
if (action === 'click')
|
|
28
30
|
return undefined;
|
|
@@ -504,7 +506,6 @@ export async function actBrowser(session, targetRef, action, value) {
|
|
|
504
506
|
attempts.push(`action.alias:${requestedAction}->${action}`);
|
|
505
507
|
}
|
|
506
508
|
const startedAt = Date.now();
|
|
507
|
-
let browser = null;
|
|
508
509
|
let failureMessage = null;
|
|
509
510
|
let failureArtifacts;
|
|
510
511
|
let currentPage = null;
|
|
@@ -525,580 +526,592 @@ export async function actBrowser(session, targetRef, action, value) {
|
|
|
525
526
|
finishFailure: async (_artifactDir) => undefined,
|
|
526
527
|
};
|
|
527
528
|
try {
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
}
|
|
542
|
-
try {
|
|
543
|
-
const page = await resolvePageByRef(browser, session, target.pageRef);
|
|
544
|
-
currentPage = page;
|
|
545
|
-
setCurrentPage(session, target.pageRef);
|
|
546
|
-
const { url } = await syncSessionPage(session, target.pageRef, page);
|
|
547
|
-
trace = await startActionTrace(page, {
|
|
548
|
-
suppressSensitiveArtifacts: Boolean(protectedExposureAtStart),
|
|
549
|
-
});
|
|
550
|
-
if (liveTarget.pageSignature && normalizePageSignature(url) !== liveTarget.pageSignature) {
|
|
551
|
-
staleReason = 'page-signature-mismatch';
|
|
552
|
-
throw new Error('stale_target_page_signature_changed');
|
|
553
|
-
}
|
|
554
|
-
const tryRebindMutableFieldTarget = async (resolvedLocator, strategy) => {
|
|
555
|
-
if (!liveTarget.domSignature) {
|
|
556
|
-
return false;
|
|
557
|
-
}
|
|
558
|
-
for (let attemptIndex = 0; attemptIndex < MUTABLE_FIELD_REBIND_RETRY_DELAYS_MS.length; attemptIndex += 1) {
|
|
559
|
-
const delayMs = MUTABLE_FIELD_REBIND_RETRY_DELAYS_MS[attemptIndex] ?? 0;
|
|
560
|
-
if (attemptIndex > 0) {
|
|
561
|
-
attempts.push(`domSignature.rebind.retry:${strategy}:${attemptIndex + 1}`);
|
|
562
|
-
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
563
|
-
}
|
|
564
|
-
const snapshot = await readLocatorBindingSnapshot(resolvedLocator).catch(() => null);
|
|
565
|
-
if (!snapshot?.domSignature) {
|
|
566
|
-
continue;
|
|
529
|
+
return await withStickyOwnerBrowser(session, async (browser) => {
|
|
530
|
+
try {
|
|
531
|
+
const page = await resolvePageByRef(browser, session, target.pageRef);
|
|
532
|
+
currentPage = page;
|
|
533
|
+
setCurrentPage(session, target.pageRef);
|
|
534
|
+
const { url } = await syncSessionPage(session, target.pageRef, page);
|
|
535
|
+
trace = await startActionTrace(page, {
|
|
536
|
+
suppressSensitiveArtifacts: Boolean(protectedExposureAtStart),
|
|
537
|
+
});
|
|
538
|
+
if (liveTarget.pageSignature &&
|
|
539
|
+
normalizePageSignature(url) !== liveTarget.pageSignature) {
|
|
540
|
+
staleReason = 'page-signature-mismatch';
|
|
541
|
+
throw new Error('stale_target_page_signature_changed');
|
|
567
542
|
}
|
|
568
|
-
|
|
543
|
+
const tryRebindMutableFieldTarget = async (resolvedLocator, strategy) => {
|
|
544
|
+
if (!liveTarget.domSignature) {
|
|
545
|
+
return false;
|
|
546
|
+
}
|
|
547
|
+
for (let attemptIndex = 0; attemptIndex < MUTABLE_FIELD_REBIND_RETRY_DELAYS_MS.length; attemptIndex += 1) {
|
|
548
|
+
const delayMs = MUTABLE_FIELD_REBIND_RETRY_DELAYS_MS[attemptIndex] ?? 0;
|
|
549
|
+
if (attemptIndex > 0) {
|
|
550
|
+
attempts.push(`domSignature.rebind.retry:${strategy}:${attemptIndex + 1}`);
|
|
551
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
552
|
+
}
|
|
553
|
+
const snapshot = await readLocatorBindingSnapshot(resolvedLocator).catch(() => null);
|
|
554
|
+
if (!snapshot?.domSignature) {
|
|
555
|
+
continue;
|
|
556
|
+
}
|
|
557
|
+
if (snapshot.domSignature === liveTarget.domSignature) {
|
|
558
|
+
return false;
|
|
559
|
+
}
|
|
560
|
+
if (!isCompatibleMutableFieldBinding(liveTarget, snapshot)) {
|
|
561
|
+
continue;
|
|
562
|
+
}
|
|
563
|
+
attempts.push(`domSignature.rebound:${strategy}`);
|
|
564
|
+
const updatedTarget = updateTarget(session, targetRef, {
|
|
565
|
+
domSignature: snapshot.domSignature,
|
|
566
|
+
label: snapshot.label ?? liveTarget.label,
|
|
567
|
+
lifecycle: 'live',
|
|
568
|
+
lifecycleReason: undefined,
|
|
569
|
+
availability: { state: 'available' },
|
|
570
|
+
semantics: {
|
|
571
|
+
...liveTarget.semantics,
|
|
572
|
+
name: snapshot.label ?? liveTarget.semantics?.name,
|
|
573
|
+
role: snapshot.role ?? liveTarget.semantics?.role,
|
|
574
|
+
},
|
|
575
|
+
});
|
|
576
|
+
if (updatedTarget) {
|
|
577
|
+
liveTarget = updatedTarget;
|
|
578
|
+
}
|
|
579
|
+
return true;
|
|
580
|
+
}
|
|
569
581
|
return false;
|
|
582
|
+
};
|
|
583
|
+
const assertResolvedTargetStillValid = async (resolvedLocator, stage, options) => {
|
|
584
|
+
const bindingTarget = options?.skipDomSignature
|
|
585
|
+
? { pageSignature: liveTarget.pageSignature, domSignature: undefined }
|
|
586
|
+
: liveTarget;
|
|
587
|
+
await assertStoredBindingStillValid(page, resolvedLocator, bindingTarget, stage, {
|
|
588
|
+
onReason: async (reason, staleStage) => {
|
|
589
|
+
switch (reason) {
|
|
590
|
+
case 'page_signature_mismatch':
|
|
591
|
+
attempts.push(`stale.page-signature:${staleStage}`);
|
|
592
|
+
staleReason = 'page-signature-mismatch';
|
|
593
|
+
return false;
|
|
594
|
+
case 'locator_resolution_failed':
|
|
595
|
+
attempts.push(`stale.locator:${staleStage}`);
|
|
596
|
+
staleReason = 'locator-resolution-failed';
|
|
597
|
+
return false;
|
|
598
|
+
case 'dom_signature_mismatch': {
|
|
599
|
+
const rebound = await tryRebindMutableFieldTarget(resolvedLocator, staleStage);
|
|
600
|
+
if (rebound) {
|
|
601
|
+
return true;
|
|
602
|
+
}
|
|
603
|
+
attempts.push(`stale.dom-signature:${staleStage}`);
|
|
604
|
+
staleReason = 'dom-signature-mismatch';
|
|
605
|
+
return false;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
},
|
|
609
|
+
errorForReason: (reason) => {
|
|
610
|
+
switch (reason) {
|
|
611
|
+
case 'page_signature_mismatch':
|
|
612
|
+
return 'stale_target_page_signature_changed';
|
|
613
|
+
case 'locator_resolution_failed':
|
|
614
|
+
return 'stale_target_locator_resolution_failed';
|
|
615
|
+
case 'dom_signature_mismatch':
|
|
616
|
+
return 'stale_target_dom_signature_changed';
|
|
617
|
+
}
|
|
618
|
+
},
|
|
619
|
+
});
|
|
620
|
+
};
|
|
621
|
+
let resolvedBy = null;
|
|
622
|
+
const beforePages = listPages(browser);
|
|
623
|
+
const shouldCheckProgress = shouldVerifyObservableProgress(target, action);
|
|
624
|
+
const beforePageObservation = shouldCheckProgress
|
|
625
|
+
? await capturePageObservation(page)
|
|
626
|
+
: null;
|
|
627
|
+
const deferSurfaceResolution = shouldDeferSurfaceResolutionForEditablePress(target, action);
|
|
628
|
+
let baseRoot = page;
|
|
629
|
+
let locatorRoot = baseRoot;
|
|
630
|
+
let surfaceRoot = null;
|
|
631
|
+
if (!deferSurfaceResolution) {
|
|
632
|
+
({ baseRoot, locatorRoot, surfaceRoot } = await resolveInteractionRoots(page, target, surface, attempts, {
|
|
633
|
+
recordSelfTargetReuse: true,
|
|
634
|
+
}));
|
|
570
635
|
}
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
const
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
return true;
|
|
591
|
-
}
|
|
592
|
-
return false;
|
|
593
|
-
};
|
|
594
|
-
const assertResolvedTargetStillValid = async (resolvedLocator, stage) => {
|
|
595
|
-
await assertStoredBindingStillValid(page, resolvedLocator, liveTarget, stage, {
|
|
596
|
-
onReason: async (reason, staleStage) => {
|
|
597
|
-
switch (reason) {
|
|
598
|
-
case 'page_signature_mismatch':
|
|
599
|
-
attempts.push(`stale.page-signature:${staleStage}`);
|
|
600
|
-
staleReason = 'page-signature-mismatch';
|
|
636
|
+
let lastError = null;
|
|
637
|
+
let sawDomSignatureMismatch = false;
|
|
638
|
+
let sawDisabledTarget = false;
|
|
639
|
+
let sawReadonlyTarget = false;
|
|
640
|
+
const attemptResolvedLocator = async (resolvedLocator, strategy, options) => {
|
|
641
|
+
if (!options?.skipDomSignature && liveTarget.domSignature) {
|
|
642
|
+
const rebound = await tryRebindMutableFieldTarget(resolvedLocator, strategy);
|
|
643
|
+
if (!rebound) {
|
|
644
|
+
const liveSignature = await readLocatorDomSignature(resolvedLocator);
|
|
645
|
+
if (liveSignature && liveSignature !== liveTarget.domSignature) {
|
|
646
|
+
attempts.push(`domSignature.mismatch:${strategy}`);
|
|
647
|
+
sawDomSignatureMismatch = true;
|
|
648
|
+
return false;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
let acceptanceProbe = null;
|
|
653
|
+
const tryRecoverActionErrorAcceptance = async () => {
|
|
654
|
+
if (!acceptanceProbe) {
|
|
601
655
|
return false;
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
656
|
+
}
|
|
657
|
+
const acceptance = await waitForAcceptanceProbe(acceptanceProbe).catch(() => null);
|
|
658
|
+
if (acceptance?.polls && acceptance.polls > 1) {
|
|
659
|
+
attempts.push(`acceptance.polled:${acceptance.polls}`);
|
|
660
|
+
}
|
|
661
|
+
if (!acceptance?.accepted) {
|
|
605
662
|
return false;
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
663
|
+
}
|
|
664
|
+
if (acceptanceProbe.policy === 'submit') {
|
|
665
|
+
const submitResolution = await resolveSubmitResult(acceptanceProbe, acceptance.afterPageObservation);
|
|
666
|
+
if (!submitResolution.acceptAsProgress) {
|
|
667
|
+
return false;
|
|
668
|
+
}
|
|
669
|
+
attempts.push(`submit-resolution:${submitResolution.finalVerdict}`);
|
|
670
|
+
if (submitResolution.claims.some((claim) => claim.kind === 'soft_result_candidate')) {
|
|
671
|
+
attempts.push('submit-resolution:soft-result-candidate');
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
attempts.push(`acceptance.recovered:${acceptanceProbe.policy}`);
|
|
675
|
+
incrementMetric(session, 'fallbackActions');
|
|
676
|
+
resolvedBy = 'playwright-locator';
|
|
677
|
+
locatorStrategy = strategy;
|
|
678
|
+
recoveredProgressProbe = acceptanceProbe;
|
|
679
|
+
progressProbe = null;
|
|
680
|
+
lastError = null;
|
|
681
|
+
recoveredAfterError = true;
|
|
682
|
+
recoveredAcceptancePolicy = acceptanceProbe.policy;
|
|
683
|
+
setTargetAvailability(session, targetRef, 'available');
|
|
684
|
+
return true;
|
|
685
|
+
};
|
|
686
|
+
try {
|
|
687
|
+
const valueProjection = await projectActionValue({
|
|
688
|
+
target,
|
|
689
|
+
action,
|
|
690
|
+
actionValue,
|
|
691
|
+
locator: resolvedLocator,
|
|
692
|
+
attempts,
|
|
693
|
+
});
|
|
694
|
+
const executionValue = valueProjection?.executionValue ?? actionValue;
|
|
695
|
+
const acceptanceValue = valueProjection?.acceptanceValue ?? actionValue;
|
|
696
|
+
acceptanceProbe = await createAcceptanceProbe({
|
|
697
|
+
session,
|
|
698
|
+
page,
|
|
699
|
+
target,
|
|
700
|
+
action,
|
|
701
|
+
actionValue: acceptanceValue,
|
|
702
|
+
locator: resolvedLocator,
|
|
703
|
+
beforePageObservation,
|
|
704
|
+
});
|
|
705
|
+
attempts.push(`resolve:${strategy}`);
|
|
706
|
+
const usedFallback = await applyActionWithFallbacks(page, locatorRoot, resolvedLocator, action, executionValue, attempts, target.controlFamily, {
|
|
707
|
+
clickActivationStrategy: clickActivationStrategyForTarget(target, action),
|
|
708
|
+
guards: {
|
|
709
|
+
assertStillValid: async (stage) => {
|
|
710
|
+
await assertResolvedTargetStillValid(resolvedLocator, stage, {
|
|
711
|
+
skipDomSignature: options?.skipDomSignature,
|
|
712
|
+
});
|
|
713
|
+
},
|
|
714
|
+
},
|
|
715
|
+
});
|
|
716
|
+
if (usedFallback) {
|
|
717
|
+
incrementMetric(session, 'fallbackActions');
|
|
718
|
+
}
|
|
719
|
+
resolvedBy = 'playwright-locator';
|
|
720
|
+
locatorStrategy = strategy;
|
|
721
|
+
progressProbe = acceptanceProbe;
|
|
722
|
+
setTargetAvailability(session, targetRef, 'available');
|
|
723
|
+
return true;
|
|
724
|
+
}
|
|
725
|
+
catch (err) {
|
|
726
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
727
|
+
const shouldAttemptAcceptanceRecovery = acceptanceProbe !== null &&
|
|
728
|
+
(acceptanceProbe.policy === 'value-change' ||
|
|
729
|
+
acceptanceProbe.policy === 'selection' ||
|
|
730
|
+
acceptanceProbe.policy === 'date-selection' ||
|
|
731
|
+
acceptanceProbe.policy === 'navigation' ||
|
|
732
|
+
acceptanceProbe.policy === 'submit');
|
|
733
|
+
if (shouldAttemptAcceptanceRecovery && (await tryRecoverActionErrorAcceptance())) {
|
|
734
|
+
return true;
|
|
735
|
+
}
|
|
736
|
+
if (staleReason) {
|
|
737
|
+
throw lastError;
|
|
738
|
+
}
|
|
739
|
+
try {
|
|
740
|
+
await assertResolvedTargetStillValid(resolvedLocator, `after-error:${strategy}`, {
|
|
741
|
+
skipDomSignature: options?.skipDomSignature,
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
catch (validationError) {
|
|
745
|
+
if ((acceptanceProbe?.policy === 'navigation' ||
|
|
746
|
+
acceptanceProbe?.policy === 'submit') &&
|
|
747
|
+
validationError instanceof Error &&
|
|
748
|
+
validationError.message === 'stale_target_page_signature_changed' &&
|
|
749
|
+
(await tryRecoverActionErrorAcceptance())) {
|
|
609
750
|
return true;
|
|
610
751
|
}
|
|
611
|
-
|
|
612
|
-
staleReason = 'dom-signature-mismatch';
|
|
613
|
-
return false;
|
|
752
|
+
throw validationError;
|
|
614
753
|
}
|
|
754
|
+
return false;
|
|
615
755
|
}
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
756
|
+
};
|
|
757
|
+
const watchForNewPage = shouldWatchForNewPageAfterAction(target, action);
|
|
758
|
+
const popupPromise = watchForNewPage
|
|
759
|
+
? waitForPopup(page.context())
|
|
760
|
+
: Promise.resolve(null);
|
|
761
|
+
const tryRankedCandidates = async () => {
|
|
762
|
+
const resolution = await resolvePreparedLocatorCandidates({
|
|
763
|
+
target,
|
|
764
|
+
action,
|
|
765
|
+
baseRoot,
|
|
766
|
+
locatorRoot,
|
|
767
|
+
surfaceRoot,
|
|
768
|
+
attempts,
|
|
769
|
+
prepareOptions: {
|
|
770
|
+
allowReadonlyFallback: action === 'fill' && target.controlFamily === 'datepicker',
|
|
771
|
+
allowDescendantPressFallback: action === 'press' &&
|
|
772
|
+
(target.controlFamily === 'text-input' ||
|
|
773
|
+
target.controlFamily === 'select' ||
|
|
774
|
+
target.controlFamily === 'datepicker'),
|
|
775
|
+
isUserActionable: isLocatorUserActionable,
|
|
776
|
+
},
|
|
777
|
+
onPreparedLocator: async (resolvedLocator, strategy) => attemptResolvedLocator(resolvedLocator, strategy.strategy, {
|
|
778
|
+
skipDomSignature: strategy.activationOwner === true,
|
|
779
|
+
}),
|
|
780
|
+
});
|
|
781
|
+
if (resolution.sawDisabledTarget) {
|
|
782
|
+
sawDisabledTarget = true;
|
|
625
783
|
}
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
if (!deferSurfaceResolution) {
|
|
640
|
-
({ baseRoot, locatorRoot, surfaceRoot } = await resolveInteractionRoots(page, target, surface, attempts, {
|
|
641
|
-
recordSelfTargetReuse: true,
|
|
642
|
-
}));
|
|
643
|
-
}
|
|
644
|
-
let lastError = null;
|
|
645
|
-
let sawDomSignatureMismatch = false;
|
|
646
|
-
let sawDisabledTarget = false;
|
|
647
|
-
let sawReadonlyTarget = false;
|
|
648
|
-
const attemptResolvedLocator = async (resolvedLocator, strategy, options) => {
|
|
649
|
-
if (!options?.skipDomSignature && liveTarget.domSignature) {
|
|
650
|
-
const rebound = await tryRebindMutableFieldTarget(resolvedLocator, strategy);
|
|
651
|
-
if (!rebound) {
|
|
652
|
-
const liveSignature = await readLocatorDomSignature(resolvedLocator);
|
|
653
|
-
if (liveSignature && liveSignature !== liveTarget.domSignature) {
|
|
654
|
-
attempts.push(`domSignature.mismatch:${strategy}`);
|
|
655
|
-
sawDomSignatureMismatch = true;
|
|
656
|
-
return false;
|
|
784
|
+
if (resolution.sawReadonlyTarget) {
|
|
785
|
+
sawReadonlyTarget = true;
|
|
786
|
+
}
|
|
787
|
+
};
|
|
788
|
+
await tryRankedCandidates();
|
|
789
|
+
if (!resolvedBy && !lastError && deferSurfaceResolution && surface) {
|
|
790
|
+
const deferredSurfaceRoot = await resolveSurfaceScopeRoot(page, surface, attempts);
|
|
791
|
+
if (deferredSurfaceRoot) {
|
|
792
|
+
surfaceRoot = deferredSurfaceRoot;
|
|
793
|
+
locatorRoot = targetUsesSurfaceAsPrimaryLocator(target, surface)
|
|
794
|
+
? baseRoot
|
|
795
|
+
: surfaceRoot;
|
|
796
|
+
await tryRankedCandidates();
|
|
657
797
|
}
|
|
658
798
|
}
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
799
|
+
if (!resolvedBy && !lastError && surfaceRoot) {
|
|
800
|
+
const recoveredLocator = await recoverLocatorFromSurfaceRoot(surfaceRoot, target, action, attempts);
|
|
801
|
+
if (recoveredLocator) {
|
|
802
|
+
await attemptResolvedLocator(recoveredLocator, 'surface-descendant', {
|
|
803
|
+
skipDomSignature: true,
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
if (!resolvedBy) {
|
|
808
|
+
if (sawDomSignatureMismatch) {
|
|
809
|
+
staleReason = 'dom-signature-mismatch';
|
|
810
|
+
throw new Error('stale_target_dom_signature_changed');
|
|
811
|
+
}
|
|
812
|
+
if (sawDisabledTarget) {
|
|
813
|
+
setTargetAvailability(session, targetRef, 'gated', 'disabled');
|
|
814
|
+
throw new Error('target_disabled');
|
|
815
|
+
}
|
|
816
|
+
if (sawReadonlyTarget && (action === 'fill' || action === 'type')) {
|
|
817
|
+
setTargetAvailability(session, targetRef, 'gated', 'readonly');
|
|
818
|
+
throw new Error('target_readonly');
|
|
819
|
+
}
|
|
820
|
+
if (!lastError &&
|
|
821
|
+
target.surfaceRef &&
|
|
822
|
+
(target.acceptancePolicy === 'selection' ||
|
|
823
|
+
target.acceptancePolicy === 'date-selection')) {
|
|
824
|
+
setTargetAvailability(session, targetRef, 'surface-inactive', 'surface-not-active');
|
|
825
|
+
throw new Error('target_surface_inactive');
|
|
826
|
+
}
|
|
827
|
+
if (!resolvedBy) {
|
|
828
|
+
if (!lastError &&
|
|
829
|
+
(target.controlFamily === 'text-input' ||
|
|
830
|
+
target.controlFamily === 'select' ||
|
|
831
|
+
target.controlFamily === 'datepicker')) {
|
|
832
|
+
staleReason = 'locator-resolution-failed';
|
|
833
|
+
throw new Error('stale_target_locator_resolution_failed');
|
|
834
|
+
}
|
|
835
|
+
if (lastError) {
|
|
836
|
+
throw lastError;
|
|
837
|
+
}
|
|
838
|
+
throw new Error('deterministic_target_resolution_failed');
|
|
839
|
+
}
|
|
664
840
|
}
|
|
665
|
-
const
|
|
666
|
-
|
|
667
|
-
|
|
841
|
+
const popup = await popupPromise;
|
|
842
|
+
const latePage = !popup && watchForNewPage ? await waitForLatePage(browser, beforePages) : null;
|
|
843
|
+
if (latePage) {
|
|
844
|
+
attempts.push('late-page-captured');
|
|
668
845
|
}
|
|
669
|
-
|
|
670
|
-
|
|
846
|
+
const discoveredPage = popup ?? latePage;
|
|
847
|
+
const afterPages = discoveredPage
|
|
848
|
+
? [...beforePages, discoveredPage]
|
|
849
|
+
: listPages(browser);
|
|
850
|
+
const capturedPopup = await capturePopupIfOpened(session, beforePages, afterPages, target.pageRef, attempts);
|
|
851
|
+
let finalPageRef = target.pageRef;
|
|
852
|
+
if (capturedPopup) {
|
|
853
|
+
await syncSessionPage(session, capturedPopup.page.pageRef, capturedPopup.popup, {
|
|
854
|
+
settleTimeoutMs: 1_500,
|
|
855
|
+
});
|
|
856
|
+
setCurrentPage(session, capturedPopup.page.pageRef);
|
|
857
|
+
finalPageRef = capturedPopup.page.pageRef;
|
|
858
|
+
currentPageRef = finalPageRef;
|
|
671
859
|
}
|
|
672
|
-
|
|
673
|
-
const
|
|
674
|
-
|
|
675
|
-
|
|
860
|
+
else {
|
|
861
|
+
const syncedPage = await syncSessionPage(session, target.pageRef, page);
|
|
862
|
+
currentPageRef = target.pageRef;
|
|
863
|
+
if (startingPageUrl && syncedPage.url && syncedPage.url !== startingPageUrl) {
|
|
864
|
+
clearProtectedExposure(session, target.pageRef);
|
|
865
|
+
markFillableFormsAbsentForPage(session, target.pageRef);
|
|
866
|
+
}
|
|
867
|
+
const progressProbeForVerification = progressProbe;
|
|
868
|
+
if (progressProbeForVerification) {
|
|
869
|
+
const finalProgressProbe = progressProbeForVerification;
|
|
870
|
+
const acceptance = await waitForAcceptanceProbe(finalProgressProbe);
|
|
871
|
+
const afterPageObservation = acceptance.afterPageObservation;
|
|
872
|
+
const accepted = acceptance.accepted;
|
|
873
|
+
const submitResolution = finalProgressProbe.policy === 'submit'
|
|
874
|
+
? await resolveSubmitResult(finalProgressProbe, afterPageObservation)
|
|
875
|
+
: null;
|
|
876
|
+
if (acceptance.polls > 1) {
|
|
877
|
+
attempts.push(`acceptance.polled:${acceptance.polls}`);
|
|
878
|
+
}
|
|
879
|
+
if (submitResolution?.finalVerdict === 'blocked') {
|
|
880
|
+
attempts.push(`acceptance.failed:${finalProgressProbe.policy}`);
|
|
881
|
+
noProgressObservations = await diagnoseNoObservableProgress(page, finalProgressProbe.locator);
|
|
882
|
+
attempts.push('submit-resolution:blocked');
|
|
883
|
+
throw new Error('validation_blocked');
|
|
884
|
+
}
|
|
885
|
+
if (!accepted) {
|
|
886
|
+
if (finalProgressProbe.policy === 'value-change') {
|
|
887
|
+
attempts.push(`acceptance.failed:${finalProgressProbe.policy}`);
|
|
888
|
+
throw new Error('action_postcondition_failed:value-change');
|
|
889
|
+
}
|
|
890
|
+
if ((finalProgressProbe.policy === 'selection' ||
|
|
891
|
+
finalProgressProbe.policy === 'date-selection') &&
|
|
892
|
+
finalProgressProbe.expectedValue !== null) {
|
|
893
|
+
attempts.push(`acceptance.failed:${finalProgressProbe.policy}`);
|
|
894
|
+
throw new Error(`action_postcondition_failed:${finalProgressProbe.policy}`);
|
|
895
|
+
}
|
|
896
|
+
if (finalProgressProbe.policy === 'submit' && submitResolution) {
|
|
897
|
+
if (submitResolution.acceptAsProgress) {
|
|
898
|
+
attempts.push(`submit-resolution:${submitResolution.finalVerdict}`);
|
|
899
|
+
if (submitResolution.claims.some((claim) => claim.kind === 'soft_result_candidate')) {
|
|
900
|
+
attempts.push('submit-resolution:soft-result-candidate');
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
else {
|
|
904
|
+
attempts.push(`acceptance.failed:${finalProgressProbe.policy}`);
|
|
905
|
+
noProgressObservations = await diagnoseNoObservableProgress(page, finalProgressProbe.locator);
|
|
906
|
+
if (hasValidationBlockedObservations(noProgressObservations)) {
|
|
907
|
+
attempts.push('submit-resolution:blocked');
|
|
908
|
+
throw new Error('validation_blocked');
|
|
909
|
+
}
|
|
910
|
+
attempts.push('no-progress.detected');
|
|
911
|
+
throw new Error('no_observable_progress');
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
else {
|
|
915
|
+
const afterLocatorObservation = finalProgressProbe.trackedStateKeys.length > 0
|
|
916
|
+
? await captureLocatorState(finalProgressProbe.locator, finalProgressProbe.trackedStateKeys)
|
|
917
|
+
: null;
|
|
918
|
+
const afterContextHash = await captureLocatorContextHash(finalProgressProbe.locator);
|
|
919
|
+
const hasComparableSignal = finalProgressProbe.trackedStateKeys.length > 0 ||
|
|
920
|
+
Boolean(finalProgressProbe.beforeContextHash || afterContextHash) ||
|
|
921
|
+
Boolean(finalProgressProbe.beforePage || afterPageObservation);
|
|
922
|
+
const pageProgressChanged = finalProgressProbe.policy === 'generic-click'
|
|
923
|
+
? genericClickObservationChanged(finalProgressProbe.beforePage, afterPageObservation)
|
|
924
|
+
: pageObservationChanged(finalProgressProbe.beforePage, afterPageObservation);
|
|
925
|
+
if (hasComparableSignal &&
|
|
926
|
+
!pageProgressChanged &&
|
|
927
|
+
finalProgressProbe.beforeContextHash === afterContextHash &&
|
|
928
|
+
!locatorStateChanged(finalProgressProbe.beforeLocator, afterLocatorObservation)) {
|
|
929
|
+
attempts.push('no-progress.detected');
|
|
930
|
+
noProgressObservations = await diagnoseNoObservableProgress(page, finalProgressProbe.locator);
|
|
931
|
+
throw new Error('no_observable_progress');
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
else {
|
|
936
|
+
partialProgressResult = await partialProgressForAliasedSelection({
|
|
937
|
+
requestedAction,
|
|
938
|
+
probe: finalProgressProbe,
|
|
939
|
+
});
|
|
940
|
+
}
|
|
676
941
|
}
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
942
|
+
else if (recoveredProgressProbe) {
|
|
943
|
+
partialProgressResult = await partialProgressForAliasedSelection({
|
|
944
|
+
requestedAction,
|
|
945
|
+
probe: recoveredProgressProbe,
|
|
946
|
+
});
|
|
680
947
|
}
|
|
681
948
|
}
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
recoveredAfterError = true;
|
|
690
|
-
recoveredAcceptancePolicy = acceptanceProbe.policy;
|
|
691
|
-
setTargetAvailability(session, targetRef, 'available');
|
|
692
|
-
return true;
|
|
693
|
-
};
|
|
694
|
-
try {
|
|
695
|
-
const valueProjection = await projectActionValue({
|
|
696
|
-
target,
|
|
697
|
-
action,
|
|
698
|
-
actionValue,
|
|
699
|
-
locator: resolvedLocator,
|
|
700
|
-
attempts,
|
|
701
|
-
});
|
|
702
|
-
const executionValue = valueProjection?.executionValue ?? actionValue;
|
|
703
|
-
const acceptanceValue = valueProjection?.acceptanceValue ?? actionValue;
|
|
704
|
-
acceptanceProbe = await createAcceptanceProbe({
|
|
949
|
+
if (resolvedBy === 'playwright-locator') {
|
|
950
|
+
incrementMetric(session, 'deterministicActions');
|
|
951
|
+
}
|
|
952
|
+
bumpPageScopeEpoch(session, target.pageRef);
|
|
953
|
+
recordActionResult(session, true, Date.now() - startedAt);
|
|
954
|
+
await trace.finishSuccess();
|
|
955
|
+
captureDiagnosticSnapshotBestEffort({
|
|
705
956
|
session,
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
actionValue: acceptanceValue,
|
|
710
|
-
locator: resolvedLocator,
|
|
711
|
-
beforePageObservation,
|
|
957
|
+
step: actStep,
|
|
958
|
+
phase: 'after',
|
|
959
|
+
pageRef: finalPageRef,
|
|
712
960
|
});
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
961
|
+
recordCommandLifecycleEventBestEffort({
|
|
962
|
+
step: actStep,
|
|
963
|
+
phase: 'completed',
|
|
964
|
+
attributes: {
|
|
965
|
+
outcomeType: partialProgressResult?.outcomeType ?? 'action_completed',
|
|
966
|
+
targetRef,
|
|
967
|
+
action: requestedAction,
|
|
968
|
+
pageRef: finalPageRef,
|
|
720
969
|
},
|
|
721
970
|
});
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
971
|
+
await finalizeActStepBestEffort(actStep, {
|
|
972
|
+
success: true,
|
|
973
|
+
outcomeType: partialProgressResult?.outcomeType ?? 'action_completed',
|
|
974
|
+
message: partialProgressResult?.message ?? 'The requested action completed successfully.',
|
|
975
|
+
});
|
|
976
|
+
return scrubProtectedExactValues(session, {
|
|
977
|
+
success: true,
|
|
978
|
+
targetRef,
|
|
979
|
+
action: requestedAction,
|
|
980
|
+
...(action !== requestedAction ? { executedAs: action } : {}),
|
|
981
|
+
value: actionValue,
|
|
982
|
+
resolvedBy,
|
|
983
|
+
locatorStrategy,
|
|
984
|
+
pageRef: finalPageRef,
|
|
985
|
+
attempts: sanitizePublicAttempts(attempts),
|
|
986
|
+
popup: Boolean(capturedPopup),
|
|
987
|
+
overlayHandled: attempts.includes('overlay.dismissed'),
|
|
988
|
+
iframe: Boolean(target.framePath?.length),
|
|
989
|
+
jsFallback: attempts.some((attempt) => attempt.startsWith('locator.evaluate.')),
|
|
990
|
+
...(recoveredAfterError
|
|
991
|
+
? {
|
|
992
|
+
recoveredAfterError: true,
|
|
993
|
+
recoveredAcceptancePolicy: recoveredAcceptancePolicy ?? undefined,
|
|
994
|
+
}
|
|
995
|
+
: {}),
|
|
996
|
+
...(partialProgressResult ?? {}),
|
|
997
|
+
durationMs: Date.now() - startedAt,
|
|
998
|
+
metrics: session.runtime?.metrics,
|
|
999
|
+
});
|
|
730
1000
|
}
|
|
731
1001
|
catch (err) {
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
(acceptanceProbe.policy === 'value-change' ||
|
|
735
|
-
acceptanceProbe.policy === 'selection' ||
|
|
736
|
-
acceptanceProbe.policy === 'date-selection' ||
|
|
737
|
-
acceptanceProbe.policy === 'navigation' ||
|
|
738
|
-
acceptanceProbe.policy === 'submit');
|
|
739
|
-
if (shouldAttemptAcceptanceRecovery && (await tryRecoverActionErrorAcceptance())) {
|
|
740
|
-
return true;
|
|
741
|
-
}
|
|
1002
|
+
failureMessage = `Act failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
1003
|
+
recordActionResult(session, false, Date.now() - startedAt);
|
|
742
1004
|
if (staleReason) {
|
|
743
|
-
|
|
744
|
-
}
|
|
745
|
-
try {
|
|
746
|
-
await assertResolvedTargetStillValid(resolvedLocator, `after-error:${strategy}`);
|
|
747
|
-
}
|
|
748
|
-
catch (validationError) {
|
|
749
|
-
if ((acceptanceProbe?.policy === 'navigation' ||
|
|
750
|
-
acceptanceProbe?.policy === 'submit') &&
|
|
751
|
-
validationError instanceof Error &&
|
|
752
|
-
validationError.message === 'stale_target_page_signature_changed' &&
|
|
753
|
-
(await tryRecoverActionErrorAcceptance())) {
|
|
754
|
-
return true;
|
|
755
|
-
}
|
|
756
|
-
throw validationError;
|
|
757
|
-
}
|
|
758
|
-
return false;
|
|
759
|
-
}
|
|
760
|
-
};
|
|
761
|
-
const watchForNewPage = shouldWatchForNewPageAfterAction(target, action);
|
|
762
|
-
const popupPromise = watchForNewPage
|
|
763
|
-
? waitForPopup(page.context())
|
|
764
|
-
: Promise.resolve(null);
|
|
765
|
-
const tryRankedCandidates = async () => {
|
|
766
|
-
const resolution = await resolvePreparedLocatorCandidates({
|
|
767
|
-
target,
|
|
768
|
-
action,
|
|
769
|
-
baseRoot,
|
|
770
|
-
locatorRoot,
|
|
771
|
-
surfaceRoot,
|
|
772
|
-
attempts,
|
|
773
|
-
prepareOptions: {
|
|
774
|
-
allowReadonlyFallback: action === 'fill' && target.controlFamily === 'datepicker',
|
|
775
|
-
allowDescendantPressFallback: action === 'press' &&
|
|
776
|
-
(target.controlFamily === 'text-input' ||
|
|
777
|
-
target.controlFamily === 'select' ||
|
|
778
|
-
target.controlFamily === 'datepicker'),
|
|
779
|
-
isUserActionable: isLocatorUserActionable,
|
|
780
|
-
},
|
|
781
|
-
onPreparedLocator: async (resolvedLocator, strategy) => attemptResolvedLocator(resolvedLocator, strategy),
|
|
782
|
-
});
|
|
783
|
-
if (resolution.sawDisabledTarget) {
|
|
784
|
-
sawDisabledTarget = true;
|
|
785
|
-
}
|
|
786
|
-
if (resolution.sawReadonlyTarget) {
|
|
787
|
-
sawReadonlyTarget = true;
|
|
788
|
-
}
|
|
789
|
-
};
|
|
790
|
-
await tryRankedCandidates();
|
|
791
|
-
if (!resolvedBy && !lastError && deferSurfaceResolution && surface) {
|
|
792
|
-
const deferredSurfaceRoot = await resolveSurfaceScopeRoot(page, surface, attempts);
|
|
793
|
-
if (deferredSurfaceRoot) {
|
|
794
|
-
surfaceRoot = deferredSurfaceRoot;
|
|
795
|
-
locatorRoot = targetUsesSurfaceAsPrimaryLocator(target, surface)
|
|
796
|
-
? baseRoot
|
|
797
|
-
: surfaceRoot;
|
|
798
|
-
await tryRankedCandidates();
|
|
799
|
-
}
|
|
800
|
-
}
|
|
801
|
-
if (!resolvedBy && !lastError && surfaceRoot) {
|
|
802
|
-
const recoveredLocator = await recoverLocatorFromSurfaceRoot(surfaceRoot, target, action, attempts);
|
|
803
|
-
if (recoveredLocator) {
|
|
804
|
-
await attemptResolvedLocator(recoveredLocator, 'surface-descendant', {
|
|
805
|
-
skipDomSignature: true,
|
|
806
|
-
});
|
|
807
|
-
}
|
|
808
|
-
}
|
|
809
|
-
if (!resolvedBy) {
|
|
810
|
-
if (sawDomSignatureMismatch) {
|
|
811
|
-
staleReason = 'dom-signature-mismatch';
|
|
812
|
-
throw new Error('stale_target_dom_signature_changed');
|
|
813
|
-
}
|
|
814
|
-
if (sawDisabledTarget) {
|
|
815
|
-
setTargetAvailability(session, targetRef, 'gated', 'disabled');
|
|
816
|
-
throw new Error('target_disabled');
|
|
817
|
-
}
|
|
818
|
-
if (sawReadonlyTarget && (action === 'fill' || action === 'type')) {
|
|
819
|
-
setTargetAvailability(session, targetRef, 'gated', 'readonly');
|
|
820
|
-
throw new Error('target_readonly');
|
|
821
|
-
}
|
|
822
|
-
if (!lastError &&
|
|
823
|
-
target.surfaceRef &&
|
|
824
|
-
(target.acceptancePolicy === 'selection' ||
|
|
825
|
-
target.acceptancePolicy === 'date-selection')) {
|
|
826
|
-
setTargetAvailability(session, targetRef, 'surface-inactive', 'surface-not-active');
|
|
827
|
-
throw new Error('target_surface_inactive');
|
|
828
|
-
}
|
|
829
|
-
if (!resolvedBy) {
|
|
830
|
-
if (!lastError &&
|
|
831
|
-
(target.controlFamily === 'text-input' ||
|
|
832
|
-
target.controlFamily === 'select' ||
|
|
833
|
-
target.controlFamily === 'datepicker')) {
|
|
834
|
-
staleReason = 'locator-resolution-failed';
|
|
835
|
-
throw new Error('stale_target_locator_resolution_failed');
|
|
1005
|
+
markTargetLifecycle(session, targetRef, 'stale', staleReason);
|
|
836
1006
|
}
|
|
837
|
-
if (
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
const popup = await popupPromise;
|
|
844
|
-
const latePage = !popup && watchForNewPage ? await waitForLatePage(browser, beforePages) : null;
|
|
845
|
-
if (latePage) {
|
|
846
|
-
attempts.push('late-page-captured');
|
|
847
|
-
}
|
|
848
|
-
const discoveredPage = popup ?? latePage;
|
|
849
|
-
const afterPages = discoveredPage ? [...beforePages, discoveredPage] : listPages(browser);
|
|
850
|
-
const capturedPopup = await capturePopupIfOpened(session, beforePages, afterPages, target.pageRef, attempts);
|
|
851
|
-
let finalPageRef = target.pageRef;
|
|
852
|
-
if (capturedPopup) {
|
|
853
|
-
await syncSessionPage(session, capturedPopup.page.pageRef, capturedPopup.popup, {
|
|
854
|
-
settleTimeoutMs: 1_500,
|
|
855
|
-
});
|
|
856
|
-
setCurrentPage(session, capturedPopup.page.pageRef);
|
|
857
|
-
finalPageRef = capturedPopup.page.pageRef;
|
|
858
|
-
currentPageRef = finalPageRef;
|
|
859
|
-
}
|
|
860
|
-
else {
|
|
861
|
-
const syncedPage = await syncSessionPage(session, target.pageRef, page);
|
|
862
|
-
currentPageRef = target.pageRef;
|
|
863
|
-
if (startingPageUrl && syncedPage.url && syncedPage.url !== startingPageUrl) {
|
|
864
|
-
clearProtectedExposure(session, target.pageRef);
|
|
865
|
-
markFillableFormsAbsentForPage(session, target.pageRef);
|
|
866
|
-
}
|
|
867
|
-
const progressProbeForVerification = progressProbe;
|
|
868
|
-
if (progressProbeForVerification) {
|
|
869
|
-
const finalProgressProbe = progressProbeForVerification;
|
|
870
|
-
const acceptance = await waitForAcceptanceProbe(finalProgressProbe);
|
|
871
|
-
const afterPageObservation = acceptance.afterPageObservation;
|
|
872
|
-
const accepted = acceptance.accepted;
|
|
873
|
-
const submitResolution = finalProgressProbe.policy === 'submit'
|
|
874
|
-
? await resolveSubmitResult(finalProgressProbe, afterPageObservation)
|
|
875
|
-
: null;
|
|
876
|
-
if (acceptance.polls > 1) {
|
|
877
|
-
attempts.push(`acceptance.polled:${acceptance.polls}`);
|
|
878
|
-
}
|
|
879
|
-
if (submitResolution?.finalVerdict === 'blocked') {
|
|
880
|
-
attempts.push(`acceptance.failed:${finalProgressProbe.policy}`);
|
|
881
|
-
noProgressObservations = await diagnoseNoObservableProgress(page, finalProgressProbe.locator);
|
|
882
|
-
attempts.push('submit-resolution:blocked');
|
|
883
|
-
throw new Error('validation_blocked');
|
|
884
|
-
}
|
|
885
|
-
if (!accepted) {
|
|
886
|
-
if (finalProgressProbe.policy === 'value-change') {
|
|
887
|
-
attempts.push(`acceptance.failed:${finalProgressProbe.policy}`);
|
|
888
|
-
throw new Error('action_postcondition_failed:value-change');
|
|
889
|
-
}
|
|
890
|
-
if ((finalProgressProbe.policy === 'selection' ||
|
|
891
|
-
finalProgressProbe.policy === 'date-selection') &&
|
|
892
|
-
finalProgressProbe.expectedValue !== null) {
|
|
893
|
-
attempts.push(`acceptance.failed:${finalProgressProbe.policy}`);
|
|
894
|
-
throw new Error(`action_postcondition_failed:${finalProgressProbe.policy}`);
|
|
895
|
-
}
|
|
896
|
-
if (finalProgressProbe.policy === 'submit' && submitResolution) {
|
|
897
|
-
if (submitResolution.acceptAsProgress) {
|
|
898
|
-
attempts.push(`submit-resolution:${submitResolution.finalVerdict}`);
|
|
899
|
-
if (submitResolution.claims.some((claim) => claim.kind === 'soft_result_candidate')) {
|
|
900
|
-
attempts.push('submit-resolution:soft-result-candidate');
|
|
901
|
-
}
|
|
1007
|
+
if (currentPage) {
|
|
1008
|
+
try {
|
|
1009
|
+
const protectedExposure = getProtectedExposure(session, currentPageRef);
|
|
1010
|
+
if (protectedExposure) {
|
|
1011
|
+
await trace.finishSuccess();
|
|
1012
|
+
failureArtifacts = buildProtectedArtifactsSuppressed(protectedExposure);
|
|
902
1013
|
}
|
|
903
1014
|
else {
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
1015
|
+
failureArtifacts = await captureActionFailureArtifacts({
|
|
1016
|
+
page: currentPage,
|
|
1017
|
+
targetRef,
|
|
1018
|
+
action: requestedAction,
|
|
1019
|
+
pageRef: currentPageRef,
|
|
1020
|
+
attempts,
|
|
1021
|
+
locatorStrategy,
|
|
1022
|
+
popup: attempts.includes('popup-captured'),
|
|
1023
|
+
overlayHandled: attempts.includes('overlay.dismissed'),
|
|
1024
|
+
iframe: Boolean(target.framePath?.length),
|
|
1025
|
+
jsFallback: attempts.some((attempt) => attempt.startsWith('locator.evaluate.')),
|
|
1026
|
+
durationMs: Date.now() - startedAt,
|
|
1027
|
+
error: failureMessage,
|
|
1028
|
+
finishTrace: (artifactDir) => trace.finishFailure(artifactDir),
|
|
1029
|
+
});
|
|
912
1030
|
}
|
|
913
1031
|
}
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
? await captureLocatorState(finalProgressProbe.locator, finalProgressProbe.trackedStateKeys)
|
|
917
|
-
: null;
|
|
918
|
-
const afterContextHash = await captureLocatorContextHash(finalProgressProbe.locator);
|
|
919
|
-
const hasComparableSignal = finalProgressProbe.trackedStateKeys.length > 0 ||
|
|
920
|
-
Boolean(finalProgressProbe.beforeContextHash || afterContextHash) ||
|
|
921
|
-
Boolean(finalProgressProbe.beforePage || afterPageObservation);
|
|
922
|
-
const pageProgressChanged = finalProgressProbe.policy === 'generic-click'
|
|
923
|
-
? genericClickObservationChanged(finalProgressProbe.beforePage, afterPageObservation)
|
|
924
|
-
: pageObservationChanged(finalProgressProbe.beforePage, afterPageObservation);
|
|
925
|
-
if (hasComparableSignal &&
|
|
926
|
-
!pageProgressChanged &&
|
|
927
|
-
finalProgressProbe.beforeContextHash === afterContextHash &&
|
|
928
|
-
!locatorStateChanged(finalProgressProbe.beforeLocator, afterLocatorObservation)) {
|
|
929
|
-
attempts.push('no-progress.detected');
|
|
930
|
-
noProgressObservations = await diagnoseNoObservableProgress(page, finalProgressProbe.locator);
|
|
931
|
-
throw new Error('no_observable_progress');
|
|
932
|
-
}
|
|
1032
|
+
catch {
|
|
1033
|
+
// Best effort only. Preserve the original action failure.
|
|
933
1034
|
}
|
|
934
1035
|
}
|
|
935
|
-
else {
|
|
936
|
-
partialProgressResult = await partialProgressForAliasedSelection({
|
|
937
|
-
requestedAction,
|
|
938
|
-
probe: finalProgressProbe,
|
|
939
|
-
});
|
|
940
|
-
}
|
|
941
1036
|
}
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
requestedAction,
|
|
945
|
-
probe: recoveredProgressProbe,
|
|
946
|
-
});
|
|
1037
|
+
if (!failureMessage) {
|
|
1038
|
+
throw new Error('unreachable_action_completion_state');
|
|
947
1039
|
}
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
1040
|
+
const failureContract = describeActFailure({
|
|
1041
|
+
failureMessage,
|
|
1042
|
+
staleReason,
|
|
1043
|
+
});
|
|
1044
|
+
const outputObservations = hasMeaningfulNoObservableProgressObservations(noProgressObservations)
|
|
1045
|
+
? noProgressObservations
|
|
1046
|
+
: undefined;
|
|
1047
|
+
const artifactManifestId = await persistActArtifactManifestBestEffort(runId, actStep, actStep?.stepId, failureArtifacts ?? null);
|
|
1048
|
+
captureDiagnosticSnapshotBestEffort({
|
|
1049
|
+
session,
|
|
1050
|
+
step: actStep,
|
|
1051
|
+
phase: 'point-in-time',
|
|
1052
|
+
pageRef: currentPageRef,
|
|
1053
|
+
artifactRefs: buildActSnapshotArtifactRefs(failureArtifacts ?? null),
|
|
1054
|
+
});
|
|
1055
|
+
recordCommandLifecycleEventBestEffort({
|
|
1056
|
+
step: actStep,
|
|
1057
|
+
phase: 'failed',
|
|
1058
|
+
attributes: {
|
|
1059
|
+
outcomeType: failureContract.outcomeType,
|
|
1060
|
+
targetRef,
|
|
1061
|
+
action: requestedAction,
|
|
1062
|
+
pageRef: currentPageRef,
|
|
1063
|
+
...(artifactManifestId ? { artifactManifestId } : {}),
|
|
1064
|
+
},
|
|
1065
|
+
});
|
|
1066
|
+
await finalizeActStepBestEffort(actStep, {
|
|
1067
|
+
success: false,
|
|
1068
|
+
outcomeType: failureContract.outcomeType,
|
|
1069
|
+
message: failureContract.message,
|
|
1070
|
+
reason: failureContract.reason,
|
|
1071
|
+
artifactManifestId,
|
|
1072
|
+
});
|
|
1073
|
+
return scrubProtectedExactValues(session, {
|
|
1074
|
+
success: false,
|
|
1075
|
+
failureSurface: 'output',
|
|
1076
|
+
error: failureContract.error,
|
|
1077
|
+
outcomeType: failureContract.outcomeType,
|
|
1078
|
+
message: failureContract.message,
|
|
1079
|
+
reason: failureContract.reason,
|
|
966
1080
|
targetRef,
|
|
967
1081
|
action: requestedAction,
|
|
968
|
-
|
|
969
|
-
|
|
1082
|
+
...(action !== requestedAction ? { executedAs: action } : {}),
|
|
1083
|
+
value: actionValue,
|
|
1084
|
+
pageRef: currentPageRef,
|
|
1085
|
+
locatorStrategy,
|
|
1086
|
+
attempts: sanitizePublicAttempts(attempts),
|
|
1087
|
+
popup: attempts.includes('popup-captured'),
|
|
1088
|
+
overlayHandled: attempts.includes('overlay.dismissed'),
|
|
1089
|
+
iframe: Boolean(target.framePath?.length),
|
|
1090
|
+
jsFallback: attempts.some((attempt) => attempt.startsWith('locator.evaluate.')),
|
|
1091
|
+
durationMs: Date.now() - startedAt,
|
|
1092
|
+
staleTarget: Boolean(staleReason),
|
|
1093
|
+
observations: outputObservations,
|
|
1094
|
+
artifacts: failureArtifacts,
|
|
1095
|
+
metrics: session.runtime?.metrics,
|
|
1096
|
+
});
|
|
970
1097
|
});
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
1098
|
+
}
|
|
1099
|
+
catch (err) {
|
|
1100
|
+
const browserConnectionFailure = describeBrowserConnectionFailure(err, {
|
|
1101
|
+
defaultMessage: 'The action could not start because AgentBrowse failed to connect to the browser.',
|
|
1102
|
+
unrecoverableSessionMessage: 'The action could not start because the previous browser session is no longer reachable.',
|
|
975
1103
|
});
|
|
976
|
-
return
|
|
977
|
-
|
|
1104
|
+
return buildActPreflightFailureResult({
|
|
1105
|
+
session,
|
|
1106
|
+
step: actStep,
|
|
1107
|
+
error: 'browser_connection_failed',
|
|
1108
|
+
outcomeType: 'blocked',
|
|
1109
|
+
message: browserConnectionFailure.message,
|
|
1110
|
+
reason: browserConnectionFailure.reason,
|
|
978
1111
|
targetRef,
|
|
979
|
-
action
|
|
980
|
-
...(action !== requestedAction ? { executedAs: action } : {}),
|
|
981
|
-
value: actionValue,
|
|
982
|
-
resolvedBy,
|
|
983
|
-
locatorStrategy,
|
|
984
|
-
pageRef: finalPageRef,
|
|
985
|
-
attempts: sanitizePublicAttempts(attempts),
|
|
986
|
-
popup: Boolean(capturedPopup),
|
|
987
|
-
overlayHandled: attempts.includes('overlay.dismissed'),
|
|
988
|
-
iframe: Boolean(target.framePath?.length),
|
|
989
|
-
jsFallback: attempts.some((attempt) => attempt.startsWith('locator.evaluate.')),
|
|
990
|
-
...(recoveredAfterError
|
|
991
|
-
? {
|
|
992
|
-
recoveredAfterError: true,
|
|
993
|
-
recoveredAcceptancePolicy: recoveredAcceptancePolicy ?? undefined,
|
|
994
|
-
}
|
|
995
|
-
: {}),
|
|
996
|
-
...(partialProgressResult ?? {}),
|
|
997
|
-
durationMs: Date.now() - startedAt,
|
|
998
|
-
metrics: session.runtime?.metrics,
|
|
1112
|
+
action,
|
|
999
1113
|
});
|
|
1000
1114
|
}
|
|
1001
|
-
catch (err) {
|
|
1002
|
-
failureMessage = `Act failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
1003
|
-
recordActionResult(session, false, Date.now() - startedAt);
|
|
1004
|
-
if (staleReason) {
|
|
1005
|
-
markTargetLifecycle(session, targetRef, 'stale', staleReason);
|
|
1006
|
-
}
|
|
1007
|
-
if (currentPage) {
|
|
1008
|
-
try {
|
|
1009
|
-
const protectedExposure = getProtectedExposure(session, currentPageRef);
|
|
1010
|
-
if (protectedExposure) {
|
|
1011
|
-
await trace.finishSuccess();
|
|
1012
|
-
failureArtifacts = buildProtectedArtifactsSuppressed(protectedExposure);
|
|
1013
|
-
}
|
|
1014
|
-
else {
|
|
1015
|
-
failureArtifacts = await captureActionFailureArtifacts({
|
|
1016
|
-
page: currentPage,
|
|
1017
|
-
targetRef,
|
|
1018
|
-
action: requestedAction,
|
|
1019
|
-
pageRef: currentPageRef,
|
|
1020
|
-
attempts,
|
|
1021
|
-
locatorStrategy,
|
|
1022
|
-
popup: attempts.includes('popup-captured'),
|
|
1023
|
-
overlayHandled: attempts.includes('overlay.dismissed'),
|
|
1024
|
-
iframe: Boolean(target.framePath?.length),
|
|
1025
|
-
jsFallback: attempts.some((attempt) => attempt.startsWith('locator.evaluate.')),
|
|
1026
|
-
durationMs: Date.now() - startedAt,
|
|
1027
|
-
error: failureMessage,
|
|
1028
|
-
finishTrace: (artifactDir) => trace.finishFailure(artifactDir),
|
|
1029
|
-
});
|
|
1030
|
-
}
|
|
1031
|
-
}
|
|
1032
|
-
catch {
|
|
1033
|
-
// Best effort only. Preserve the original action failure.
|
|
1034
|
-
}
|
|
1035
|
-
}
|
|
1036
|
-
}
|
|
1037
|
-
finally {
|
|
1038
|
-
if (browser) {
|
|
1039
|
-
await disconnectPlaywright(browser);
|
|
1040
|
-
}
|
|
1041
|
-
}
|
|
1042
|
-
if (!failureMessage) {
|
|
1043
|
-
throw new Error('unreachable_action_completion_state');
|
|
1044
|
-
}
|
|
1045
|
-
const failureContract = describeActFailure({
|
|
1046
|
-
failureMessage,
|
|
1047
|
-
staleReason,
|
|
1048
|
-
});
|
|
1049
|
-
const outputObservations = hasMeaningfulNoObservableProgressObservations(noProgressObservations)
|
|
1050
|
-
? noProgressObservations
|
|
1051
|
-
: undefined;
|
|
1052
|
-
const artifactManifestId = await persistActArtifactManifestBestEffort(runId, actStep, actStep?.stepId, failureArtifacts ?? null);
|
|
1053
|
-
captureDiagnosticSnapshotBestEffort({
|
|
1054
|
-
session,
|
|
1055
|
-
step: actStep,
|
|
1056
|
-
phase: 'point-in-time',
|
|
1057
|
-
pageRef: currentPageRef,
|
|
1058
|
-
artifactRefs: buildActSnapshotArtifactRefs(failureArtifacts ?? null),
|
|
1059
|
-
});
|
|
1060
|
-
recordCommandLifecycleEventBestEffort({
|
|
1061
|
-
step: actStep,
|
|
1062
|
-
phase: 'failed',
|
|
1063
|
-
attributes: {
|
|
1064
|
-
outcomeType: failureContract.outcomeType,
|
|
1065
|
-
targetRef,
|
|
1066
|
-
action: requestedAction,
|
|
1067
|
-
pageRef: currentPageRef,
|
|
1068
|
-
...(artifactManifestId ? { artifactManifestId } : {}),
|
|
1069
|
-
},
|
|
1070
|
-
});
|
|
1071
|
-
await finalizeActStepBestEffort(actStep, {
|
|
1072
|
-
success: false,
|
|
1073
|
-
outcomeType: failureContract.outcomeType,
|
|
1074
|
-
message: failureContract.message,
|
|
1075
|
-
reason: failureContract.reason,
|
|
1076
|
-
artifactManifestId,
|
|
1077
|
-
});
|
|
1078
|
-
return scrubProtectedExactValues(session, {
|
|
1079
|
-
success: false,
|
|
1080
|
-
failureSurface: 'output',
|
|
1081
|
-
error: failureContract.error,
|
|
1082
|
-
outcomeType: failureContract.outcomeType,
|
|
1083
|
-
message: failureContract.message,
|
|
1084
|
-
reason: failureContract.reason,
|
|
1085
|
-
targetRef,
|
|
1086
|
-
action: requestedAction,
|
|
1087
|
-
...(action !== requestedAction ? { executedAs: action } : {}),
|
|
1088
|
-
value: actionValue,
|
|
1089
|
-
pageRef: currentPageRef,
|
|
1090
|
-
locatorStrategy,
|
|
1091
|
-
attempts: sanitizePublicAttempts(attempts),
|
|
1092
|
-
popup: attempts.includes('popup-captured'),
|
|
1093
|
-
overlayHandled: attempts.includes('overlay.dismissed'),
|
|
1094
|
-
iframe: Boolean(target.framePath?.length),
|
|
1095
|
-
jsFallback: attempts.some((attempt) => attempt.startsWith('locator.evaluate.')),
|
|
1096
|
-
durationMs: Date.now() - startedAt,
|
|
1097
|
-
staleTarget: Boolean(staleReason),
|
|
1098
|
-
observations: outputObservations,
|
|
1099
|
-
artifacts: failureArtifacts,
|
|
1100
|
-
metrics: session.runtime?.metrics,
|
|
1101
|
-
});
|
|
1102
1115
|
});
|
|
1103
1116
|
}
|
|
1104
1117
|
export async function act(session, targetRef, action, value) {
|