@nuanu-ai/agentbrowse 0.2.46 → 0.2.48
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 +69 -10
- package/dist/agentpay-gateway.d.ts +9 -0
- package/dist/agentpay-gateway.d.ts.map +1 -1
- package/dist/agentpay-gateway.js +30 -0
- package/dist/agentpay-stagehand-llm.d.ts.map +1 -1
- package/dist/agentpay-stagehand-llm.js +9 -1
- package/dist/command-api-tracing.d.ts +19 -0
- package/dist/command-api-tracing.d.ts.map +1 -0
- package/dist/command-api-tracing.js +137 -0
- package/dist/commands/act.d.ts.map +1 -1
- package/dist/commands/act.js +822 -670
- package/dist/commands/act.test-harness.d.ts +6 -0
- package/dist/commands/act.test-harness.d.ts.map +1 -1
- package/dist/commands/act.test-harness.js +44 -1
- package/dist/commands/action-acceptance.d.ts.map +1 -1
- package/dist/commands/action-acceptance.js +115 -0
- package/dist/commands/captcha-solve.d.ts.map +1 -1
- package/dist/commands/captcha-solve.js +83 -16
- package/dist/commands/click-action-executor.d.ts +0 -1
- package/dist/commands/click-action-executor.d.ts.map +1 -1
- package/dist/commands/click-action-executor.js +31 -77
- package/dist/commands/close.d.ts +3 -3
- package/dist/commands/close.d.ts.map +1 -1
- package/dist/commands/close.js +178 -0
- package/dist/commands/descriptor-validation.d.ts.map +1 -1
- package/dist/commands/descriptor-validation.js +75 -57
- package/dist/commands/end-session.d.ts +25 -0
- package/dist/commands/end-session.d.ts.map +1 -0
- package/dist/commands/end-session.js +161 -0
- package/dist/commands/extract-stagehand-executor.js +1 -1
- package/dist/commands/extract.d.ts.map +1 -1
- package/dist/commands/extract.js +339 -202
- package/dist/commands/fill-secret.d.ts +3 -3
- package/dist/commands/fill-secret.d.ts.map +1 -1
- package/dist/commands/fill-secret.js +419 -234
- package/dist/commands/get-secrets-catalog.d.ts.map +1 -1
- package/dist/commands/get-secrets-catalog.js +66 -5
- package/dist/commands/interaction-kernel.d.ts +46 -0
- package/dist/commands/interaction-kernel.d.ts.map +1 -0
- package/dist/commands/interaction-kernel.js +215 -0
- package/dist/commands/launch.d.ts +0 -2
- package/dist/commands/launch.d.ts.map +1 -1
- package/dist/commands/launch.js +109 -17
- package/dist/commands/navigate.d.ts.map +1 -1
- package/dist/commands/navigate.js +188 -45
- package/dist/commands/observe-accessibility.d.ts.map +1 -1
- package/dist/commands/observe-accessibility.js +50 -39
- package/dist/commands/observe-dom-label-contract.d.ts.map +1 -1
- package/dist/commands/observe-dom-label-contract.js +5 -0
- package/dist/commands/observe-inventory.d.ts +13 -0
- package/dist/commands/observe-inventory.d.ts.map +1 -1
- package/dist/commands/observe-inventory.js +320 -65
- package/dist/commands/observe-persistence.d.ts.map +1 -1
- package/dist/commands/observe-persistence.js +3 -0
- package/dist/commands/observe-projection.d.ts +1 -0
- package/dist/commands/observe-projection.d.ts.map +1 -1
- package/dist/commands/observe-projection.js +7 -2
- package/dist/commands/observe-protected.d.ts +1 -0
- package/dist/commands/observe-protected.d.ts.map +1 -1
- package/dist/commands/observe-protected.js +9 -4
- package/dist/commands/observe-semantics.d.ts.map +1 -1
- package/dist/commands/observe-semantics.js +5 -2
- package/dist/commands/observe-stagehand.d.ts +1 -0
- package/dist/commands/observe-stagehand.d.ts.map +1 -1
- package/dist/commands/observe-stagehand.js +2 -0
- package/dist/commands/observe.d.ts +2 -0
- package/dist/commands/observe.d.ts.map +1 -1
- package/dist/commands/observe.js +387 -203
- package/dist/commands/observe.test-harness.d.ts +8 -0
- package/dist/commands/observe.test-harness.d.ts.map +1 -1
- package/dist/commands/observe.test-harness.js +48 -1
- package/dist/commands/poll-secret.d.ts +6 -0
- package/dist/commands/poll-secret.d.ts.map +1 -0
- package/dist/commands/poll-secret.js +159 -0
- package/dist/commands/request-secret.d.ts +6 -0
- package/dist/commands/request-secret.d.ts.map +1 -0
- package/dist/commands/request-secret.js +284 -0
- package/dist/commands/screenshot.d.ts.map +1 -1
- package/dist/commands/screenshot.js +172 -7
- package/dist/commands/select-action-executor.d.ts.map +1 -1
- package/dist/commands/semantic-observe.d.ts +4 -0
- package/dist/commands/semantic-observe.d.ts.map +1 -1
- package/dist/commands/semantic-observe.js +388 -17
- package/dist/commands/start-session.d.ts +31 -0
- package/dist/commands/start-session.d.ts.map +1 -0
- package/dist/commands/start-session.js +347 -0
- package/dist/commands/status.d.ts +2 -1
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +166 -144
- package/dist/control-semantics.d.ts +1 -0
- package/dist/control-semantics.d.ts.map +1 -1
- package/dist/control-semantics.js +51 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +144 -45
- package/dist/otel-exporter.d.ts +58 -0
- package/dist/otel-exporter.d.ts.map +1 -0
- package/dist/otel-exporter.js +263 -0
- package/dist/otel-projector.d.ts +75 -0
- package/dist/otel-projector.d.ts.map +1 -0
- package/dist/otel-projector.js +409 -0
- package/dist/owned-browser.d.ts +1 -1
- package/dist/owned-browser.d.ts.map +1 -1
- package/dist/owned-browser.js +13 -1
- package/dist/owned-process.d.ts +2 -0
- package/dist/owned-process.d.ts.map +1 -1
- package/dist/owned-process.js +7 -3
- package/dist/playwright-runtime.d.ts +1 -1
- package/dist/playwright-runtime.d.ts.map +1 -1
- package/dist/playwright-runtime.js +8 -8
- package/dist/run-observability.d.ts +25 -0
- package/dist/run-observability.d.ts.map +1 -0
- package/dist/run-observability.js +115 -0
- package/dist/run-store.d.ts +274 -0
- package/dist/run-store.d.ts.map +1 -0
- package/dist/run-store.js +631 -0
- package/dist/runtime-metrics.d.ts +27 -0
- package/dist/runtime-metrics.d.ts.map +1 -0
- package/dist/runtime-metrics.js +66 -0
- package/dist/runtime-page-state.d.ts +11 -0
- package/dist/runtime-page-state.d.ts.map +1 -0
- package/dist/runtime-page-state.js +62 -0
- package/dist/runtime-protected-state.d.ts +16 -0
- package/dist/runtime-protected-state.d.ts.map +1 -0
- package/dist/runtime-protected-state.js +157 -0
- package/dist/runtime-state.d.ts +10 -44
- package/dist/runtime-state.d.ts.map +1 -1
- package/dist/runtime-state.js +57 -222
- package/dist/secrets/backend.d.ts +65 -16
- package/dist/secrets/backend.d.ts.map +1 -1
- package/dist/secrets/backend.js +135 -95
- package/dist/secrets/catalog-sync.d.ts.map +1 -1
- package/dist/secrets/catalog-sync.js +4 -1
- package/dist/secrets/form-matcher.d.ts +5 -5
- package/dist/secrets/form-matcher.d.ts.map +1 -1
- package/dist/secrets/form-matcher.js +292 -164
- package/dist/secrets/intent-output.d.ts +6 -10
- package/dist/secrets/intent-output.d.ts.map +1 -1
- package/dist/secrets/intent-output.js +4 -58
- package/dist/secrets/mock-agentpay-cabinet.d.ts +38 -27
- package/dist/secrets/mock-agentpay-cabinet.d.ts.map +1 -1
- package/dist/secrets/mock-agentpay-cabinet.js +177 -111
- package/dist/secrets/protected-artifact-guard.d.ts +2 -2
- package/dist/secrets/protected-artifact-guard.d.ts.map +1 -1
- package/dist/secrets/protected-artifact-guard.js +2 -2
- package/dist/secrets/protected-bindings.d.ts +1 -1
- package/dist/secrets/protected-bindings.d.ts.map +1 -1
- package/dist/secrets/protected-bindings.js +6 -0
- package/dist/secrets/protected-field-semantics.d.ts +9 -0
- package/dist/secrets/protected-field-semantics.d.ts.map +1 -0
- package/dist/secrets/protected-field-semantics.js +154 -0
- package/dist/secrets/protected-field-values.d.ts.map +1 -1
- package/dist/secrets/protected-field-values.js +3 -3
- package/dist/secrets/protected-fill.d.ts +1 -1
- package/dist/secrets/protected-fill.d.ts.map +1 -1
- package/dist/secrets/protected-fill.js +45 -149
- package/dist/secrets/protected-value-adapters.d.ts +2 -1
- package/dist/secrets/protected-value-adapters.d.ts.map +1 -1
- package/dist/secrets/protected-value-adapters.js +80 -1
- package/dist/secrets/request-output.d.ts +11 -0
- package/dist/secrets/request-output.d.ts.map +1 -0
- package/dist/secrets/request-output.js +75 -0
- package/dist/secrets/types.d.ts +15 -9
- package/dist/secrets/types.d.ts.map +1 -1
- package/dist/secrets/types.js +3 -0
- package/dist/session-event-exporter.d.ts +36 -0
- package/dist/session-event-exporter.d.ts.map +1 -0
- package/dist/session-event-exporter.js +428 -0
- package/dist/session.d.ts +16 -7
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +150 -23
- package/dist/sessions-backend.d.ts +354 -0
- package/dist/sessions-backend.d.ts.map +1 -0
- package/dist/sessions-backend.js +126 -0
- package/dist/solver/browser-launcher.d.ts +1 -1
- package/dist/solver/browser-launcher.d.ts.map +1 -1
- package/dist/solver/browser-launcher.js +39 -13
- package/dist/solver/captcha-solver.d.ts.map +1 -1
- package/dist/solver/captcha-solver.js +8 -1
- package/dist/solver/types.d.ts +1 -0
- package/dist/solver/types.d.ts.map +1 -1
- package/dist/workflow-session-completion.d.ts +33 -0
- package/dist/workflow-session-completion.d.ts.map +1 -0
- package/dist/workflow-session-completion.js +156 -0
- package/package.json +9 -1
- package/dist/commands/create-intent.d.ts +0 -6
- package/dist/commands/create-intent.d.ts.map +0 -1
- package/dist/commands/create-intent.js +0 -75
- package/dist/commands/poll-intent.d.ts +0 -6
- package/dist/commands/poll-intent.d.ts.map +0 -1
- package/dist/commands/poll-intent.js +0 -57
package/dist/commands/act.js
CHANGED
|
@@ -2,21 +2,28 @@
|
|
|
2
2
|
* browse act <targetRef> <action> [value] — Perform a deterministic action on a stored target.
|
|
3
3
|
*/
|
|
4
4
|
import { saveSession } from '../session.js';
|
|
5
|
-
import {
|
|
5
|
+
import { getSurface, getTarget, markTargetLifecycle, setTargetAvailability, updateTarget, } from '../runtime-state.js';
|
|
6
|
+
import { incrementMetric, recordActionResult } from '../runtime-metrics.js';
|
|
7
|
+
import { bumpPageScopeEpoch, registerPage, setCurrentPage } from '../runtime-page-state.js';
|
|
8
|
+
import { clearProtectedExposure, getProtectedExposure } from '../runtime-protected-state.js';
|
|
6
9
|
import { outputContractFailure, outputFailure, outputJSON } from '../output.js';
|
|
7
|
-
import { capturePageObservation, captureLocatorContextHash, captureLocatorState, createAcceptanceProbe, diagnoseNoObservableProgress, genericClickObservationChanged, locatorStateChanged, pageObservationChanged,
|
|
10
|
+
import { capturePageObservation, captureLocatorContextHash, captureLocatorState, createAcceptanceProbe, diagnoseNoObservableProgress, genericClickObservationChanged, locatorStateChanged, pageObservationChanged, shouldVerifyObservableProgress, waitForAcceptanceProbe, } from './action-acceptance.js';
|
|
8
11
|
import { captureActionFailureArtifacts, startActionTrace } from './action-artifacts.js';
|
|
9
|
-
import { buildLocator, resolveLocatorRoot } from './action-fallbacks.js';
|
|
10
12
|
import { clickActivationStrategyForTarget } from './click-activation-policy.js';
|
|
11
13
|
import { resolveSubmitResult } from './action-result-resolution.js';
|
|
12
14
|
import { projectActionValue } from './action-value-projection.js';
|
|
13
15
|
import { applyActionWithFallbacks } from './action-executor.js';
|
|
14
16
|
import { BROWSE_ACTIONS, isBrowseAction } from './browse-actions.js';
|
|
15
17
|
import { isCompatibleMutableFieldBinding, normalizePageSignature, readLocatorBindingSnapshot, readLocatorDomSignature, } from './descriptor-validation.js';
|
|
18
|
+
import { assertStoredBindingStillValid, resolvePreparedLocatorCandidates, resolveInteractionRoots, targetUsesSurfaceAsPrimaryLocator, } from './interaction-kernel.js';
|
|
16
19
|
import { isLocatorUserActionable } from './user-actionable.js';
|
|
17
20
|
import { resolveSurfaceScopeRoot } from './target-resolution.js';
|
|
18
21
|
import { buildProtectedArtifactsSuppressed } from '../secrets/protected-artifact-guard.js';
|
|
19
22
|
import { connectPlaywright, disconnectPlaywright, listPages, resolvePageByRef, syncSessionPage, } from '../playwright-runtime.js';
|
|
23
|
+
import { finishRunStep, saveArtifactManifest, startRunStep } from '../run-store.js';
|
|
24
|
+
import { appendCommandLifecycleEventBestEffort, captureStepSnapshotBestEffort, } from '../run-observability.js';
|
|
25
|
+
import { withApiTraceContext } from '../command-api-tracing.js';
|
|
26
|
+
import { exportRunStepToOtlpHttpJsonBestEffort } from '../otel-exporter.js';
|
|
20
27
|
function ensureValue(action, value) {
|
|
21
28
|
if (action === 'click')
|
|
22
29
|
return undefined;
|
|
@@ -25,7 +32,65 @@ function ensureValue(action, value) {
|
|
|
25
32
|
throw new Error(`Act value is required for action: ${action}`);
|
|
26
33
|
}
|
|
27
34
|
const MUTABLE_FIELD_REBIND_RETRY_DELAYS_MS = [0, 25, 50, 100];
|
|
28
|
-
|
|
35
|
+
const PARTIAL_SELECTION_PROGRESS_MESSAGE = 'Text entered. Selection is not complete. Choose one option from the visible list.';
|
|
36
|
+
async function readExpandedState(locator) {
|
|
37
|
+
if (!locator) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
const state = await captureLocatorState(locator, ['expanded']).catch(() => null);
|
|
41
|
+
return typeof state?.expanded === 'boolean' ? state.expanded : null;
|
|
42
|
+
}
|
|
43
|
+
async function partialProgressForAliasedSelection(args) {
|
|
44
|
+
const { requestedAction, probe } = args;
|
|
45
|
+
if ((requestedAction !== 'fill' && requestedAction !== 'type') || probe?.policy !== 'selection') {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
const readExpanded = await readExpandedState(probe.readLocator);
|
|
49
|
+
if (readExpanded === true) {
|
|
50
|
+
return {
|
|
51
|
+
outcomeType: 'partial_progress',
|
|
52
|
+
message: PARTIAL_SELECTION_PROGRESS_MESSAGE,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
const fallbackExpanded = probe.readLocator === probe.locator ? readExpanded : await readExpandedState(probe.locator);
|
|
56
|
+
if (fallbackExpanded === true) {
|
|
57
|
+
return {
|
|
58
|
+
outcomeType: 'partial_progress',
|
|
59
|
+
message: PARTIAL_SELECTION_PROGRESS_MESSAGE,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
async function emitActPreflightFailure(params) {
|
|
65
|
+
finalizeActStepBestEffort(params.runId, params.stepId, {
|
|
66
|
+
success: false,
|
|
67
|
+
outcomeType: params.outcomeType,
|
|
68
|
+
message: params.message,
|
|
69
|
+
reason: params.reason,
|
|
70
|
+
});
|
|
71
|
+
const step = params.runId && params.stepId
|
|
72
|
+
? {
|
|
73
|
+
runId: params.runId,
|
|
74
|
+
stepId: params.stepId,
|
|
75
|
+
command: 'act',
|
|
76
|
+
}
|
|
77
|
+
: null;
|
|
78
|
+
captureStepSnapshotBestEffort({
|
|
79
|
+
session: params.session,
|
|
80
|
+
step,
|
|
81
|
+
phase: 'point-in-time',
|
|
82
|
+
pageRef: params.session.runtime?.currentPageRef,
|
|
83
|
+
});
|
|
84
|
+
appendCommandLifecycleEventBestEffort({
|
|
85
|
+
step,
|
|
86
|
+
phase: 'failed',
|
|
87
|
+
attributes: {
|
|
88
|
+
outcomeType: params.outcomeType,
|
|
89
|
+
targetRef: params.targetRef,
|
|
90
|
+
reason: params.reason,
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
await exportRunStepToOtlpHttpJsonBestEffort(params.runId, params.stepId);
|
|
29
94
|
return outputContractFailure({
|
|
30
95
|
error: params.error,
|
|
31
96
|
outcomeType: params.outcomeType,
|
|
@@ -120,6 +185,81 @@ function redactNoObservableProgressDiagnosis(diagnosis) {
|
|
|
120
185
|
: {}),
|
|
121
186
|
};
|
|
122
187
|
}
|
|
188
|
+
function buildActArtifactManifest(params) {
|
|
189
|
+
if ('suppressed' in params.artifacts) {
|
|
190
|
+
return {
|
|
191
|
+
artifactManifestId: `${params.stepId}-artifacts`,
|
|
192
|
+
stepId: params.stepId,
|
|
193
|
+
screenshots: [],
|
|
194
|
+
htmlSnapshots: [],
|
|
195
|
+
traces: [],
|
|
196
|
+
logs: [],
|
|
197
|
+
suppressed: [
|
|
198
|
+
{ kind: 'screenshot', reason: 'protected_exposure_active' },
|
|
199
|
+
{ kind: 'html', reason: 'protected_exposure_active' },
|
|
200
|
+
{ kind: 'trace', reason: 'protected_exposure_active' },
|
|
201
|
+
{ kind: 'log', reason: 'protected_exposure_active' },
|
|
202
|
+
],
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
return {
|
|
206
|
+
artifactManifestId: `${params.stepId}-artifacts`,
|
|
207
|
+
stepId: params.stepId,
|
|
208
|
+
screenshots: params.artifacts.screenshotPath
|
|
209
|
+
? [{ path: params.artifacts.screenshotPath, purpose: 'failure_screenshot' }]
|
|
210
|
+
: [],
|
|
211
|
+
htmlSnapshots: params.artifacts.htmlPath
|
|
212
|
+
? [{ path: params.artifacts.htmlPath, purpose: 'failure_html_snapshot' }]
|
|
213
|
+
: [],
|
|
214
|
+
traces: params.artifacts.tracePath
|
|
215
|
+
? [{ path: params.artifacts.tracePath, purpose: 'failure_trace' }]
|
|
216
|
+
: [],
|
|
217
|
+
logs: [{ path: params.artifacts.actionLogPath, purpose: 'failure_action_log' }],
|
|
218
|
+
suppressed: [],
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
function persistActArtifactManifestBestEffort(runId, stepId, artifacts) {
|
|
222
|
+
if (!runId || !stepId || !artifacts) {
|
|
223
|
+
return undefined;
|
|
224
|
+
}
|
|
225
|
+
try {
|
|
226
|
+
const manifest = buildActArtifactManifest({
|
|
227
|
+
stepId,
|
|
228
|
+
artifacts,
|
|
229
|
+
});
|
|
230
|
+
saveArtifactManifest(runId, manifest);
|
|
231
|
+
return manifest.artifactManifestId;
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
return undefined;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
function buildActSnapshotArtifactRefs(artifacts) {
|
|
238
|
+
if (!artifacts || 'suppressed' in artifacts) {
|
|
239
|
+
return undefined;
|
|
240
|
+
}
|
|
241
|
+
return {
|
|
242
|
+
...(artifacts.screenshotPath ? { screenshotPath: artifacts.screenshotPath } : {}),
|
|
243
|
+
...(artifacts.htmlPath ? { htmlPath: artifacts.htmlPath } : {}),
|
|
244
|
+
...(artifacts.tracePath ? { tracePath: artifacts.tracePath } : {}),
|
|
245
|
+
...(artifacts.actionLogPath ? { logPath: artifacts.actionLogPath } : {}),
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
function finalizeActStepBestEffort(runId, stepId, options) {
|
|
249
|
+
if (!runId || !stepId) {
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
try {
|
|
253
|
+
finishRunStep({
|
|
254
|
+
runId,
|
|
255
|
+
stepId,
|
|
256
|
+
...options,
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
catch {
|
|
260
|
+
// Best effort only.
|
|
261
|
+
}
|
|
262
|
+
}
|
|
123
263
|
function isEditableLikeTarget(target) {
|
|
124
264
|
if (target.controlFamily === 'text-input' ||
|
|
125
265
|
target.controlFamily === 'select' ||
|
|
@@ -148,136 +288,6 @@ function shouldDeferSurfaceResolutionForEditablePress(target, action) {
|
|
|
148
288
|
isEditableLikeTarget(target) &&
|
|
149
289
|
!target.locatorCandidates.some((candidate) => candidate.scope === 'surface'));
|
|
150
290
|
}
|
|
151
|
-
function targetUsesSurfaceAsPrimaryLocator(target, surface) {
|
|
152
|
-
const surfaceCandidates = new Set(surface.locatorCandidates.map(locatorCandidateKey));
|
|
153
|
-
return target.locatorCandidates.some((candidate) => surfaceCandidates.has(locatorCandidateKey(candidate)));
|
|
154
|
-
}
|
|
155
|
-
async function resolveActionRoot(page, target, attempts, surface) {
|
|
156
|
-
const baseRoot = resolveLocatorRoot(page, target.framePath ?? surface?.framePath);
|
|
157
|
-
if (!surface) {
|
|
158
|
-
return {
|
|
159
|
-
baseRoot,
|
|
160
|
-
locatorRoot: baseRoot,
|
|
161
|
-
surfaceRoot: null,
|
|
162
|
-
};
|
|
163
|
-
}
|
|
164
|
-
const surfaceRoot = await resolveSurfaceScopeRoot(page, surface, attempts);
|
|
165
|
-
if (surfaceRoot) {
|
|
166
|
-
if (targetUsesSurfaceAsPrimaryLocator(target, surface)) {
|
|
167
|
-
attempts.push('surface.resolve.self-target');
|
|
168
|
-
return {
|
|
169
|
-
baseRoot,
|
|
170
|
-
locatorRoot: baseRoot,
|
|
171
|
-
surfaceRoot,
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
return {
|
|
175
|
-
baseRoot,
|
|
176
|
-
locatorRoot: surfaceRoot,
|
|
177
|
-
surfaceRoot,
|
|
178
|
-
};
|
|
179
|
-
}
|
|
180
|
-
attempts.push('surface.resolve.fallback:page');
|
|
181
|
-
return {
|
|
182
|
-
baseRoot,
|
|
183
|
-
locatorRoot: baseRoot,
|
|
184
|
-
surfaceRoot: null,
|
|
185
|
-
};
|
|
186
|
-
}
|
|
187
|
-
function resolveLocatorRootForCandidate(baseRoot, defaultRoot, surfaceRoot, candidate) {
|
|
188
|
-
if (candidate.scope === 'root') {
|
|
189
|
-
return baseRoot;
|
|
190
|
-
}
|
|
191
|
-
if (candidate.scope === 'surface') {
|
|
192
|
-
return surfaceRoot;
|
|
193
|
-
}
|
|
194
|
-
return defaultRoot;
|
|
195
|
-
}
|
|
196
|
-
async function prepareLocatorForAction(locator, action, strategy, attempts, options) {
|
|
197
|
-
const count = await locator.count().catch(() => 0);
|
|
198
|
-
if (count === 0) {
|
|
199
|
-
attempts.push(`resolve.skip:${strategy}:empty`);
|
|
200
|
-
return { locator: null };
|
|
201
|
-
}
|
|
202
|
-
if (action !== 'click' && count > 1) {
|
|
203
|
-
attempts.push(`resolve.skip:${strategy}:ambiguous:${count}`);
|
|
204
|
-
return { locator: null };
|
|
205
|
-
}
|
|
206
|
-
let resolvedLocator = locator.first();
|
|
207
|
-
if (action === 'click' && count > 1) {
|
|
208
|
-
const visibleCandidates = [];
|
|
209
|
-
for (let index = 0; index < count; index += 1) {
|
|
210
|
-
const candidate = locator.nth(index);
|
|
211
|
-
const visible = await isLocatorUserActionable(candidate);
|
|
212
|
-
if (!visible) {
|
|
213
|
-
continue;
|
|
214
|
-
}
|
|
215
|
-
visibleCandidates.push(candidate);
|
|
216
|
-
}
|
|
217
|
-
if (visibleCandidates.length === 1) {
|
|
218
|
-
attempts.push(`resolve.visible-unique:${strategy}`);
|
|
219
|
-
resolvedLocator = visibleCandidates[0] ?? locator.first();
|
|
220
|
-
}
|
|
221
|
-
else if (visibleCandidates.length > 1) {
|
|
222
|
-
attempts.push(`resolve.skip:${strategy}:ambiguous-visible:${visibleCandidates.length}`);
|
|
223
|
-
return { locator: null };
|
|
224
|
-
}
|
|
225
|
-
else {
|
|
226
|
-
attempts.push(`resolve.skip:${strategy}:hidden`);
|
|
227
|
-
return { locator: null };
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
const visible = await isLocatorUserActionable(resolvedLocator);
|
|
231
|
-
if (!visible) {
|
|
232
|
-
attempts.push(`resolve.skip:${strategy}:hidden`);
|
|
233
|
-
return { locator: null };
|
|
234
|
-
}
|
|
235
|
-
const disabled = await resolvedLocator.isDisabled?.().catch(() => false);
|
|
236
|
-
if (disabled) {
|
|
237
|
-
attempts.push(`resolve.skip:${strategy}:disabled`);
|
|
238
|
-
return { locator: null, blockedReason: 'disabled' };
|
|
239
|
-
}
|
|
240
|
-
if (action === 'fill' || action === 'type' || options?.allowDescendantPressFallback) {
|
|
241
|
-
const editable = await resolvedLocator.isEditable().catch(() => false);
|
|
242
|
-
if (!editable) {
|
|
243
|
-
const descendantSelector = options?.allowDescendantPressFallback
|
|
244
|
-
? 'input:not([type="hidden"]), textarea, select, [contenteditable="true"], [role="textbox"], [role="combobox"]'
|
|
245
|
-
: 'input:not([type="hidden"]), textarea, select, [contenteditable="true"]';
|
|
246
|
-
const descendantCandidates = resolvedLocator.locator(descendantSelector);
|
|
247
|
-
const descendantCount = await descendantCandidates.count().catch(() => 0);
|
|
248
|
-
const candidateDescendants = [];
|
|
249
|
-
for (let index = 0; index < descendantCount; index += 1) {
|
|
250
|
-
const descendant = descendantCandidates.nth(index);
|
|
251
|
-
const descendantVisible = await isLocatorUserActionable(descendant);
|
|
252
|
-
if (!descendantVisible)
|
|
253
|
-
continue;
|
|
254
|
-
if (!options?.allowDescendantPressFallback || action === 'fill' || action === 'type') {
|
|
255
|
-
const descendantEditable = await descendant.isEditable().catch(() => false);
|
|
256
|
-
if (!descendantEditable)
|
|
257
|
-
continue;
|
|
258
|
-
}
|
|
259
|
-
candidateDescendants.push(descendant);
|
|
260
|
-
}
|
|
261
|
-
if (candidateDescendants.length === 1) {
|
|
262
|
-
attempts.push(options?.allowDescendantPressFallback && action === 'press'
|
|
263
|
-
? `resolve.descendant-press:${strategy}`
|
|
264
|
-
: `resolve.descendant-editable:${strategy}`);
|
|
265
|
-
return { locator: candidateDescendants[0] ?? null };
|
|
266
|
-
}
|
|
267
|
-
if (candidateDescendants.length > 1) {
|
|
268
|
-
attempts.push(`resolve.skip:${strategy}:descendant-ambiguous:${candidateDescendants.length}`);
|
|
269
|
-
return { locator: null };
|
|
270
|
-
}
|
|
271
|
-
if (options?.allowReadonlyFallback) {
|
|
272
|
-
attempts.push(`resolve.readonly-fallback:${strategy}`);
|
|
273
|
-
return { locator: resolvedLocator };
|
|
274
|
-
}
|
|
275
|
-
attempts.push(`resolve.skip:${strategy}:readonly`);
|
|
276
|
-
return { locator: null, blockedReason: 'readonly' };
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
return { locator: resolvedLocator };
|
|
280
|
-
}
|
|
281
291
|
async function recoverLocatorFromSurfaceRoot(locatorRoot, target, action, attempts) {
|
|
282
292
|
if (action !== 'press' ||
|
|
283
293
|
!(target.controlFamily === 'text-input' ||
|
|
@@ -340,599 +350,741 @@ async function waitForLatePage(browser, beforePages, timeoutMs = 2_000) {
|
|
|
340
350
|
return null;
|
|
341
351
|
}
|
|
342
352
|
export async function act(session, targetRef, action, value) {
|
|
343
|
-
const
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
reason: `Target ${targetRef} has capability ${target.capability}, not actionable.`,
|
|
370
|
-
targetRef,
|
|
371
|
-
action,
|
|
372
|
-
});
|
|
373
|
-
}
|
|
374
|
-
if (!target.allowedActions.includes(action)) {
|
|
375
|
-
return emitActPreflightFailure({
|
|
376
|
-
error: 'action_not_allowed_for_target',
|
|
377
|
-
outcomeType: 'unsupported',
|
|
378
|
-
message: 'The requested action is not allowed for this target.',
|
|
379
|
-
reason: `Target ${targetRef} allows ${target.allowedActions.join(', ')}, not ${action}.`,
|
|
380
|
-
targetRef,
|
|
381
|
-
action,
|
|
382
|
-
});
|
|
383
|
-
}
|
|
384
|
-
if (target.availability.state === 'gated' &&
|
|
385
|
-
(target.availability.reason === 'occupied' || target.availability.reason === 'not-selectable')) {
|
|
386
|
-
return emitActPreflightFailure({
|
|
387
|
-
error: 'target_gated',
|
|
388
|
-
outcomeType: 'blocked',
|
|
389
|
-
message: 'The requested target is currently gated.',
|
|
390
|
-
reason: `Target ${targetRef} is gated${target.availability.reason ? ` because ${target.availability.reason}` : ''}.`,
|
|
391
|
-
targetRef,
|
|
392
|
-
action,
|
|
393
|
-
});
|
|
394
|
-
}
|
|
395
|
-
const surface = target.surfaceRef ? getSurface(session, target.surfaceRef) : null;
|
|
396
|
-
if (surface && surface.lifecycle !== 'live') {
|
|
397
|
-
setTargetAvailability(session, targetRef, 'surface-inactive', surface.lifecycleReason ?? `surface-${surface.lifecycle}`);
|
|
398
|
-
saveSession(session);
|
|
399
|
-
return emitActPreflightFailure({
|
|
400
|
-
error: 'target_surface_not_live',
|
|
401
|
-
outcomeType: 'blocked',
|
|
402
|
-
message: 'The requested target surface is no longer live.',
|
|
403
|
-
reason: `Surface ${surface.ref} is ${surface.lifecycle}${surface.lifecycleReason ? ` because ${surface.lifecycleReason}` : ''}.`,
|
|
404
|
-
targetRef,
|
|
405
|
-
action,
|
|
406
|
-
});
|
|
407
|
-
}
|
|
408
|
-
if (surface && surface.availability.state !== 'available') {
|
|
409
|
-
setTargetAvailability(session, targetRef, 'surface-inactive', surface.availability.reason ?? `surface-${surface.availability.state}`);
|
|
410
|
-
saveSession(session);
|
|
411
|
-
return emitActPreflightFailure({
|
|
412
|
-
error: 'target_surface_unavailable',
|
|
413
|
-
outcomeType: 'blocked',
|
|
414
|
-
message: 'The requested target surface is not currently available.',
|
|
415
|
-
reason: `Surface ${surface.ref} is ${surface.availability.state}${surface.availability.reason ? ` because ${surface.availability.reason}` : ''}.`,
|
|
416
|
-
targetRef,
|
|
417
|
-
action,
|
|
418
|
-
});
|
|
419
|
-
}
|
|
420
|
-
const actionValue = ensureValue(action, value);
|
|
421
|
-
const attempts = [];
|
|
422
|
-
const startedAt = Date.now();
|
|
423
|
-
let browser = null;
|
|
424
|
-
let failureMessage = null;
|
|
425
|
-
let failureArtifacts;
|
|
426
|
-
let currentPage = null;
|
|
427
|
-
let currentPageRef = target.pageRef;
|
|
428
|
-
const startingPageUrl = session.runtime?.pages?.[target.pageRef]?.url ?? null;
|
|
429
|
-
const protectedExposureAtStart = getProtectedExposure(session, target.pageRef);
|
|
430
|
-
let locatorStrategy = null;
|
|
431
|
-
let recoveredAfterError = false;
|
|
432
|
-
let recoveredAcceptancePolicy = null;
|
|
433
|
-
let staleReason = null;
|
|
434
|
-
let progressProbe = null;
|
|
435
|
-
let noProgressDiagnosis = null;
|
|
436
|
-
let liveTarget = target;
|
|
437
|
-
let trace = {
|
|
438
|
-
finishSuccess: async () => { },
|
|
439
|
-
finishFailure: async (_artifactDir) => undefined,
|
|
440
|
-
};
|
|
441
|
-
try {
|
|
442
|
-
browser = await connectPlaywright(session.cdpUrl);
|
|
443
|
-
}
|
|
444
|
-
catch (err) {
|
|
445
|
-
return emitActPreflightFailure({
|
|
446
|
-
error: 'browser_connection_failed',
|
|
447
|
-
outcomeType: 'blocked',
|
|
448
|
-
message: 'The action could not start because AgentBrowse failed to connect to the browser.',
|
|
449
|
-
reason: err instanceof Error ? err.message : String(err),
|
|
353
|
+
const requestedAction = action;
|
|
354
|
+
const runId = session.activeRunId;
|
|
355
|
+
const actStep = runId
|
|
356
|
+
? startRunStep({
|
|
357
|
+
runId,
|
|
358
|
+
command: 'act',
|
|
359
|
+
input: {
|
|
360
|
+
targetRef,
|
|
361
|
+
action: requestedAction,
|
|
362
|
+
valueSupplied: typeof value === 'string' && value.length > 0,
|
|
363
|
+
},
|
|
364
|
+
refs: {
|
|
365
|
+
targetRef,
|
|
366
|
+
},
|
|
367
|
+
})
|
|
368
|
+
: null;
|
|
369
|
+
captureStepSnapshotBestEffort({
|
|
370
|
+
session,
|
|
371
|
+
step: actStep,
|
|
372
|
+
phase: 'before',
|
|
373
|
+
pageRef: session.runtime?.currentPageRef,
|
|
374
|
+
});
|
|
375
|
+
appendCommandLifecycleEventBestEffort({
|
|
376
|
+
step: actStep,
|
|
377
|
+
phase: 'started',
|
|
378
|
+
attributes: {
|
|
450
379
|
targetRef,
|
|
451
|
-
action,
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
380
|
+
action: requestedAction,
|
|
381
|
+
valueSupplied: typeof value === 'string' && value.length > 0,
|
|
382
|
+
},
|
|
383
|
+
});
|
|
384
|
+
return withApiTraceContext({
|
|
385
|
+
runId,
|
|
386
|
+
stepId: actStep?.stepId,
|
|
387
|
+
command: 'act',
|
|
388
|
+
}, async () => {
|
|
389
|
+
const target = getTarget(session, targetRef);
|
|
390
|
+
if (!target) {
|
|
391
|
+
return emitActPreflightFailure({
|
|
392
|
+
session,
|
|
393
|
+
error: 'unknown_target_ref',
|
|
394
|
+
outcomeType: 'blocked',
|
|
395
|
+
message: 'The requested targetRef is unknown.',
|
|
396
|
+
reason: `No stored target matches targetRef ${targetRef}.`,
|
|
397
|
+
targetRef,
|
|
398
|
+
action,
|
|
399
|
+
runId,
|
|
400
|
+
stepId: actStep?.stepId,
|
|
401
|
+
});
|
|
465
402
|
}
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
403
|
+
if (target.lifecycle !== 'live') {
|
|
404
|
+
return emitActPreflightFailure({
|
|
405
|
+
session,
|
|
406
|
+
error: 'stale_target_ref',
|
|
407
|
+
outcomeType: 'binding_stale',
|
|
408
|
+
message: 'The requested target is no longer live.',
|
|
409
|
+
reason: `Target ${targetRef} is ${target.lifecycle}${target.lifecycleReason ? ` because ${target.lifecycleReason}` : ''}.`,
|
|
410
|
+
targetRef,
|
|
411
|
+
action,
|
|
412
|
+
runId,
|
|
413
|
+
stepId: actStep?.stepId,
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
if (target.capability !== 'actionable') {
|
|
417
|
+
return emitActPreflightFailure({
|
|
418
|
+
session,
|
|
419
|
+
error: 'target_not_actionable',
|
|
420
|
+
outcomeType: 'unsupported',
|
|
421
|
+
message: 'The requested target cannot be used for actions.',
|
|
422
|
+
reason: `Target ${targetRef} has capability ${target.capability}, not actionable.`,
|
|
423
|
+
targetRef,
|
|
424
|
+
action: requestedAction,
|
|
425
|
+
runId,
|
|
426
|
+
stepId: actStep?.stepId,
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
if (!target.allowedActions.includes(action)) {
|
|
430
|
+
const canAliasFillLikeToSelect = (requestedAction === 'fill' || requestedAction === 'type') &&
|
|
431
|
+
target.controlFamily === 'select' &&
|
|
432
|
+
target.allowedActions.includes('select') &&
|
|
433
|
+
typeof value === 'string' &&
|
|
434
|
+
value.length > 0;
|
|
435
|
+
if (canAliasFillLikeToSelect) {
|
|
436
|
+
action = 'select';
|
|
469
437
|
}
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
}
|
|
480
|
-
if (snapshot.domSignature === liveTarget.domSignature) {
|
|
481
|
-
return false;
|
|
482
|
-
}
|
|
483
|
-
if (!isCompatibleMutableFieldBinding(liveTarget, snapshot)) {
|
|
484
|
-
continue;
|
|
485
|
-
}
|
|
486
|
-
attempts.push(`domSignature.rebound:${strategy}`);
|
|
487
|
-
const updatedTarget = updateTarget(session, targetRef, {
|
|
488
|
-
domSignature: snapshot.domSignature,
|
|
489
|
-
label: snapshot.label ?? liveTarget.label,
|
|
490
|
-
lifecycle: 'live',
|
|
491
|
-
lifecycleReason: undefined,
|
|
492
|
-
availability: { state: 'available' },
|
|
493
|
-
semantics: {
|
|
494
|
-
...liveTarget.semantics,
|
|
495
|
-
name: snapshot.label ?? liveTarget.semantics?.name,
|
|
496
|
-
role: snapshot.role ?? liveTarget.semantics?.role,
|
|
497
|
-
},
|
|
438
|
+
else {
|
|
439
|
+
return emitActPreflightFailure({
|
|
440
|
+
session,
|
|
441
|
+
error: 'action_not_allowed_for_target',
|
|
442
|
+
outcomeType: 'unsupported',
|
|
443
|
+
message: 'The requested action is not allowed for this target.',
|
|
444
|
+
reason: `Target ${targetRef} allows ${target.allowedActions.join(', ')}, not ${requestedAction}.`,
|
|
445
|
+
targetRef,
|
|
446
|
+
action: requestedAction,
|
|
498
447
|
});
|
|
499
|
-
if (updatedTarget) {
|
|
500
|
-
liveTarget = updatedTarget;
|
|
501
|
-
}
|
|
502
|
-
return true;
|
|
503
448
|
}
|
|
504
|
-
|
|
449
|
+
}
|
|
450
|
+
if (target.availability.state === 'gated' &&
|
|
451
|
+
(target.availability.reason === 'occupied' || target.availability.reason === 'not-selectable')) {
|
|
452
|
+
return emitActPreflightFailure({
|
|
453
|
+
session,
|
|
454
|
+
error: 'target_gated',
|
|
455
|
+
outcomeType: 'blocked',
|
|
456
|
+
message: 'The requested target is currently gated.',
|
|
457
|
+
reason: `Target ${targetRef} is gated${target.availability.reason ? ` because ${target.availability.reason}` : ''}.`,
|
|
458
|
+
targetRef,
|
|
459
|
+
action: requestedAction,
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
const surface = target.surfaceRef ? getSurface(session, target.surfaceRef) : null;
|
|
463
|
+
if (surface && surface.lifecycle !== 'live') {
|
|
464
|
+
setTargetAvailability(session, targetRef, 'surface-inactive', surface.lifecycleReason ?? `surface-${surface.lifecycle}`);
|
|
465
|
+
saveSession(session);
|
|
466
|
+
return emitActPreflightFailure({
|
|
467
|
+
session,
|
|
468
|
+
error: 'target_surface_not_live',
|
|
469
|
+
outcomeType: 'blocked',
|
|
470
|
+
message: 'The requested target surface is no longer live.',
|
|
471
|
+
reason: `Surface ${surface.ref} is ${surface.lifecycle}${surface.lifecycleReason ? ` because ${surface.lifecycleReason}` : ''}.`,
|
|
472
|
+
targetRef,
|
|
473
|
+
action: requestedAction,
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
if (surface && surface.availability.state !== 'available') {
|
|
477
|
+
setTargetAvailability(session, targetRef, 'surface-inactive', surface.availability.reason ?? `surface-${surface.availability.state}`);
|
|
478
|
+
saveSession(session);
|
|
479
|
+
return emitActPreflightFailure({
|
|
480
|
+
session,
|
|
481
|
+
error: 'target_surface_unavailable',
|
|
482
|
+
outcomeType: 'blocked',
|
|
483
|
+
message: 'The requested target surface is not currently available.',
|
|
484
|
+
reason: `Surface ${surface.ref} is ${surface.availability.state}${surface.availability.reason ? ` because ${surface.availability.reason}` : ''}.`,
|
|
485
|
+
targetRef,
|
|
486
|
+
action: requestedAction,
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
const actionValue = ensureValue(action, value);
|
|
490
|
+
const attempts = [];
|
|
491
|
+
if (action !== requestedAction) {
|
|
492
|
+
attempts.push(`action.alias:${requestedAction}->${action}`);
|
|
493
|
+
}
|
|
494
|
+
const startedAt = Date.now();
|
|
495
|
+
let browser = null;
|
|
496
|
+
let failureMessage = null;
|
|
497
|
+
let failureArtifacts;
|
|
498
|
+
let currentPage = null;
|
|
499
|
+
let currentPageRef = target.pageRef;
|
|
500
|
+
const startingPageUrl = session.runtime?.pages?.[target.pageRef]?.url ?? null;
|
|
501
|
+
const protectedExposureAtStart = getProtectedExposure(session, target.pageRef);
|
|
502
|
+
let locatorStrategy = null;
|
|
503
|
+
let recoveredAfterError = false;
|
|
504
|
+
let recoveredAcceptancePolicy = null;
|
|
505
|
+
let recoveredProgressProbe = null;
|
|
506
|
+
let staleReason = null;
|
|
507
|
+
let progressProbe = null;
|
|
508
|
+
let noProgressDiagnosis = null;
|
|
509
|
+
let partialProgressResult = null;
|
|
510
|
+
let liveTarget = target;
|
|
511
|
+
let trace = {
|
|
512
|
+
finishSuccess: async () => { },
|
|
513
|
+
finishFailure: async (_artifactDir) => undefined,
|
|
505
514
|
};
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
515
|
+
try {
|
|
516
|
+
browser = await connectPlaywright(session.cdpUrl);
|
|
517
|
+
}
|
|
518
|
+
catch (err) {
|
|
519
|
+
return emitActPreflightFailure({
|
|
520
|
+
session,
|
|
521
|
+
error: 'browser_connection_failed',
|
|
522
|
+
outcomeType: 'blocked',
|
|
523
|
+
message: 'The action could not start because AgentBrowse failed to connect to the browser.',
|
|
524
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
525
|
+
targetRef,
|
|
526
|
+
action,
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
try {
|
|
530
|
+
const page = await resolvePageByRef(browser, session, target.pageRef);
|
|
531
|
+
currentPage = page;
|
|
532
|
+
setCurrentPage(session, target.pageRef);
|
|
533
|
+
const { url } = await syncSessionPage(session, target.pageRef, page);
|
|
534
|
+
trace = await startActionTrace(page, {
|
|
535
|
+
suppressSensitiveArtifacts: Boolean(protectedExposureAtStart),
|
|
536
|
+
});
|
|
537
|
+
if (liveTarget.pageSignature && normalizePageSignature(url) !== liveTarget.pageSignature) {
|
|
510
538
|
staleReason = 'page-signature-mismatch';
|
|
511
539
|
throw new Error('stale_target_page_signature_changed');
|
|
512
540
|
}
|
|
513
|
-
const
|
|
514
|
-
|
|
515
|
-
attempts.push(`stale.locator:${stage}`);
|
|
516
|
-
staleReason = 'locator-resolution-failed';
|
|
517
|
-
throw new Error('stale_target_locator_resolution_failed');
|
|
518
|
-
}
|
|
519
|
-
if (liveTarget.domSignature) {
|
|
520
|
-
const rebound = await tryRebindMutableFieldTarget(resolvedLocator, stage);
|
|
521
|
-
if (!rebound) {
|
|
522
|
-
const liveSignature = await readLocatorDomSignature(resolvedLocator).catch(() => null);
|
|
523
|
-
if (liveSignature && liveSignature !== liveTarget.domSignature) {
|
|
524
|
-
attempts.push(`stale.dom-signature:${stage}`);
|
|
525
|
-
staleReason = 'dom-signature-mismatch';
|
|
526
|
-
throw new Error('stale_target_dom_signature_changed');
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
};
|
|
531
|
-
let resolvedBy = null;
|
|
532
|
-
const beforePages = listPages(browser);
|
|
533
|
-
const shouldCheckProgress = shouldVerifyObservableProgress(target, action);
|
|
534
|
-
const beforePageObservation = shouldCheckProgress ? await capturePageObservation(page) : null;
|
|
535
|
-
const deferSurfaceResolution = shouldDeferSurfaceResolutionForEditablePress(target, action);
|
|
536
|
-
let baseRoot = resolveLocatorRoot(page, target.framePath ?? surface?.framePath);
|
|
537
|
-
let locatorRoot = baseRoot;
|
|
538
|
-
let surfaceRoot = null;
|
|
539
|
-
if (!deferSurfaceResolution) {
|
|
540
|
-
({ baseRoot, locatorRoot, surfaceRoot } = await resolveActionRoot(page, target, attempts, surface));
|
|
541
|
-
}
|
|
542
|
-
let lastError = null;
|
|
543
|
-
let sawDomSignatureMismatch = false;
|
|
544
|
-
let sawDisabledTarget = false;
|
|
545
|
-
let sawReadonlyTarget = false;
|
|
546
|
-
const attemptResolvedLocator = async (resolvedLocator, strategy, options) => {
|
|
547
|
-
if (!options?.skipDomSignature && liveTarget.domSignature) {
|
|
548
|
-
const rebound = await tryRebindMutableFieldTarget(resolvedLocator, strategy);
|
|
549
|
-
if (!rebound) {
|
|
550
|
-
const liveSignature = await readLocatorDomSignature(resolvedLocator);
|
|
551
|
-
if (liveSignature && liveSignature !== liveTarget.domSignature) {
|
|
552
|
-
attempts.push(`domSignature.mismatch:${strategy}`);
|
|
553
|
-
sawDomSignatureMismatch = true;
|
|
554
|
-
return false;
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
let acceptanceProbe = null;
|
|
559
|
-
const tryRecoverActionErrorAcceptance = async () => {
|
|
560
|
-
if (!acceptanceProbe) {
|
|
561
|
-
return false;
|
|
562
|
-
}
|
|
563
|
-
const acceptance = await waitForAcceptanceProbe(acceptanceProbe).catch(() => null);
|
|
564
|
-
if (acceptance?.polls && acceptance.polls > 1) {
|
|
565
|
-
attempts.push(`acceptance.polled:${acceptance.polls}`);
|
|
566
|
-
}
|
|
567
|
-
if (!acceptance?.accepted) {
|
|
541
|
+
const tryRebindMutableFieldTarget = async (resolvedLocator, strategy) => {
|
|
542
|
+
if (!liveTarget.domSignature) {
|
|
568
543
|
return false;
|
|
569
544
|
}
|
|
570
|
-
|
|
571
|
-
const
|
|
572
|
-
if (
|
|
545
|
+
for (let attemptIndex = 0; attemptIndex < MUTABLE_FIELD_REBIND_RETRY_DELAYS_MS.length; attemptIndex += 1) {
|
|
546
|
+
const delayMs = MUTABLE_FIELD_REBIND_RETRY_DELAYS_MS[attemptIndex] ?? 0;
|
|
547
|
+
if (attemptIndex > 0) {
|
|
548
|
+
attempts.push(`domSignature.rebind.retry:${strategy}:${attemptIndex + 1}`);
|
|
549
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
550
|
+
}
|
|
551
|
+
const snapshot = await readLocatorBindingSnapshot(resolvedLocator).catch(() => null);
|
|
552
|
+
if (!snapshot?.domSignature) {
|
|
553
|
+
continue;
|
|
554
|
+
}
|
|
555
|
+
if (snapshot.domSignature === liveTarget.domSignature) {
|
|
573
556
|
return false;
|
|
574
557
|
}
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
attempts.push('submit-resolution:soft-result-candidate');
|
|
558
|
+
if (!isCompatibleMutableFieldBinding(liveTarget, snapshot)) {
|
|
559
|
+
continue;
|
|
578
560
|
}
|
|
561
|
+
attempts.push(`domSignature.rebound:${strategy}`);
|
|
562
|
+
const updatedTarget = updateTarget(session, targetRef, {
|
|
563
|
+
domSignature: snapshot.domSignature,
|
|
564
|
+
label: snapshot.label ?? liveTarget.label,
|
|
565
|
+
lifecycle: 'live',
|
|
566
|
+
lifecycleReason: undefined,
|
|
567
|
+
availability: { state: 'available' },
|
|
568
|
+
semantics: {
|
|
569
|
+
...liveTarget.semantics,
|
|
570
|
+
name: snapshot.label ?? liveTarget.semantics?.name,
|
|
571
|
+
role: snapshot.role ?? liveTarget.semantics?.role,
|
|
572
|
+
},
|
|
573
|
+
});
|
|
574
|
+
if (updatedTarget) {
|
|
575
|
+
liveTarget = updatedTarget;
|
|
576
|
+
}
|
|
577
|
+
return true;
|
|
579
578
|
}
|
|
580
|
-
|
|
581
|
-
incrementMetric(session, 'fallbackActions');
|
|
582
|
-
resolvedBy = 'playwright-locator';
|
|
583
|
-
locatorStrategy = strategy;
|
|
584
|
-
progressProbe = null;
|
|
585
|
-
lastError = null;
|
|
586
|
-
recoveredAfterError = true;
|
|
587
|
-
recoveredAcceptancePolicy = acceptanceProbe.policy;
|
|
588
|
-
setTargetAvailability(session, targetRef, 'available');
|
|
589
|
-
return true;
|
|
579
|
+
return false;
|
|
590
580
|
};
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
581
|
+
const assertResolvedTargetStillValid = async (resolvedLocator, stage) => {
|
|
582
|
+
await assertStoredBindingStillValid(page, resolvedLocator, liveTarget, stage, {
|
|
583
|
+
onReason: async (reason, staleStage) => {
|
|
584
|
+
switch (reason) {
|
|
585
|
+
case 'page_signature_mismatch':
|
|
586
|
+
attempts.push(`stale.page-signature:${staleStage}`);
|
|
587
|
+
staleReason = 'page-signature-mismatch';
|
|
588
|
+
return false;
|
|
589
|
+
case 'locator_resolution_failed':
|
|
590
|
+
attempts.push(`stale.locator:${staleStage}`);
|
|
591
|
+
staleReason = 'locator-resolution-failed';
|
|
592
|
+
return false;
|
|
593
|
+
case 'dom_signature_mismatch': {
|
|
594
|
+
const rebound = await tryRebindMutableFieldTarget(resolvedLocator, staleStage);
|
|
595
|
+
if (rebound) {
|
|
596
|
+
return true;
|
|
597
|
+
}
|
|
598
|
+
attempts.push(`stale.dom-signature:${staleStage}`);
|
|
599
|
+
staleReason = 'dom-signature-mismatch';
|
|
600
|
+
return false;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
},
|
|
604
|
+
errorForReason: (reason) => {
|
|
605
|
+
switch (reason) {
|
|
606
|
+
case 'page_signature_mismatch':
|
|
607
|
+
return 'stale_target_page_signature_changed';
|
|
608
|
+
case 'locator_resolution_failed':
|
|
609
|
+
return 'stale_target_locator_resolution_failed';
|
|
610
|
+
case 'dom_signature_mismatch':
|
|
611
|
+
return 'stale_target_dom_signature_changed';
|
|
612
|
+
}
|
|
617
613
|
},
|
|
618
614
|
});
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
615
|
+
};
|
|
616
|
+
let resolvedBy = null;
|
|
617
|
+
const beforePages = listPages(browser);
|
|
618
|
+
const shouldCheckProgress = shouldVerifyObservableProgress(target, action);
|
|
619
|
+
const beforePageObservation = shouldCheckProgress ? await capturePageObservation(page) : null;
|
|
620
|
+
const deferSurfaceResolution = shouldDeferSurfaceResolutionForEditablePress(target, action);
|
|
621
|
+
let baseRoot = page;
|
|
622
|
+
let locatorRoot = baseRoot;
|
|
623
|
+
let surfaceRoot = null;
|
|
624
|
+
if (!deferSurfaceResolution) {
|
|
625
|
+
({ baseRoot, locatorRoot, surfaceRoot } = await resolveInteractionRoots(page, target, surface, attempts, {
|
|
626
|
+
recordSelfTargetReuse: true,
|
|
627
|
+
}));
|
|
627
628
|
}
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
629
|
+
let lastError = null;
|
|
630
|
+
let sawDomSignatureMismatch = false;
|
|
631
|
+
let sawDisabledTarget = false;
|
|
632
|
+
let sawReadonlyTarget = false;
|
|
633
|
+
const attemptResolvedLocator = async (resolvedLocator, strategy, options) => {
|
|
634
|
+
if (!options?.skipDomSignature && liveTarget.domSignature) {
|
|
635
|
+
const rebound = await tryRebindMutableFieldTarget(resolvedLocator, strategy);
|
|
636
|
+
if (!rebound) {
|
|
637
|
+
const liveSignature = await readLocatorDomSignature(resolvedLocator);
|
|
638
|
+
if (liveSignature && liveSignature !== liveTarget.domSignature) {
|
|
639
|
+
attempts.push(`domSignature.mismatch:${strategy}`);
|
|
640
|
+
sawDomSignatureMismatch = true;
|
|
641
|
+
return false;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
641
644
|
}
|
|
645
|
+
let acceptanceProbe = null;
|
|
646
|
+
const tryRecoverActionErrorAcceptance = async () => {
|
|
647
|
+
if (!acceptanceProbe) {
|
|
648
|
+
return false;
|
|
649
|
+
}
|
|
650
|
+
const acceptance = await waitForAcceptanceProbe(acceptanceProbe).catch(() => null);
|
|
651
|
+
if (acceptance?.polls && acceptance.polls > 1) {
|
|
652
|
+
attempts.push(`acceptance.polled:${acceptance.polls}`);
|
|
653
|
+
}
|
|
654
|
+
if (!acceptance?.accepted) {
|
|
655
|
+
return false;
|
|
656
|
+
}
|
|
657
|
+
if (acceptanceProbe.policy === 'submit') {
|
|
658
|
+
const submitResolution = await resolveSubmitResult(acceptanceProbe, acceptance.afterPageObservation);
|
|
659
|
+
if (!submitResolution.acceptAsProgress) {
|
|
660
|
+
return false;
|
|
661
|
+
}
|
|
662
|
+
attempts.push(`submit-resolution:${submitResolution.finalVerdict}`);
|
|
663
|
+
if (submitResolution.claims.some((claim) => claim.kind === 'soft_result_candidate')) {
|
|
664
|
+
attempts.push('submit-resolution:soft-result-candidate');
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
attempts.push(`acceptance.recovered:${acceptanceProbe.policy}`);
|
|
668
|
+
incrementMetric(session, 'fallbackActions');
|
|
669
|
+
resolvedBy = 'playwright-locator';
|
|
670
|
+
locatorStrategy = strategy;
|
|
671
|
+
recoveredProgressProbe = acceptanceProbe;
|
|
672
|
+
progressProbe = null;
|
|
673
|
+
lastError = null;
|
|
674
|
+
recoveredAfterError = true;
|
|
675
|
+
recoveredAcceptancePolicy = acceptanceProbe.policy;
|
|
676
|
+
setTargetAvailability(session, targetRef, 'available');
|
|
677
|
+
return true;
|
|
678
|
+
};
|
|
642
679
|
try {
|
|
643
|
-
await
|
|
680
|
+
const valueProjection = await projectActionValue({
|
|
681
|
+
target,
|
|
682
|
+
action,
|
|
683
|
+
actionValue,
|
|
684
|
+
locator: resolvedLocator,
|
|
685
|
+
attempts,
|
|
686
|
+
});
|
|
687
|
+
const executionValue = valueProjection?.executionValue ?? actionValue;
|
|
688
|
+
const acceptanceValue = valueProjection?.acceptanceValue ?? actionValue;
|
|
689
|
+
acceptanceProbe = await createAcceptanceProbe({
|
|
690
|
+
session,
|
|
691
|
+
page,
|
|
692
|
+
target,
|
|
693
|
+
action,
|
|
694
|
+
actionValue: acceptanceValue,
|
|
695
|
+
locator: resolvedLocator,
|
|
696
|
+
beforePageObservation,
|
|
697
|
+
});
|
|
698
|
+
attempts.push(`resolve:${strategy}`);
|
|
699
|
+
const usedFallback = await applyActionWithFallbacks(page, locatorRoot, resolvedLocator, action, executionValue, attempts, target.controlFamily, {
|
|
700
|
+
clickActivationStrategy: clickActivationStrategyForTarget(target, action),
|
|
701
|
+
guards: {
|
|
702
|
+
assertStillValid: async (stage) => {
|
|
703
|
+
await assertResolvedTargetStillValid(resolvedLocator, stage);
|
|
704
|
+
},
|
|
705
|
+
},
|
|
706
|
+
});
|
|
707
|
+
if (usedFallback) {
|
|
708
|
+
incrementMetric(session, 'fallbackActions');
|
|
709
|
+
}
|
|
710
|
+
resolvedBy = 'playwright-locator';
|
|
711
|
+
locatorStrategy = strategy;
|
|
712
|
+
progressProbe = acceptanceProbe;
|
|
713
|
+
setTargetAvailability(session, targetRef, 'available');
|
|
714
|
+
return true;
|
|
644
715
|
}
|
|
645
|
-
catch (
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
716
|
+
catch (err) {
|
|
717
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
718
|
+
const shouldAttemptAcceptanceRecovery = acceptanceProbe !== null &&
|
|
719
|
+
(acceptanceProbe.policy === 'value-change' ||
|
|
720
|
+
acceptanceProbe.policy === 'selection' ||
|
|
721
|
+
acceptanceProbe.policy === 'date-selection' ||
|
|
722
|
+
acceptanceProbe.policy === 'navigation' ||
|
|
723
|
+
acceptanceProbe.policy === 'submit');
|
|
724
|
+
if (shouldAttemptAcceptanceRecovery && (await tryRecoverActionErrorAcceptance())) {
|
|
650
725
|
return true;
|
|
651
726
|
}
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
727
|
+
if (staleReason) {
|
|
728
|
+
throw lastError;
|
|
729
|
+
}
|
|
730
|
+
try {
|
|
731
|
+
await assertResolvedTargetStillValid(resolvedLocator, `after-error:${strategy}`);
|
|
732
|
+
}
|
|
733
|
+
catch (validationError) {
|
|
734
|
+
if ((acceptanceProbe?.policy === 'navigation' || acceptanceProbe?.policy === 'submit') &&
|
|
735
|
+
validationError instanceof Error &&
|
|
736
|
+
validationError.message === 'stale_target_page_signature_changed' &&
|
|
737
|
+
(await tryRecoverActionErrorAcceptance())) {
|
|
738
|
+
return true;
|
|
739
|
+
}
|
|
740
|
+
throw validationError;
|
|
741
|
+
}
|
|
742
|
+
return false;
|
|
667
743
|
}
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
744
|
+
};
|
|
745
|
+
const watchForNewPage = shouldWatchForNewPageAfterAction(target, action);
|
|
746
|
+
const popupPromise = watchForNewPage
|
|
747
|
+
? waitForPopup(page.context())
|
|
748
|
+
: Promise.resolve(null);
|
|
749
|
+
const tryRankedCandidates = async () => {
|
|
750
|
+
const resolution = await resolvePreparedLocatorCandidates({
|
|
751
|
+
target,
|
|
752
|
+
action,
|
|
753
|
+
baseRoot,
|
|
754
|
+
locatorRoot,
|
|
755
|
+
surfaceRoot,
|
|
756
|
+
attempts,
|
|
757
|
+
prepareOptions: {
|
|
758
|
+
allowReadonlyFallback: action === 'fill' && target.controlFamily === 'datepicker',
|
|
759
|
+
allowDescendantPressFallback: action === 'press' &&
|
|
760
|
+
(target.controlFamily === 'text-input' ||
|
|
761
|
+
target.controlFamily === 'select' ||
|
|
762
|
+
target.controlFamily === 'datepicker'),
|
|
763
|
+
isUserActionable: isLocatorUserActionable,
|
|
764
|
+
},
|
|
765
|
+
onPreparedLocator: async (resolvedLocator, strategy) => attemptResolvedLocator(resolvedLocator, strategy),
|
|
677
766
|
});
|
|
678
|
-
if (
|
|
767
|
+
if (resolution.sawDisabledTarget) {
|
|
679
768
|
sawDisabledTarget = true;
|
|
680
769
|
}
|
|
681
|
-
if (
|
|
770
|
+
if (resolution.sawReadonlyTarget) {
|
|
682
771
|
sawReadonlyTarget = true;
|
|
683
772
|
}
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
773
|
+
};
|
|
774
|
+
await tryRankedCandidates();
|
|
775
|
+
if (!resolvedBy && !lastError && deferSurfaceResolution && surface) {
|
|
776
|
+
const deferredSurfaceRoot = await resolveSurfaceScopeRoot(page, surface, attempts);
|
|
777
|
+
if (deferredSurfaceRoot) {
|
|
778
|
+
surfaceRoot = deferredSurfaceRoot;
|
|
779
|
+
locatorRoot = targetUsesSurfaceAsPrimaryLocator(target, surface) ? baseRoot : surfaceRoot;
|
|
780
|
+
await tryRankedCandidates();
|
|
691
781
|
}
|
|
692
782
|
}
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
await tryRankedCandidates();
|
|
701
|
-
}
|
|
702
|
-
}
|
|
703
|
-
if (!resolvedBy && !lastError && surfaceRoot) {
|
|
704
|
-
const recoveredLocator = await recoverLocatorFromSurfaceRoot(surfaceRoot, target, action, attempts);
|
|
705
|
-
if (recoveredLocator) {
|
|
706
|
-
await attemptResolvedLocator(recoveredLocator, 'surface-descendant', {
|
|
707
|
-
skipDomSignature: true,
|
|
708
|
-
});
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
if (!resolvedBy) {
|
|
712
|
-
if (sawDomSignatureMismatch) {
|
|
713
|
-
staleReason = 'dom-signature-mismatch';
|
|
714
|
-
throw new Error('stale_target_dom_signature_changed');
|
|
715
|
-
}
|
|
716
|
-
if (sawDisabledTarget) {
|
|
717
|
-
setTargetAvailability(session, targetRef, 'gated', 'disabled');
|
|
718
|
-
throw new Error('target_disabled');
|
|
719
|
-
}
|
|
720
|
-
if (sawReadonlyTarget && (action === 'fill' || action === 'type')) {
|
|
721
|
-
setTargetAvailability(session, targetRef, 'gated', 'readonly');
|
|
722
|
-
throw new Error('target_readonly');
|
|
723
|
-
}
|
|
724
|
-
if (!lastError &&
|
|
725
|
-
target.surfaceRef &&
|
|
726
|
-
(target.acceptancePolicy === 'selection' || target.acceptancePolicy === 'date-selection')) {
|
|
727
|
-
setTargetAvailability(session, targetRef, 'surface-inactive', 'surface-not-active');
|
|
728
|
-
throw new Error('target_surface_inactive');
|
|
783
|
+
if (!resolvedBy && !lastError && surfaceRoot) {
|
|
784
|
+
const recoveredLocator = await recoverLocatorFromSurfaceRoot(surfaceRoot, target, action, attempts);
|
|
785
|
+
if (recoveredLocator) {
|
|
786
|
+
await attemptResolvedLocator(recoveredLocator, 'surface-descendant', {
|
|
787
|
+
skipDomSignature: true,
|
|
788
|
+
});
|
|
789
|
+
}
|
|
729
790
|
}
|
|
730
791
|
if (!resolvedBy) {
|
|
792
|
+
if (sawDomSignatureMismatch) {
|
|
793
|
+
staleReason = 'dom-signature-mismatch';
|
|
794
|
+
throw new Error('stale_target_dom_signature_changed');
|
|
795
|
+
}
|
|
796
|
+
if (sawDisabledTarget) {
|
|
797
|
+
setTargetAvailability(session, targetRef, 'gated', 'disabled');
|
|
798
|
+
throw new Error('target_disabled');
|
|
799
|
+
}
|
|
800
|
+
if (sawReadonlyTarget && (action === 'fill' || action === 'type')) {
|
|
801
|
+
setTargetAvailability(session, targetRef, 'gated', 'readonly');
|
|
802
|
+
throw new Error('target_readonly');
|
|
803
|
+
}
|
|
731
804
|
if (!lastError &&
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
throw new Error('stale_target_locator_resolution_failed');
|
|
805
|
+
target.surfaceRef &&
|
|
806
|
+
(target.acceptancePolicy === 'selection' || target.acceptancePolicy === 'date-selection')) {
|
|
807
|
+
setTargetAvailability(session, targetRef, 'surface-inactive', 'surface-not-active');
|
|
808
|
+
throw new Error('target_surface_inactive');
|
|
737
809
|
}
|
|
738
|
-
if (
|
|
739
|
-
|
|
810
|
+
if (!resolvedBy) {
|
|
811
|
+
if (!lastError &&
|
|
812
|
+
(target.controlFamily === 'text-input' ||
|
|
813
|
+
target.controlFamily === 'select' ||
|
|
814
|
+
target.controlFamily === 'datepicker')) {
|
|
815
|
+
staleReason = 'locator-resolution-failed';
|
|
816
|
+
throw new Error('stale_target_locator_resolution_failed');
|
|
817
|
+
}
|
|
818
|
+
if (lastError) {
|
|
819
|
+
throw lastError;
|
|
820
|
+
}
|
|
821
|
+
throw new Error('deterministic_target_resolution_failed');
|
|
740
822
|
}
|
|
741
|
-
throw new Error('deterministic_target_resolution_failed');
|
|
742
823
|
}
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
currentPageRef = finalPageRef;
|
|
760
|
-
}
|
|
761
|
-
else {
|
|
762
|
-
const syncedPage = await syncSessionPage(session, target.pageRef, page);
|
|
763
|
-
currentPageRef = target.pageRef;
|
|
764
|
-
if (startingPageUrl && syncedPage.url && syncedPage.url !== startingPageUrl) {
|
|
765
|
-
clearProtectedExposure(session, target.pageRef);
|
|
824
|
+
const popup = await popupPromise;
|
|
825
|
+
const latePage = !popup && watchForNewPage ? await waitForLatePage(browser, beforePages) : null;
|
|
826
|
+
if (latePage) {
|
|
827
|
+
attempts.push('late-page-captured');
|
|
828
|
+
}
|
|
829
|
+
const discoveredPage = popup ?? latePage;
|
|
830
|
+
const afterPages = discoveredPage ? [...beforePages, discoveredPage] : listPages(browser);
|
|
831
|
+
const capturedPopup = await capturePopupIfOpened(session, beforePages, afterPages, target.pageRef, attempts);
|
|
832
|
+
let finalPageRef = target.pageRef;
|
|
833
|
+
if (capturedPopup) {
|
|
834
|
+
await syncSessionPage(session, capturedPopup.page.pageRef, capturedPopup.popup, {
|
|
835
|
+
settleTimeoutMs: 1_500,
|
|
836
|
+
});
|
|
837
|
+
setCurrentPage(session, capturedPopup.page.pageRef);
|
|
838
|
+
finalPageRef = capturedPopup.page.pageRef;
|
|
839
|
+
currentPageRef = finalPageRef;
|
|
766
840
|
}
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
if (acceptance.polls > 1) {
|
|
773
|
-
attempts.push(`acceptance.polled:${acceptance.polls}`);
|
|
841
|
+
else {
|
|
842
|
+
const syncedPage = await syncSessionPage(session, target.pageRef, page);
|
|
843
|
+
currentPageRef = target.pageRef;
|
|
844
|
+
if (startingPageUrl && syncedPage.url && syncedPage.url !== startingPageUrl) {
|
|
845
|
+
clearProtectedExposure(session, target.pageRef);
|
|
774
846
|
}
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
attempts.push(`acceptance.failed:${finalProgressProbe.policy}`);
|
|
784
|
-
throw new Error(`action_postcondition_failed:${finalProgressProbe.policy}`);
|
|
847
|
+
const progressProbeForVerification = progressProbe;
|
|
848
|
+
if (progressProbeForVerification) {
|
|
849
|
+
const finalProgressProbe = progressProbeForVerification;
|
|
850
|
+
const acceptance = await waitForAcceptanceProbe(finalProgressProbe);
|
|
851
|
+
const afterPageObservation = acceptance.afterPageObservation;
|
|
852
|
+
const accepted = acceptance.accepted;
|
|
853
|
+
if (acceptance.polls > 1) {
|
|
854
|
+
attempts.push(`acceptance.polled:${acceptance.polls}`);
|
|
785
855
|
}
|
|
786
|
-
if (
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
856
|
+
if (!accepted) {
|
|
857
|
+
if (finalProgressProbe.policy === 'value-change') {
|
|
858
|
+
attempts.push(`acceptance.failed:${finalProgressProbe.policy}`);
|
|
859
|
+
throw new Error('action_postcondition_failed:value-change');
|
|
860
|
+
}
|
|
861
|
+
if ((finalProgressProbe.policy === 'selection' ||
|
|
862
|
+
finalProgressProbe.policy === 'date-selection') &&
|
|
863
|
+
finalProgressProbe.expectedValue !== null) {
|
|
864
|
+
attempts.push(`acceptance.failed:${finalProgressProbe.policy}`);
|
|
865
|
+
throw new Error(`action_postcondition_failed:${finalProgressProbe.policy}`);
|
|
866
|
+
}
|
|
867
|
+
if (finalProgressProbe.policy === 'submit') {
|
|
868
|
+
const submitResolution = await resolveSubmitResult(finalProgressProbe, afterPageObservation);
|
|
869
|
+
if (submitResolution.acceptAsProgress) {
|
|
870
|
+
attempts.push(`submit-resolution:${submitResolution.finalVerdict}`);
|
|
871
|
+
if (submitResolution.claims.some((claim) => claim.kind === 'soft_result_candidate')) {
|
|
872
|
+
attempts.push('submit-resolution:soft-result-candidate');
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
else {
|
|
876
|
+
attempts.push(`acceptance.failed:${finalProgressProbe.policy}`);
|
|
877
|
+
attempts.push('no-progress.detected');
|
|
878
|
+
noProgressDiagnosis = await diagnoseNoObservableProgress(page, finalProgressProbe.locator);
|
|
879
|
+
if (noProgressDiagnosis) {
|
|
880
|
+
attempts.push(`no-progress.diagnosis:${noProgressDiagnosis.kind}`);
|
|
881
|
+
}
|
|
882
|
+
throw new Error('no_observable_progress');
|
|
792
883
|
}
|
|
793
884
|
}
|
|
794
885
|
else {
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
886
|
+
const afterLocatorObservation = finalProgressProbe.trackedStateKeys.length > 0
|
|
887
|
+
? await captureLocatorState(finalProgressProbe.locator, finalProgressProbe.trackedStateKeys)
|
|
888
|
+
: null;
|
|
889
|
+
const afterContextHash = await captureLocatorContextHash(finalProgressProbe.locator);
|
|
890
|
+
const hasComparableSignal = finalProgressProbe.trackedStateKeys.length > 0 ||
|
|
891
|
+
Boolean(finalProgressProbe.beforeContextHash || afterContextHash) ||
|
|
892
|
+
Boolean(finalProgressProbe.beforePage || afterPageObservation);
|
|
893
|
+
const pageProgressChanged = finalProgressProbe.policy === 'generic-click'
|
|
894
|
+
? genericClickObservationChanged(finalProgressProbe.beforePage, afterPageObservation)
|
|
895
|
+
: pageObservationChanged(finalProgressProbe.beforePage, afterPageObservation);
|
|
896
|
+
if (hasComparableSignal &&
|
|
897
|
+
!pageProgressChanged &&
|
|
898
|
+
finalProgressProbe.beforeContextHash === afterContextHash &&
|
|
899
|
+
!locatorStateChanged(finalProgressProbe.beforeLocator, afterLocatorObservation)) {
|
|
900
|
+
attempts.push('no-progress.detected');
|
|
901
|
+
noProgressDiagnosis = await diagnoseNoObservableProgress(page, finalProgressProbe.locator);
|
|
902
|
+
if (noProgressDiagnosis) {
|
|
903
|
+
attempts.push(`no-progress.diagnosis:${noProgressDiagnosis.kind}`);
|
|
904
|
+
}
|
|
905
|
+
throw new Error('no_observable_progress');
|
|
800
906
|
}
|
|
801
|
-
throw new Error('no_observable_progress');
|
|
802
907
|
}
|
|
803
908
|
}
|
|
804
909
|
else {
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
:
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
Boolean(finalProgressProbe.beforePage || afterPageObservation);
|
|
812
|
-
const pageProgressChanged = finalProgressProbe.policy === 'generic-click'
|
|
813
|
-
? genericClickObservationChanged(finalProgressProbe.beforePage, afterPageObservation)
|
|
814
|
-
: pageObservationChanged(finalProgressProbe.beforePage, afterPageObservation);
|
|
815
|
-
if (hasComparableSignal &&
|
|
816
|
-
!pageProgressChanged &&
|
|
817
|
-
finalProgressProbe.beforeContextHash === afterContextHash &&
|
|
818
|
-
!locatorStateChanged(finalProgressProbe.beforeLocator, afterLocatorObservation)) {
|
|
819
|
-
attempts.push('no-progress.detected');
|
|
820
|
-
noProgressDiagnosis = await diagnoseNoObservableProgress(page, finalProgressProbe.locator);
|
|
821
|
-
if (noProgressDiagnosis) {
|
|
822
|
-
attempts.push(`no-progress.diagnosis:${noProgressDiagnosis.kind}`);
|
|
823
|
-
}
|
|
824
|
-
throw new Error('no_observable_progress');
|
|
910
|
+
partialProgressResult = await partialProgressForAliasedSelection({
|
|
911
|
+
requestedAction,
|
|
912
|
+
probe: finalProgressProbe,
|
|
913
|
+
});
|
|
914
|
+
if (partialProgressResult) {
|
|
915
|
+
attempts.push('outcome.partial-progress:selection-not-complete');
|
|
825
916
|
}
|
|
826
917
|
}
|
|
827
918
|
}
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
await trace.finishSuccess();
|
|
837
|
-
outputJSON({
|
|
838
|
-
success: true,
|
|
839
|
-
targetRef,
|
|
840
|
-
action,
|
|
841
|
-
value: actionValue,
|
|
842
|
-
resolvedBy,
|
|
843
|
-
locatorStrategy,
|
|
844
|
-
pageRef: finalPageRef,
|
|
845
|
-
attempts,
|
|
846
|
-
popup: Boolean(capturedPopup),
|
|
847
|
-
overlayHandled: attempts.includes('overlay.dismissed'),
|
|
848
|
-
iframe: Boolean(target.framePath?.length),
|
|
849
|
-
jsFallback: attempts.some((attempt) => attempt.startsWith('locator.evaluate.')),
|
|
850
|
-
...(recoveredAfterError
|
|
851
|
-
? {
|
|
852
|
-
recoveredAfterError: true,
|
|
853
|
-
recoveredAcceptancePolicy: recoveredAcceptancePolicy ?? undefined,
|
|
919
|
+
else if (recoveredProgressProbe) {
|
|
920
|
+
partialProgressResult = await partialProgressForAliasedSelection({
|
|
921
|
+
requestedAction,
|
|
922
|
+
probe: recoveredProgressProbe,
|
|
923
|
+
});
|
|
924
|
+
if (partialProgressResult) {
|
|
925
|
+
attempts.push('outcome.partial-progress:selection-not-complete');
|
|
926
|
+
}
|
|
854
927
|
}
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
928
|
+
}
|
|
929
|
+
if (resolvedBy === 'playwright-locator') {
|
|
930
|
+
incrementMetric(session, 'deterministicActions');
|
|
931
|
+
}
|
|
932
|
+
bumpPageScopeEpoch(session, target.pageRef);
|
|
933
|
+
recordActionResult(session, true, Date.now() - startedAt);
|
|
934
|
+
saveSession(session);
|
|
935
|
+
await trace.finishSuccess();
|
|
936
|
+
captureStepSnapshotBestEffort({
|
|
937
|
+
session,
|
|
938
|
+
step: actStep,
|
|
939
|
+
phase: 'after',
|
|
940
|
+
pageRef: finalPageRef,
|
|
941
|
+
});
|
|
942
|
+
appendCommandLifecycleEventBestEffort({
|
|
943
|
+
step: actStep,
|
|
944
|
+
phase: 'completed',
|
|
945
|
+
attributes: {
|
|
946
|
+
outcomeType: partialProgressResult?.outcomeType ?? 'action_completed',
|
|
947
|
+
targetRef,
|
|
948
|
+
action: requestedAction,
|
|
949
|
+
pageRef: finalPageRef,
|
|
950
|
+
},
|
|
951
|
+
});
|
|
952
|
+
finalizeActStepBestEffort(runId, actStep?.stepId, {
|
|
953
|
+
success: true,
|
|
954
|
+
outcomeType: partialProgressResult?.outcomeType ?? 'action_completed',
|
|
955
|
+
message: partialProgressResult?.message ?? 'The requested action completed successfully.',
|
|
956
|
+
});
|
|
957
|
+
await exportRunStepToOtlpHttpJsonBestEffort(runId, actStep?.stepId);
|
|
958
|
+
outputJSON({
|
|
959
|
+
success: true,
|
|
960
|
+
targetRef,
|
|
961
|
+
action: requestedAction,
|
|
962
|
+
...(action !== requestedAction ? { executedAs: action } : {}),
|
|
963
|
+
value: actionValue,
|
|
964
|
+
resolvedBy,
|
|
965
|
+
locatorStrategy,
|
|
966
|
+
pageRef: finalPageRef,
|
|
967
|
+
attempts,
|
|
968
|
+
popup: Boolean(capturedPopup),
|
|
969
|
+
overlayHandled: attempts.includes('overlay.dismissed'),
|
|
970
|
+
iframe: Boolean(target.framePath?.length),
|
|
971
|
+
jsFallback: attempts.some((attempt) => attempt.startsWith('locator.evaluate.')),
|
|
972
|
+
...(recoveredAfterError
|
|
973
|
+
? {
|
|
974
|
+
recoveredAfterError: true,
|
|
975
|
+
recoveredAcceptancePolicy: recoveredAcceptancePolicy ?? undefined,
|
|
976
|
+
}
|
|
977
|
+
: {}),
|
|
978
|
+
...(partialProgressResult ?? {}),
|
|
979
|
+
durationMs: Date.now() - startedAt,
|
|
980
|
+
metrics: session.runtime?.metrics,
|
|
981
|
+
});
|
|
865
982
|
}
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
983
|
+
catch (err) {
|
|
984
|
+
failureMessage = `Act failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
985
|
+
recordActionResult(session, false, Date.now() - startedAt);
|
|
986
|
+
if (staleReason) {
|
|
987
|
+
markTargetLifecycle(session, targetRef, 'stale', staleReason);
|
|
988
|
+
}
|
|
989
|
+
saveSession(session);
|
|
990
|
+
if (currentPage) {
|
|
991
|
+
try {
|
|
992
|
+
const protectedExposure = getProtectedExposure(session, currentPageRef);
|
|
993
|
+
if (protectedExposure) {
|
|
994
|
+
await trace.finishSuccess();
|
|
995
|
+
failureArtifacts = buildProtectedArtifactsSuppressed(protectedExposure);
|
|
996
|
+
}
|
|
997
|
+
else {
|
|
998
|
+
failureArtifacts = await captureActionFailureArtifacts({
|
|
999
|
+
page: currentPage,
|
|
1000
|
+
targetRef,
|
|
1001
|
+
action: requestedAction,
|
|
1002
|
+
pageRef: currentPageRef,
|
|
1003
|
+
attempts,
|
|
1004
|
+
locatorStrategy,
|
|
1005
|
+
popup: attempts.includes('popup-captured'),
|
|
1006
|
+
overlayHandled: attempts.includes('overlay.dismissed'),
|
|
1007
|
+
iframe: Boolean(target.framePath?.length),
|
|
1008
|
+
jsFallback: attempts.some((attempt) => attempt.startsWith('locator.evaluate.')),
|
|
1009
|
+
durationMs: Date.now() - startedAt,
|
|
1010
|
+
error: failureMessage,
|
|
1011
|
+
finishTrace: (artifactDir) => trace.finishFailure(artifactDir),
|
|
1012
|
+
});
|
|
1013
|
+
}
|
|
873
1014
|
}
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
page: currentPage,
|
|
877
|
-
targetRef,
|
|
878
|
-
action,
|
|
879
|
-
pageRef: currentPageRef,
|
|
880
|
-
attempts,
|
|
881
|
-
locatorStrategy,
|
|
882
|
-
popup: attempts.includes('popup-captured'),
|
|
883
|
-
overlayHandled: attempts.includes('overlay.dismissed'),
|
|
884
|
-
iframe: Boolean(target.framePath?.length),
|
|
885
|
-
jsFallback: attempts.some((attempt) => attempt.startsWith('locator.evaluate.')),
|
|
886
|
-
durationMs: Date.now() - startedAt,
|
|
887
|
-
error: failureMessage,
|
|
888
|
-
finishTrace: (artifactDir) => trace.finishFailure(artifactDir),
|
|
889
|
-
});
|
|
1015
|
+
catch {
|
|
1016
|
+
// Best effort only. Preserve the original action failure.
|
|
890
1017
|
}
|
|
891
1018
|
}
|
|
892
|
-
catch {
|
|
893
|
-
// Best effort only. Preserve the original action failure.
|
|
894
|
-
}
|
|
895
1019
|
}
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
1020
|
+
finally {
|
|
1021
|
+
if (browser) {
|
|
1022
|
+
await disconnectPlaywright(browser);
|
|
1023
|
+
}
|
|
900
1024
|
}
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
1025
|
+
if (failureMessage) {
|
|
1026
|
+
if (failureMessage === 'Act failed: no_observable_progress' && noProgressDiagnosis) {
|
|
1027
|
+
failureMessage = `Act failed: no_observable_progress (${noProgressDiagnosis.kind})`;
|
|
1028
|
+
}
|
|
1029
|
+
const failureContract = describeActFailure({
|
|
1030
|
+
failureMessage,
|
|
1031
|
+
staleReason,
|
|
1032
|
+
diagnosis: noProgressDiagnosis,
|
|
1033
|
+
});
|
|
1034
|
+
const protectedExposure = getProtectedExposure(session, currentPageRef);
|
|
1035
|
+
const outputDiagnosis = protectedExposure && noProgressDiagnosis
|
|
1036
|
+
? redactNoObservableProgressDiagnosis(noProgressDiagnosis)
|
|
1037
|
+
: noProgressDiagnosis;
|
|
1038
|
+
const artifactManifestId = persistActArtifactManifestBestEffort(runId, actStep?.stepId, failureArtifacts ?? null);
|
|
1039
|
+
captureStepSnapshotBestEffort({
|
|
1040
|
+
session,
|
|
1041
|
+
step: actStep,
|
|
1042
|
+
phase: 'point-in-time',
|
|
1043
|
+
pageRef: currentPageRef,
|
|
1044
|
+
artifactRefs: buildActSnapshotArtifactRefs(failureArtifacts ?? null),
|
|
1045
|
+
});
|
|
1046
|
+
appendCommandLifecycleEventBestEffort({
|
|
1047
|
+
step: actStep,
|
|
1048
|
+
phase: 'failed',
|
|
1049
|
+
attributes: {
|
|
1050
|
+
outcomeType: failureContract.outcomeType,
|
|
1051
|
+
targetRef,
|
|
1052
|
+
action: requestedAction,
|
|
1053
|
+
pageRef: currentPageRef,
|
|
1054
|
+
...(artifactManifestId ? { artifactManifestId } : {}),
|
|
1055
|
+
},
|
|
1056
|
+
});
|
|
1057
|
+
finalizeActStepBestEffort(runId, actStep?.stepId, {
|
|
1058
|
+
success: false,
|
|
1059
|
+
outcomeType: failureContract.outcomeType,
|
|
1060
|
+
message: failureContract.message,
|
|
1061
|
+
reason: failureContract.reason,
|
|
1062
|
+
artifactManifestId,
|
|
1063
|
+
});
|
|
1064
|
+
await exportRunStepToOtlpHttpJsonBestEffort(runId, actStep?.stepId);
|
|
1065
|
+
outputFailure({
|
|
1066
|
+
error: failureMessage,
|
|
1067
|
+
outcomeType: failureContract.outcomeType,
|
|
1068
|
+
message: failureContract.message,
|
|
1069
|
+
reason: failureContract.reason,
|
|
1070
|
+
targetRef,
|
|
1071
|
+
action: requestedAction,
|
|
1072
|
+
...(action !== requestedAction ? { executedAs: action } : {}),
|
|
1073
|
+
value: actionValue,
|
|
1074
|
+
pageRef: currentPageRef,
|
|
1075
|
+
locatorStrategy,
|
|
1076
|
+
attempts,
|
|
1077
|
+
popup: attempts.includes('popup-captured'),
|
|
1078
|
+
overlayHandled: attempts.includes('overlay.dismissed'),
|
|
1079
|
+
iframe: Boolean(target.framePath?.length),
|
|
1080
|
+
jsFallback: attempts.some((attempt) => attempt.startsWith('locator.evaluate.')),
|
|
1081
|
+
durationMs: Date.now() - startedAt,
|
|
1082
|
+
staleTarget: Boolean(staleReason),
|
|
1083
|
+
staleReason: staleReason ?? undefined,
|
|
1084
|
+
diagnosis: outputDiagnosis ?? undefined,
|
|
1085
|
+
artifacts: failureArtifacts,
|
|
1086
|
+
metrics: session.runtime?.metrics,
|
|
1087
|
+
});
|
|
905
1088
|
}
|
|
906
|
-
|
|
907
|
-
failureMessage,
|
|
908
|
-
staleReason,
|
|
909
|
-
diagnosis: noProgressDiagnosis,
|
|
910
|
-
});
|
|
911
|
-
const protectedExposure = getProtectedExposure(session, currentPageRef);
|
|
912
|
-
const outputDiagnosis = protectedExposure && noProgressDiagnosis
|
|
913
|
-
? redactNoObservableProgressDiagnosis(noProgressDiagnosis)
|
|
914
|
-
: noProgressDiagnosis;
|
|
915
|
-
outputFailure({
|
|
916
|
-
error: failureMessage,
|
|
917
|
-
outcomeType: failureContract.outcomeType,
|
|
918
|
-
message: failureContract.message,
|
|
919
|
-
reason: failureContract.reason,
|
|
920
|
-
targetRef,
|
|
921
|
-
action,
|
|
922
|
-
value: actionValue,
|
|
923
|
-
pageRef: currentPageRef,
|
|
924
|
-
locatorStrategy,
|
|
925
|
-
attempts,
|
|
926
|
-
popup: attempts.includes('popup-captured'),
|
|
927
|
-
overlayHandled: attempts.includes('overlay.dismissed'),
|
|
928
|
-
iframe: Boolean(target.framePath?.length),
|
|
929
|
-
jsFallback: attempts.some((attempt) => attempt.startsWith('locator.evaluate.')),
|
|
930
|
-
durationMs: Date.now() - startedAt,
|
|
931
|
-
staleTarget: Boolean(staleReason),
|
|
932
|
-
staleReason: staleReason ?? undefined,
|
|
933
|
-
diagnosis: outputDiagnosis ?? undefined,
|
|
934
|
-
artifacts: failureArtifacts,
|
|
935
|
-
metrics: session.runtime?.metrics,
|
|
936
|
-
});
|
|
937
|
-
}
|
|
1089
|
+
});
|
|
938
1090
|
}
|