@descryy/mcp 0.11.3 → 0.11.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +20 -20
- package/dist/action-handshake.d.ts +0 -38
- package/dist/action-handshake.d.ts.map +0 -1
- package/dist/action-handshake.js +0 -127
- package/dist/action-handshake.js.map +0 -1
- package/dist/bin/descry-mcp.d.ts +0 -7
- package/dist/bin/descry-mcp.d.ts.map +0 -1
- package/dist/bin/descry-mcp.js +0 -72
- package/dist/bin/descry-mcp.js.map +0 -1
- package/dist/browser/auth.d.ts +0 -84
- package/dist/browser/auth.d.ts.map +0 -1
- package/dist/browser/auth.js +0 -233
- package/dist/browser/auth.js.map +0 -1
- package/dist/browser/capability-probe.d.ts +0 -109
- package/dist/browser/capability-probe.d.ts.map +0 -1
- package/dist/browser/capability-probe.js +0 -201
- package/dist/browser/capability-probe.js.map +0 -1
- package/dist/browser/driver.d.ts +0 -570
- package/dist/browser/driver.d.ts.map +0 -1
- package/dist/browser/driver.js +0 -116
- package/dist/browser/driver.js.map +0 -1
- package/dist/browser/evidence.d.ts +0 -28
- package/dist/browser/evidence.d.ts.map +0 -1
- package/dist/browser/evidence.js +0 -82
- package/dist/browser/evidence.js.map +0 -1
- package/dist/browser/fake-driver.d.ts +0 -116
- package/dist/browser/fake-driver.d.ts.map +0 -1
- package/dist/browser/fake-driver.js +0 -416
- package/dist/browser/fake-driver.js.map +0 -1
- package/dist/browser/fault-attribution.d.ts +0 -178
- package/dist/browser/fault-attribution.d.ts.map +0 -1
- package/dist/browser/fault-attribution.js +0 -266
- package/dist/browser/fault-attribution.js.map +0 -1
- package/dist/browser/graph-write.d.ts +0 -45
- package/dist/browser/graph-write.d.ts.map +0 -1
- package/dist/browser/graph-write.js +0 -103
- package/dist/browser/graph-write.js.map +0 -1
- package/dist/browser/identity-graph-write.d.ts +0 -55
- package/dist/browser/identity-graph-write.d.ts.map +0 -1
- package/dist/browser/identity-graph-write.js +0 -45
- package/dist/browser/identity-graph-write.js.map +0 -1
- package/dist/browser/page-probe.d.ts +0 -81
- package/dist/browser/page-probe.d.ts.map +0 -1
- package/dist/browser/page-probe.js +0 -202
- package/dist/browser/page-probe.js.map +0 -1
- package/dist/browser/playwright-driver.d.ts +0 -114
- package/dist/browser/playwright-driver.d.ts.map +0 -1
- package/dist/browser/playwright-driver.js +0 -1479
- package/dist/browser/playwright-driver.js.map +0 -1
- package/dist/browser/provider.d.ts +0 -26
- package/dist/browser/provider.d.ts.map +0 -1
- package/dist/browser/provider.js +0 -14
- package/dist/browser/provider.js.map +0 -1
- package/dist/browser/reach-recording-session.d.ts +0 -44
- package/dist/browser/reach-recording-session.d.ts.map +0 -1
- package/dist/browser/reach-recording-session.js +0 -143
- package/dist/browser/reach-recording-session.js.map +0 -1
- package/dist/browser/reachability.d.ts +0 -168
- package/dist/browser/reachability.d.ts.map +0 -1
- package/dist/browser/reachability.js +0 -294
- package/dist/browser/reachability.js.map +0 -1
- package/dist/browser/registry.d.ts +0 -126
- package/dist/browser/registry.d.ts.map +0 -1
- package/dist/browser/registry.js +0 -177
- package/dist/browser/registry.js.map +0 -1
- package/dist/browser/scenario-provenance.d.ts +0 -23
- package/dist/browser/scenario-provenance.d.ts.map +0 -1
- package/dist/browser/scenario-provenance.js +0 -73
- package/dist/browser/scenario-provenance.js.map +0 -1
- package/dist/browser/scenario-resolve.d.ts +0 -28
- package/dist/browser/scenario-resolve.d.ts.map +0 -1
- package/dist/browser/scenario-resolve.js +0 -96
- package/dist/browser/scenario-resolve.js.map +0 -1
- package/dist/browser/scenario-runner.d.ts +0 -72
- package/dist/browser/scenario-runner.d.ts.map +0 -1
- package/dist/browser/scenario-runner.js +0 -353
- package/dist/browser/scenario-runner.js.map +0 -1
- package/dist/browser/stack-parser.d.ts +0 -19
- package/dist/browser/stack-parser.d.ts.map +0 -1
- package/dist/browser/stack-parser.js +0 -85
- package/dist/browser/stack-parser.js.map +0 -1
- package/dist/browser/tool-support.d.ts +0 -71
- package/dist/browser/tool-support.d.ts.map +0 -1
- package/dist/browser/tool-support.js +0 -216
- package/dist/browser/tool-support.js.map +0 -1
- package/dist/browser/url-scheme.d.ts +0 -14
- package/dist/browser/url-scheme.d.ts.map +0 -1
- package/dist/browser/url-scheme.js +0 -38
- package/dist/browser/url-scheme.js.map +0 -1
- package/dist/browser/wait-target.d.ts +0 -37
- package/dist/browser/wait-target.d.ts.map +0 -1
- package/dist/browser/wait-target.js +0 -54
- package/dist/browser/wait-target.js.map +0 -1
- package/dist/cancellation.d.ts +0 -20
- package/dist/cancellation.d.ts.map +0 -1
- package/dist/cancellation.js +0 -41
- package/dist/cancellation.js.map +0 -1
- package/dist/capped.d.ts +0 -17
- package/dist/capped.d.ts.map +0 -1
- package/dist/capped.js +0 -15
- package/dist/capped.js.map +0 -1
- package/dist/credential-store.d.ts +0 -22
- package/dist/credential-store.d.ts.map +0 -1
- package/dist/credential-store.js +0 -33
- package/dist/credential-store.js.map +0 -1
- package/dist/disclosure-ledger.d.ts +0 -14
- package/dist/disclosure-ledger.d.ts.map +0 -1
- package/dist/disclosure-ledger.js +0 -18
- package/dist/disclosure-ledger.js.map +0 -1
- package/dist/evidence/observed-route-evidence.d.ts +0 -25
- package/dist/evidence/observed-route-evidence.d.ts.map +0 -1
- package/dist/evidence/observed-route-evidence.js +0 -76
- package/dist/evidence/observed-route-evidence.js.map +0 -1
- package/dist/heap.d.ts +0 -61
- package/dist/heap.d.ts.map +0 -1
- package/dist/heap.js +0 -82
- package/dist/heap.js.map +0 -1
- package/dist/heartbeat.d.ts +0 -46
- package/dist/heartbeat.d.ts.map +0 -1
- package/dist/heartbeat.js +0 -66
- package/dist/heartbeat.js.map +0 -1
- package/dist/index.d.ts +0 -32
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -19
- package/dist/index.js.map +0 -1
- package/dist/module-trust.d.ts +0 -37
- package/dist/module-trust.d.ts.map +0 -1
- package/dist/module-trust.js +0 -77
- package/dist/module-trust.js.map +0 -1
- package/dist/path-confinement.d.ts +0 -31
- package/dist/path-confinement.d.ts.map +0 -1
- package/dist/path-confinement.js +0 -44
- package/dist/path-confinement.js.map +0 -1
- package/dist/planner/predict-then-propose.d.ts +0 -134
- package/dist/planner/predict-then-propose.d.ts.map +0 -1
- package/dist/planner/predict-then-propose.js +0 -138
- package/dist/planner/predict-then-propose.js.map +0 -1
- package/dist/planner/propose-journey.d.ts +0 -75
- package/dist/planner/propose-journey.d.ts.map +0 -1
- package/dist/planner/propose-journey.js +0 -96
- package/dist/planner/propose-journey.js.map +0 -1
- package/dist/protocol.d.ts +0 -57
- package/dist/protocol.d.ts.map +0 -1
- package/dist/protocol.js +0 -90
- package/dist/protocol.js.map +0 -1
- package/dist/registry.d.ts +0 -103
- package/dist/registry.d.ts.map +0 -1
- package/dist/registry.js +0 -319
- package/dist/registry.js.map +0 -1
- package/dist/render.d.ts +0 -580
- package/dist/render.d.ts.map +0 -1
- package/dist/render.js +0 -867
- package/dist/render.js.map +0 -1
- package/dist/runtime-registry.d.ts +0 -22
- package/dist/runtime-registry.d.ts.map +0 -1
- package/dist/runtime-registry.js +0 -142
- package/dist/runtime-registry.js.map +0 -1
- package/dist/sandbox-defaults.d.ts +0 -41
- package/dist/sandbox-defaults.d.ts.map +0 -1
- package/dist/sandbox-defaults.js +0 -14
- package/dist/sandbox-defaults.js.map +0 -1
- package/dist/scenarios/browser-run-plan-projection.d.ts +0 -161
- package/dist/scenarios/browser-run-plan-projection.d.ts.map +0 -1
- package/dist/scenarios/browser-run-plan-projection.js +0 -264
- package/dist/scenarios/browser-run-plan-projection.js.map +0 -1
- package/dist/scenarios/credential-ref.d.ts +0 -102
- package/dist/scenarios/credential-ref.d.ts.map +0 -1
- package/dist/scenarios/credential-ref.js +0 -148
- package/dist/scenarios/credential-ref.js.map +0 -1
- package/dist/scenarios/index.d.ts +0 -7
- package/dist/scenarios/index.d.ts.map +0 -1
- package/dist/scenarios/index.js +0 -7
- package/dist/scenarios/index.js.map +0 -1
- package/dist/scenarios/parse.d.ts +0 -18
- package/dist/scenarios/parse.d.ts.map +0 -1
- package/dist/scenarios/parse.js +0 -212
- package/dist/scenarios/parse.js.map +0 -1
- package/dist/scenarios/scenario.d.ts +0 -93
- package/dist/scenarios/scenario.d.ts.map +0 -1
- package/dist/scenarios/scenario.js +0 -27
- package/dist/scenarios/scenario.js.map +0 -1
- package/dist/scenarios/secret-ref.d.ts +0 -86
- package/dist/scenarios/secret-ref.d.ts.map +0 -1
- package/dist/scenarios/secret-ref.js +0 -125
- package/dist/scenarios/secret-ref.js.map +0 -1
- package/dist/scenarios/storage.d.ts +0 -21
- package/dist/scenarios/storage.d.ts.map +0 -1
- package/dist/scenarios/storage.js +0 -103
- package/dist/scenarios/storage.js.map +0 -1
- package/dist/server.d.ts +0 -61
- package/dist/server.d.ts.map +0 -1
- package/dist/server.js +0 -541
- package/dist/server.js.map +0 -1
- package/dist/session.d.ts +0 -243
- package/dist/session.d.ts.map +0 -1
- package/dist/session.js +0 -602
- package/dist/session.js.map +0 -1
- package/dist/tools/alias-kit.d.ts +0 -29
- package/dist/tools/alias-kit.d.ts.map +0 -1
- package/dist/tools/alias-kit.js +0 -21
- package/dist/tools/alias-kit.js.map +0 -1
- package/dist/tools/aliases.d.ts +0 -42
- package/dist/tools/aliases.d.ts.map +0 -1
- package/dist/tools/aliases.js +0 -52
- package/dist/tools/aliases.js.map +0 -1
- package/dist/tools/analyze-workspace.d.ts +0 -52
- package/dist/tools/analyze-workspace.d.ts.map +0 -1
- package/dist/tools/analyze-workspace.js +0 -207
- package/dist/tools/analyze-workspace.js.map +0 -1
- package/dist/tools/analyze.d.ts +0 -163
- package/dist/tools/analyze.d.ts.map +0 -1
- package/dist/tools/analyze.js +0 -724
- package/dist/tools/analyze.js.map +0 -1
- package/dist/tools/browser-click.d.ts +0 -18
- package/dist/tools/browser-click.d.ts.map +0 -1
- package/dist/tools/browser-click.js +0 -110
- package/dist/tools/browser-click.js.map +0 -1
- package/dist/tools/browser-close-session.d.ts +0 -39
- package/dist/tools/browser-close-session.d.ts.map +0 -1
- package/dist/tools/browser-close-session.js +0 -120
- package/dist/tools/browser-close-session.js.map +0 -1
- package/dist/tools/browser-fill.d.ts +0 -18
- package/dist/tools/browser-fill.d.ts.map +0 -1
- package/dist/tools/browser-fill.js +0 -117
- package/dist/tools/browser-fill.js.map +0 -1
- package/dist/tools/browser-navigate.d.ts +0 -15
- package/dist/tools/browser-navigate.d.ts.map +0 -1
- package/dist/tools/browser-navigate.js +0 -104
- package/dist/tools/browser-navigate.js.map +0 -1
- package/dist/tools/browser-read.d.ts +0 -33
- package/dist/tools/browser-read.d.ts.map +0 -1
- package/dist/tools/browser-read.js +0 -113
- package/dist/tools/browser-read.js.map +0 -1
- package/dist/tools/browser-run-scenario.d.ts +0 -28
- package/dist/tools/browser-run-scenario.d.ts.map +0 -1
- package/dist/tools/browser-run-scenario.js +0 -206
- package/dist/tools/browser-run-scenario.js.map +0 -1
- package/dist/tools/browser-save-scenario.d.ts +0 -19
- package/dist/tools/browser-save-scenario.d.ts.map +0 -1
- package/dist/tools/browser-save-scenario.js +0 -447
- package/dist/tools/browser-save-scenario.js.map +0 -1
- package/dist/tools/browser-select.d.ts +0 -20
- package/dist/tools/browser-select.d.ts.map +0 -1
- package/dist/tools/browser-select.js +0 -120
- package/dist/tools/browser-select.js.map +0 -1
- package/dist/tools/browser-snapshot.d.ts +0 -46
- package/dist/tools/browser-snapshot.d.ts.map +0 -1
- package/dist/tools/browser-snapshot.js +0 -127
- package/dist/tools/browser-snapshot.js.map +0 -1
- package/dist/tools/browser-start-session.d.ts +0 -24
- package/dist/tools/browser-start-session.d.ts.map +0 -1
- package/dist/tools/browser-start-session.js +0 -622
- package/dist/tools/browser-start-session.js.map +0 -1
- package/dist/tools/browser-submit.d.ts +0 -18
- package/dist/tools/browser-submit.d.ts.map +0 -1
- package/dist/tools/browser-submit.js +0 -106
- package/dist/tools/browser-submit.js.map +0 -1
- package/dist/tools/browser-type.d.ts +0 -18
- package/dist/tools/browser-type.d.ts.map +0 -1
- package/dist/tools/browser-type.js +0 -115
- package/dist/tools/browser-type.js.map +0 -1
- package/dist/tools/browser-wait-for.d.ts +0 -45
- package/dist/tools/browser-wait-for.d.ts.map +0 -1
- package/dist/tools/browser-wait-for.js +0 -150
- package/dist/tools/browser-wait-for.js.map +0 -1
- package/dist/tools/browser.d.ts +0 -69
- package/dist/tools/browser.d.ts.map +0 -1
- package/dist/tools/browser.js +0 -386
- package/dist/tools/browser.js.map +0 -1
- package/dist/tools/chain.d.ts +0 -41
- package/dist/tools/chain.d.ts.map +0 -1
- package/dist/tools/chain.js +0 -105
- package/dist/tools/chain.js.map +0 -1
- package/dist/tools/change-scope.d.ts +0 -25
- package/dist/tools/change-scope.d.ts.map +0 -1
- package/dist/tools/change-scope.js +0 -110
- package/dist/tools/change-scope.js.map +0 -1
- package/dist/tools/code-context.d.ts +0 -83
- package/dist/tools/code-context.d.ts.map +0 -1
- package/dist/tools/code-context.js +0 -416
- package/dist/tools/code-context.js.map +0 -1
- package/dist/tools/contracts.d.ts +0 -74
- package/dist/tools/contracts.d.ts.map +0 -1
- package/dist/tools/contracts.js +0 -196
- package/dist/tools/contracts.js.map +0 -1
- package/dist/tools/cross-pr.d.ts +0 -178
- package/dist/tools/cross-pr.d.ts.map +0 -1
- package/dist/tools/cross-pr.js +0 -321
- package/dist/tools/cross-pr.js.map +0 -1
- package/dist/tools/disclosure.d.ts +0 -75
- package/dist/tools/disclosure.d.ts.map +0 -1
- package/dist/tools/disclosure.js +0 -288
- package/dist/tools/disclosure.js.map +0 -1
- package/dist/tools/draw-conclusion.d.ts +0 -55
- package/dist/tools/draw-conclusion.d.ts.map +0 -1
- package/dist/tools/draw-conclusion.js +0 -259
- package/dist/tools/draw-conclusion.js.map +0 -1
- package/dist/tools/git-context.d.ts +0 -39
- package/dist/tools/git-context.d.ts.map +0 -1
- package/dist/tools/git-context.js +0 -364
- package/dist/tools/git-context.js.map +0 -1
- package/dist/tools/git-diff.d.ts +0 -45
- package/dist/tools/git-diff.d.ts.map +0 -1
- package/dist/tools/git-diff.js +0 -338
- package/dist/tools/git-diff.js.map +0 -1
- package/dist/tools/git-history.d.ts +0 -16
- package/dist/tools/git-history.d.ts.map +0 -1
- package/dist/tools/git-history.js +0 -86
- package/dist/tools/git-history.js.map +0 -1
- package/dist/tools/history.d.ts +0 -34
- package/dist/tools/history.d.ts.map +0 -1
- package/dist/tools/history.js +0 -121
- package/dist/tools/history.js.map +0 -1
- package/dist/tools/impact.d.ts +0 -46
- package/dist/tools/impact.d.ts.map +0 -1
- package/dist/tools/impact.js +0 -225
- package/dist/tools/impact.js.map +0 -1
- package/dist/tools/index.d.ts +0 -59
- package/dist/tools/index.d.ts.map +0 -1
- package/dist/tools/index.js +0 -70
- package/dist/tools/index.js.map +0 -1
- package/dist/tools/kit.d.ts +0 -165
- package/dist/tools/kit.d.ts.map +0 -1
- package/dist/tools/kit.js +0 -153
- package/dist/tools/kit.js.map +0 -1
- package/dist/tools/link-workspace.d.ts +0 -24
- package/dist/tools/link-workspace.d.ts.map +0 -1
- package/dist/tools/link-workspace.js +0 -131
- package/dist/tools/link-workspace.js.map +0 -1
- package/dist/tools/lookup.d.ts +0 -19
- package/dist/tools/lookup.d.ts.map +0 -1
- package/dist/tools/lookup.js +0 -63
- package/dist/tools/lookup.js.map +0 -1
- package/dist/tools/mark-incident.d.ts +0 -28
- package/dist/tools/mark-incident.d.ts.map +0 -1
- package/dist/tools/mark-incident.js +0 -188
- package/dist/tools/mark-incident.js.map +0 -1
- package/dist/tools/merged-browser.d.ts +0 -11
- package/dist/tools/merged-browser.d.ts.map +0 -1
- package/dist/tools/merged-browser.js +0 -20
- package/dist/tools/merged-browser.js.map +0 -1
- package/dist/tools/merged-context.d.ts +0 -11
- package/dist/tools/merged-context.d.ts.map +0 -1
- package/dist/tools/merged-context.js +0 -21
- package/dist/tools/merged-context.js.map +0 -1
- package/dist/tools/merged-observe.d.ts +0 -13
- package/dist/tools/merged-observe.d.ts.map +0 -1
- package/dist/tools/merged-observe.js +0 -21
- package/dist/tools/merged-observe.js.map +0 -1
- package/dist/tools/merged-workspace.d.ts +0 -14
- package/dist/tools/merged-workspace.d.ts.map +0 -1
- package/dist/tools/merged-workspace.js +0 -21
- package/dist/tools/merged-workspace.js.map +0 -1
- package/dist/tools/observe-runtime.d.ts +0 -250
- package/dist/tools/observe-runtime.d.ts.map +0 -1
- package/dist/tools/observe-runtime.js +0 -1753
- package/dist/tools/observe-runtime.js.map +0 -1
- package/dist/tools/observe-tests.d.ts +0 -57
- package/dist/tools/observe-tests.d.ts.map +0 -1
- package/dist/tools/observe-tests.js +0 -368
- package/dist/tools/observe-tests.js.map +0 -1
- package/dist/tools/observe.d.ts +0 -28
- package/dist/tools/observe.d.ts.map +0 -1
- package/dist/tools/observe.js +0 -330
- package/dist/tools/observe.js.map +0 -1
- package/dist/tools/pr-analysis.d.ts +0 -189
- package/dist/tools/pr-analysis.d.ts.map +0 -1
- package/dist/tools/pr-analysis.js +0 -299
- package/dist/tools/pr-analysis.js.map +0 -1
- package/dist/tools/pre-push.d.ts +0 -168
- package/dist/tools/pre-push.d.ts.map +0 -1
- package/dist/tools/pre-push.js +0 -416
- package/dist/tools/pre-push.js.map +0 -1
- package/dist/tools/predict-reach.d.ts +0 -57
- package/dist/tools/predict-reach.d.ts.map +0 -1
- package/dist/tools/predict-reach.js +0 -159
- package/dist/tools/predict-reach.js.map +0 -1
- package/dist/tools/propagation.d.ts +0 -37
- package/dist/tools/propagation.d.ts.map +0 -1
- package/dist/tools/propagation.js +0 -162
- package/dist/tools/propagation.js.map +0 -1
- package/dist/tools/propose-journey.d.ts +0 -16
- package/dist/tools/propose-journey.d.ts.map +0 -1
- package/dist/tools/propose-journey.js +0 -121
- package/dist/tools/propose-journey.js.map +0 -1
- package/dist/tools/prove-reach.d.ts +0 -74
- package/dist/tools/prove-reach.d.ts.map +0 -1
- package/dist/tools/prove-reach.js +0 -286
- package/dist/tools/prove-reach.js.map +0 -1
- package/dist/tools/questions.d.ts +0 -30
- package/dist/tools/questions.d.ts.map +0 -1
- package/dist/tools/questions.js +0 -232
- package/dist/tools/questions.js.map +0 -1
- package/dist/tools/refusal-fetch.d.ts +0 -35
- package/dist/tools/refusal-fetch.d.ts.map +0 -1
- package/dist/tools/refusal-fetch.js +0 -103
- package/dist/tools/refusal-fetch.js.map +0 -1
- package/dist/tools/runtime-incident.d.ts +0 -33
- package/dist/tools/runtime-incident.d.ts.map +0 -1
- package/dist/tools/runtime-incident.js +0 -67
- package/dist/tools/runtime-incident.js.map +0 -1
- package/dist/tools/runtime-journey-drive.d.ts +0 -102
- package/dist/tools/runtime-journey-drive.d.ts.map +0 -1
- package/dist/tools/runtime-journey-drive.js +0 -247
- package/dist/tools/runtime-journey-drive.js.map +0 -1
- package/dist/tools/scope.d.ts +0 -45
- package/dist/tools/scope.d.ts.map +0 -1
- package/dist/tools/scope.js +0 -186
- package/dist/tools/scope.js.map +0 -1
- package/dist/tools/similar-incidents.d.ts +0 -43
- package/dist/tools/similar-incidents.d.ts.map +0 -1
- package/dist/tools/similar-incidents.js +0 -179
- package/dist/tools/similar-incidents.js.map +0 -1
- package/dist/tools/validate.d.ts +0 -133
- package/dist/tools/validate.d.ts.map +0 -1
- package/dist/tools/validate.js +0 -442
- package/dist/tools/validate.js.map +0 -1
- package/dist/tools/verb.d.ts +0 -46
- package/dist/tools/verb.d.ts.map +0 -1
- package/dist/tools/verb.js +0 -78
- package/dist/tools/verb.js.map +0 -1
- package/dist/tools/verification-status.d.ts +0 -38
- package/dist/tools/verification-status.d.ts.map +0 -1
- package/dist/tools/verification-status.js +0 -203
- package/dist/tools/verification-status.js.map +0 -1
- package/dist/tools/verify-claim.d.ts +0 -29
- package/dist/tools/verify-claim.d.ts.map +0 -1
- package/dist/tools/verify-claim.js +0 -220
- package/dist/tools/verify-claim.js.map +0 -1
- package/dist/tools/workspace.d.ts +0 -28
- package/dist/tools/workspace.d.ts.map +0 -1
- package/dist/tools/workspace.js +0 -101
- package/dist/tools/workspace.js.map +0 -1
- package/dist/transport.d.ts +0 -32
- package/dist/transport.d.ts.map +0 -1
- package/dist/transport.js +0 -85
- package/dist/transport.js.map +0 -1
- package/dist/workspace-index.d.ts +0 -164
- package/dist/workspace-index.d.ts.map +0 -1
- package/dist/workspace-index.js +0 -381
- package/dist/workspace-index.js.map +0 -1
|
@@ -1,1479 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The real `BrowserDriver`, over `@descryy/runtime-browser`. Import stays
|
|
3
|
-
* dynamic — `loadRuntimeModule` is a test injection seam (fake module, no
|
|
4
|
-
* browser needed). Adds only element refs over the runtime; stack-trace parsing is caller-supplied (rule 1: no language named here).
|
|
5
|
-
*/
|
|
6
|
-
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
7
|
-
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
8
|
-
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
9
|
-
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
10
|
-
});
|
|
11
|
-
}
|
|
12
|
-
return path;
|
|
13
|
-
};
|
|
14
|
-
import { BrowserSessionCrashedError, NoSuchOptionError, NotOperableError, NotSelectableError, NotSubmittableError, UnknownElementRefError, } from "./driver.js";
|
|
15
|
-
import { clampWaitTimeout, describeWaitTarget, isEmptyWaitTarget, matchesWaitTarget, WAIT_POLL_INTERVAL_MS, } from "./wait-target.js";
|
|
16
|
-
import { PROBE_CONSTANTS, probeInPage } from "./page-probe.js";
|
|
17
|
-
/** The module the driver composes. Named once, imported at call time. */
|
|
18
|
-
const RUNTIME_BROWSER_MODULE = "@descryy/runtime-browser";
|
|
19
|
-
const defaultRuntimeModuleLoader = () => import(__rewriteRelativeImportExtension(RUNTIME_BROWSER_MODULE));
|
|
20
|
-
/** Same seam, for the optional identity-grounding module — imported dynamically and only
|
|
21
|
-
* when `groundIdentity` is set, so a build or a session that never asks for it pays nothing. */
|
|
22
|
-
const IDENTITY_GROUNDING_MODULE = "@descryy/runtime-identity-grounding";
|
|
23
|
-
const defaultIdentityGroundingModuleLoader = () => import(__rewriteRelativeImportExtension(IDENTITY_GROUNDING_MODULE));
|
|
24
|
-
/** Ceiling on waiting for network activity to *start* — no signal proves "never will". Kept at the old fixed 300ms so a no-op action observes the same as before. */
|
|
25
|
-
const ACTIVITY_GRACE_MS = 300;
|
|
26
|
-
/** How long after the last request event counts as settled; fires only once activity is seen, so it measures a gap, not a guess about more coming. */
|
|
27
|
-
const NETWORK_QUIET_MS = 120;
|
|
28
|
-
/** Ceiling on the whole wait; reaching it is recorded on the observation and disclosed, never swallowed. */
|
|
29
|
-
const SETTLE_TIMEOUT_MS = 5_000;
|
|
30
|
-
/** How often the settle loop re-checks. Small enough not to add latency of its own. */
|
|
31
|
-
const POLL_INTERVAL_MS = 10;
|
|
32
|
-
/** Elements an agent can act on — deliberately narrow; a full-page snapshot would blow the reply budget. `[role]` included for custom controls. */
|
|
33
|
-
const INTERACTIVE = "a, button, input, select, textarea, [role], [onclick], summary";
|
|
34
|
-
/** How many scroll-and-re-check rounds a reveal pass runs before it stops and says it stopped. */
|
|
35
|
-
const REVEAL_MAX_PASSES = 12;
|
|
36
|
-
/** How long after a scroll before re-checking. A lazily-rendered row needs a frame or two to exist. */
|
|
37
|
-
const REVEAL_SETTLE_MS = 150;
|
|
38
|
-
/** `RawElementIdentity` -> the port's own `ElementComponentIdentity` — the one place the two
|
|
39
|
-
* vocabularies are joined, so a change to either shows up as one diff here. */
|
|
40
|
-
function mapIdentity(raw) {
|
|
41
|
-
switch (raw.outcome) {
|
|
42
|
-
case "grounded":
|
|
43
|
-
return {
|
|
44
|
-
outcome: "grounded",
|
|
45
|
-
component: raw.component.name,
|
|
46
|
-
file: raw.sourceLocation.file,
|
|
47
|
-
line: raw.sourceLocation.line,
|
|
48
|
-
authoredSource: raw.authoredSource,
|
|
49
|
-
reason: null,
|
|
50
|
-
};
|
|
51
|
-
case "component-only":
|
|
52
|
-
return {
|
|
53
|
-
outcome: "component-only",
|
|
54
|
-
component: raw.component.name,
|
|
55
|
-
file: null,
|
|
56
|
-
line: null,
|
|
57
|
-
authoredSource: false,
|
|
58
|
-
reason: raw.reason,
|
|
59
|
-
};
|
|
60
|
-
case "refused":
|
|
61
|
-
return {
|
|
62
|
-
outcome: "refused",
|
|
63
|
-
component: null,
|
|
64
|
-
file: null,
|
|
65
|
-
line: null,
|
|
66
|
-
authoredSource: false,
|
|
67
|
-
reason: raw.reason,
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
/** Folds the session's install-time identity warnings into a drained observation, once —
|
|
72
|
-
* same drain-not-peek discipline `drainEvidence` and `close` already hold every other field
|
|
73
|
-
* to, so a warning surfaces on the first read after it happened and never again. */
|
|
74
|
-
function withIdentityWarnings(observation, warnings) {
|
|
75
|
-
if (warnings.length === 0)
|
|
76
|
-
return observation;
|
|
77
|
-
const drained = [...warnings];
|
|
78
|
-
warnings.length = 0;
|
|
79
|
-
return { ...observation, evidenceWarnings: [...observation.evidenceWarnings, ...drained] };
|
|
80
|
-
}
|
|
81
|
-
/** Grounds one selector, never throwing — a probe failure is reported as a named refusal
|
|
82
|
-
* (rule 2: no guess), the same as every other outcome the grounder itself can name. */
|
|
83
|
-
async function groundOne(grounder, page, selector) {
|
|
84
|
-
try {
|
|
85
|
-
const raw = await grounder.ground(page, selector);
|
|
86
|
-
return mapIdentity(raw);
|
|
87
|
-
}
|
|
88
|
-
catch (error) {
|
|
89
|
-
return {
|
|
90
|
-
outcome: "refused",
|
|
91
|
-
component: null,
|
|
92
|
-
file: null,
|
|
93
|
-
line: null,
|
|
94
|
-
authoredSource: false,
|
|
95
|
-
reason: `the identity probe failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
/** What one element looks like from inside the page. Serialized and run there, so it closes over
|
|
100
|
-
* nothing in this module — every helper it needs is declared inside it.
|
|
101
|
-
*
|
|
102
|
-
* Exported for `browser-element-describe.test.ts`: role, name and renderedness are pure
|
|
103
|
-
* functions of one element, and pinning their decision table should not need a browser. */
|
|
104
|
-
export function describeElement(el) {
|
|
105
|
-
const tag = el.tagName.toLowerCase();
|
|
106
|
-
const explicit = el.getAttribute("role");
|
|
107
|
-
const type = el.getAttribute("type")?.toLowerCase() ?? null;
|
|
108
|
-
/** The HTML→ARIA role of this element. The old chain ended `: tag`, which reported
|
|
109
|
-
* `input[type="number"]` as role `"input"` — a role no agent filters on and no verb
|
|
110
|
-
* validates against, so a real control went silently missing from every role-filtered
|
|
111
|
-
* read. The mapping below is HTML's own, and it names no framework (rule 1).
|
|
112
|
-
*
|
|
113
|
-
* It still ends at the tag for a tag this does not know, deliberately: `[role]` and
|
|
114
|
-
* `[onclick]` put arbitrary elements into the query, and inventing a role for one of
|
|
115
|
-
* them would be this same defect pointed the other way. */
|
|
116
|
-
function derivedRole() {
|
|
117
|
-
if (tag === "a")
|
|
118
|
-
return "link";
|
|
119
|
-
if (tag === "button")
|
|
120
|
-
return "button";
|
|
121
|
-
if (tag === "textarea")
|
|
122
|
-
return "textbox";
|
|
123
|
-
// `summary` is the control that opens a `<details>`; a click is what it takes.
|
|
124
|
-
if (tag === "summary")
|
|
125
|
-
return "button";
|
|
126
|
-
if (tag === "select") {
|
|
127
|
-
const multiple = el.getAttribute("multiple") !== null;
|
|
128
|
-
const size = Number(el.getAttribute("size") ?? "1");
|
|
129
|
-
return multiple || size > 1 ? "listbox" : "combobox";
|
|
130
|
-
}
|
|
131
|
-
if (tag !== "input")
|
|
132
|
-
return tag;
|
|
133
|
-
switch (type) {
|
|
134
|
-
case "button":
|
|
135
|
-
case "submit":
|
|
136
|
-
case "reset":
|
|
137
|
-
case "image":
|
|
138
|
-
return "button";
|
|
139
|
-
case "checkbox":
|
|
140
|
-
return "checkbox";
|
|
141
|
-
case "radio":
|
|
142
|
-
return "radio";
|
|
143
|
-
case "range":
|
|
144
|
-
return "slider";
|
|
145
|
-
case "number":
|
|
146
|
-
return "spinbutton";
|
|
147
|
-
case "search":
|
|
148
|
-
return "searchbox";
|
|
149
|
-
// `password` has no ARIA role of its own; `textbox` is both the useful answer and the
|
|
150
|
-
// true one about how it is operated. The rest are HTML's textbox-mapped types.
|
|
151
|
-
case null:
|
|
152
|
-
case "text":
|
|
153
|
-
case "email":
|
|
154
|
-
case "tel":
|
|
155
|
-
case "url":
|
|
156
|
-
case "password":
|
|
157
|
-
return "textbox";
|
|
158
|
-
default:
|
|
159
|
-
// `file`, `color`, `date`, `hidden`, and anything a later HTML adds. Reporting the
|
|
160
|
-
// tag here is a disclosed gap; reporting `textbox` would be a wrong fact.
|
|
161
|
-
return tag;
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
const role = explicit ?? derivedRole();
|
|
165
|
-
/** A pragmatic accessible-name approximation, not a full AccName implementation.
|
|
166
|
-
*
|
|
167
|
-
* One case is carved out rather than approximated: a **chooser's own contents are its
|
|
168
|
-
* options**, so `innerText` on a `<select>` returns the list of things it can be set to,
|
|
169
|
-
* and a period picker came back named `"This month\nLast month"`. That is not a rough
|
|
170
|
-
* name, it is a false one — no person calls the control that, and the `select` verb
|
|
171
|
-
* matches on name. A chooser therefore skips the text tiers entirely and is left unnamed
|
|
172
|
-
* when nothing else names it. Unnamed is a gap; named after its options is a lie. */
|
|
173
|
-
function derivedName() {
|
|
174
|
-
const aria = el.getAttribute("aria-label");
|
|
175
|
-
if (aria !== null && aria.trim() !== "")
|
|
176
|
-
return aria;
|
|
177
|
-
if (tag !== "select") {
|
|
178
|
-
const inner = el.innerText?.trim();
|
|
179
|
-
if (inner !== undefined && inner !== "")
|
|
180
|
-
return inner;
|
|
181
|
-
const text = el.textContent?.trim();
|
|
182
|
-
if (text !== undefined && text !== null && text !== "")
|
|
183
|
-
return text;
|
|
184
|
-
}
|
|
185
|
-
// A push-button input carries its label in `value`, where no text tier would find it.
|
|
186
|
-
if (tag === "input" && (type === "button" || type === "submit" || type === "reset")) {
|
|
187
|
-
const value = el.getAttribute("value");
|
|
188
|
-
if (value !== null && value.trim() !== "")
|
|
189
|
-
return value.trim();
|
|
190
|
-
}
|
|
191
|
-
return el.getAttribute("placeholder") ?? el.getAttribute("title") ?? el.getAttribute("name") ?? "";
|
|
192
|
-
}
|
|
193
|
-
const name = derivedName();
|
|
194
|
-
/** Whether the page is laying this element out and painting it — true, false, or `null`
|
|
195
|
-
* when nothing available could answer.
|
|
196
|
-
*
|
|
197
|
-
* **Deliberately not a viewport test.** An element below the fold is on the page and the
|
|
198
|
-
* reveal pass depends on finding it; treating off-screen as hidden would trade this
|
|
199
|
-
* precision defect for a recall one. What it does reject is an element the page renders
|
|
200
|
-
* nowhere at all: `display:none`, the `hidden` attribute, a closed `<dialog>`,
|
|
201
|
-
* `content-visibility: hidden`, `visibility: hidden`, and `<canvas>` fallback content.
|
|
202
|
-
*
|
|
203
|
-
* Opacity is **not** rejected: a zero-opacity element still occupies space and still
|
|
204
|
-
* takes a click, and a fade-in caught mid-animation would vanish from the snapshot. */
|
|
205
|
-
function renderedState() {
|
|
206
|
-
if (typeof el.checkVisibility === "function") {
|
|
207
|
-
try {
|
|
208
|
-
return el.checkVisibility({ visibilityProperty: true });
|
|
209
|
-
}
|
|
210
|
-
catch {
|
|
211
|
-
return null;
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
if (typeof el.getClientRects !== "function")
|
|
215
|
-
return null;
|
|
216
|
-
try {
|
|
217
|
-
if (el.getClientRects().length === 0)
|
|
218
|
-
return false;
|
|
219
|
-
}
|
|
220
|
-
catch {
|
|
221
|
-
return null;
|
|
222
|
-
}
|
|
223
|
-
const view = globalThis.getComputedStyle;
|
|
224
|
-
if (typeof view !== "function")
|
|
225
|
-
return null;
|
|
226
|
-
try {
|
|
227
|
-
const visibility = view.call(globalThis, el)?.visibility;
|
|
228
|
-
if (visibility === undefined)
|
|
229
|
-
return null;
|
|
230
|
-
return visibility !== "hidden" && visibility !== "collapse";
|
|
231
|
-
}
|
|
232
|
-
catch {
|
|
233
|
-
return null;
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
// The node's own root: the document for a light-DOM element, the shadow root for one inside
|
|
237
|
-
// an open shadow root. Only a shadow root has a `host`, which is what tells them apart, and
|
|
238
|
-
// it is also the scope a uniqueness check has to run in — `ownerDocument.querySelectorAll`
|
|
239
|
-
// matches nothing at all for a shadow child, so every such element used to come back with a
|
|
240
|
-
// null selector for a reason that read like "no stable hook".
|
|
241
|
-
const root = typeof el.getRootNode === "function" ? el.getRootNode() : el.ownerDocument;
|
|
242
|
-
const inShadowRoot = root !== el.ownerDocument && root.host !== undefined && root.host !== null;
|
|
243
|
-
// Computed in this same round trip so role/name/selector read off one live DOM state.
|
|
244
|
-
// Rejects ids that look generated (hex run, React's `:r0:` useId
|
|
245
|
-
// style, or blank) — "stable across reload" is what a generated id lacks.
|
|
246
|
-
const GENERATED_ID = /[0-9a-f]{8}|:r[0-9a-z]+:|^\s*$/i;
|
|
247
|
-
const FORM_CONTROLS = ["input", "select", "textarea", "button"];
|
|
248
|
-
function escapeAttrValue(value) {
|
|
249
|
-
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
250
|
-
}
|
|
251
|
-
function isUnique(candidate) {
|
|
252
|
-
try {
|
|
253
|
-
return root.querySelectorAll(candidate).length === 1;
|
|
254
|
-
}
|
|
255
|
-
catch {
|
|
256
|
-
// Invalid selector (rare) is treated as "no candidate", not thrown.
|
|
257
|
-
return false;
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
/** The element's own stable-hook selector, or null. Used both on the target itself and while climbing for a structural path's anchor. */
|
|
261
|
-
function stableHookSelector(node) {
|
|
262
|
-
const testId = node.getAttribute("data-testid");
|
|
263
|
-
if (testId !== null && testId.trim() !== "")
|
|
264
|
-
return `[data-testid="${escapeAttrValue(testId)}"]`;
|
|
265
|
-
const testAttr = node.getAttribute("data-test");
|
|
266
|
-
if (testAttr !== null && testAttr.trim() !== "")
|
|
267
|
-
return `[data-test="${escapeAttrValue(testAttr)}"]`;
|
|
268
|
-
const id = node.getAttribute("id");
|
|
269
|
-
if (id !== null && !GENERATED_ID.test(id))
|
|
270
|
-
return `[id="${escapeAttrValue(id)}"]`;
|
|
271
|
-
return null;
|
|
272
|
-
}
|
|
273
|
-
function nthOfTypeSegment(node, parent) {
|
|
274
|
-
const nodeTag = node.tagName.toLowerCase();
|
|
275
|
-
const siblings = parent.querySelectorAll(`:scope > ${nodeTag}`);
|
|
276
|
-
if (siblings.length <= 1)
|
|
277
|
-
return nodeTag;
|
|
278
|
-
for (let i = 0; i < siblings.length; i += 1) {
|
|
279
|
-
if (siblings.item(i) === node)
|
|
280
|
-
return `${nodeTag}:nth-of-type(${i + 1})`;
|
|
281
|
-
}
|
|
282
|
-
return nodeTag;
|
|
283
|
-
}
|
|
284
|
-
/** A bounded path from the nearest ancestor carrying a stable hook, capped at 6 segments. Null
|
|
285
|
-
* when none is found in range — including when the climb reaches a shadow boundary, where
|
|
286
|
-
* `parentElement` is null and there is genuinely no further ancestor inside this root. */
|
|
287
|
-
function structuralPath(target) {
|
|
288
|
-
const segments = [];
|
|
289
|
-
let node = target;
|
|
290
|
-
for (let depth = 0; depth < 6; depth += 1) {
|
|
291
|
-
const parent = node.parentElement;
|
|
292
|
-
if (parent === null)
|
|
293
|
-
return null;
|
|
294
|
-
segments.unshift(nthOfTypeSegment(node, parent));
|
|
295
|
-
const hook = stableHookSelector(parent);
|
|
296
|
-
if (hook !== null) {
|
|
297
|
-
const candidate = `${hook} > ${segments.join(" > ")}`;
|
|
298
|
-
return isUnique(candidate) ? candidate : null;
|
|
299
|
-
}
|
|
300
|
-
node = parent;
|
|
301
|
-
}
|
|
302
|
-
return null;
|
|
303
|
-
}
|
|
304
|
-
function computeSelector(target) {
|
|
305
|
-
// Priority order: first unique candidate wins. Each tier is checked
|
|
306
|
-
// independently — a shared data-testid isn't addressable either,
|
|
307
|
-
// so the next tier gets a turn instead of stopping there.
|
|
308
|
-
const testId = target.getAttribute("data-testid");
|
|
309
|
-
if (testId !== null && testId.trim() !== "") {
|
|
310
|
-
const candidate = `[data-testid="${escapeAttrValue(testId)}"]`;
|
|
311
|
-
if (isUnique(candidate))
|
|
312
|
-
return candidate;
|
|
313
|
-
}
|
|
314
|
-
const testAttr = target.getAttribute("data-test");
|
|
315
|
-
if (testAttr !== null && testAttr.trim() !== "") {
|
|
316
|
-
const candidate = `[data-test="${escapeAttrValue(testAttr)}"]`;
|
|
317
|
-
if (isUnique(candidate))
|
|
318
|
-
return candidate;
|
|
319
|
-
}
|
|
320
|
-
const id = target.getAttribute("id");
|
|
321
|
-
if (id !== null && !GENERATED_ID.test(id)) {
|
|
322
|
-
const candidate = `[id="${escapeAttrValue(id)}"]`;
|
|
323
|
-
if (isUnique(candidate))
|
|
324
|
-
return candidate;
|
|
325
|
-
}
|
|
326
|
-
const controlTag = target.tagName.toLowerCase();
|
|
327
|
-
const controlName = target.getAttribute("name");
|
|
328
|
-
if (controlName !== null && controlName.trim() !== "" && FORM_CONTROLS.includes(controlTag)) {
|
|
329
|
-
const candidate = `${controlTag}[name="${escapeAttrValue(controlName)}"]`;
|
|
330
|
-
if (isUnique(candidate))
|
|
331
|
-
return candidate;
|
|
332
|
-
}
|
|
333
|
-
const explicitRole = target.getAttribute("role");
|
|
334
|
-
const ariaLabel = target.getAttribute("aria-label");
|
|
335
|
-
if (explicitRole !== null && ariaLabel !== null && ariaLabel.trim() !== "") {
|
|
336
|
-
const candidate = `[role="${escapeAttrValue(explicitRole)}"][aria-label="${escapeAttrValue(ariaLabel)}"]`;
|
|
337
|
-
if (isUnique(candidate))
|
|
338
|
-
return candidate;
|
|
339
|
-
}
|
|
340
|
-
return structuralPath(target);
|
|
341
|
-
}
|
|
342
|
-
return { role, name: name.slice(0, 200), selector: computeSelector(el), inShadowRoot, rendered: renderedState() };
|
|
343
|
-
}
|
|
344
|
-
/** Said once, so the cap and the sentence describing it cannot drift apart. */
|
|
345
|
-
const CONTAINER_SCAN_NOTE = "this page holds more elements than the reveal pass scans for scrollable containers, so a list " +
|
|
346
|
-
"inside one past that point was not scrolled and anything only it holds is not above";
|
|
347
|
-
/**
|
|
348
|
-
* One reveal step, run inside the page: scrolls the document **and every scrollable container
|
|
349
|
-
* in it** by just under their own height.
|
|
350
|
-
*
|
|
351
|
-
* The container half is the point. `window.scrollBy` does nothing to a list inside
|
|
352
|
-
* `overflow: auto` — a shape common enough that a window-only reveal reports "nothing below
|
|
353
|
-
* the fold" about a page whose entire content is below one. The step stays under a full
|
|
354
|
-
* viewport so a row sitting on the seam is never scrolled past between two enumerations.
|
|
355
|
-
*
|
|
356
|
-
* Serialized and run in the page, so it closes over nothing in this module — the scan cap is
|
|
357
|
-
* declared inside it for that reason and mirrored in `CONTAINER_SCAN_NOTE` above.
|
|
358
|
-
*/
|
|
359
|
-
function revealStep() {
|
|
360
|
-
const win = globalThis;
|
|
361
|
-
const doc = win.document;
|
|
362
|
-
const SCAN_CAP = 5000;
|
|
363
|
-
let scrolled = false;
|
|
364
|
-
if (win.scrollY + win.innerHeight < doc.documentElement.scrollHeight - 1) {
|
|
365
|
-
win.scrollBy(0, Math.round(win.innerHeight * 0.9));
|
|
366
|
-
scrolled = true;
|
|
367
|
-
}
|
|
368
|
-
const all = doc.querySelectorAll("*");
|
|
369
|
-
const limit = all.length > SCAN_CAP ? SCAN_CAP : all.length;
|
|
370
|
-
let containers = 0;
|
|
371
|
-
for (let i = 0; i < limit; i += 1) {
|
|
372
|
-
const node = all.item(i);
|
|
373
|
-
if (node === null)
|
|
374
|
-
continue;
|
|
375
|
-
// Geometry first, style second: the overflow read is the expensive one, and an element
|
|
376
|
-
// whose content fits inside it can never be a scroller whatever its overflow says.
|
|
377
|
-
if (typeof node.scrollHeight !== "number" || typeof node.clientHeight !== "number")
|
|
378
|
-
continue;
|
|
379
|
-
if (node.clientHeight === 0 || node.scrollHeight <= node.clientHeight + 1)
|
|
380
|
-
continue;
|
|
381
|
-
const overflow = typeof win.getComputedStyle === "function" ? win.getComputedStyle(node)?.overflowY : undefined;
|
|
382
|
-
if (overflow !== "auto" && overflow !== "scroll" && overflow !== "overlay")
|
|
383
|
-
continue;
|
|
384
|
-
containers += 1;
|
|
385
|
-
if (node.scrollTop + node.clientHeight < node.scrollHeight - 1) {
|
|
386
|
-
node.scrollBy(0, Math.round(node.clientHeight * 0.9));
|
|
387
|
-
scrolled = true;
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
return { y: win.scrollY, scrolled, containers, scanCapped: all.length > SCAN_CAP };
|
|
391
|
-
}
|
|
392
|
-
/** A frame's URL, or "" — a frame detaching mid-snapshot throws from `url()`. */
|
|
393
|
-
function safeUrl(frame) {
|
|
394
|
-
try {
|
|
395
|
-
return typeof frame.url === "function" ? frame.url() : "";
|
|
396
|
-
}
|
|
397
|
-
catch {
|
|
398
|
-
return "";
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
/** Something a person can read back: the frame's name, else the iframe element's id or title.
|
|
402
|
-
* Null when it carries none — the reply then falls back to the frame's URL. */
|
|
403
|
-
async function frameLabel(frame) {
|
|
404
|
-
try {
|
|
405
|
-
const declared = typeof frame.name === "function" ? frame.name() : "";
|
|
406
|
-
if (declared !== "")
|
|
407
|
-
return declared;
|
|
408
|
-
if (typeof frame.frameElement !== "function")
|
|
409
|
-
return null;
|
|
410
|
-
const element = await frame.frameElement();
|
|
411
|
-
try {
|
|
412
|
-
if (typeof element.getAttribute !== "function")
|
|
413
|
-
return null;
|
|
414
|
-
const id = await element.getAttribute("id");
|
|
415
|
-
if (id !== null && id.trim() !== "")
|
|
416
|
-
return id;
|
|
417
|
-
const title = await element.getAttribute("title");
|
|
418
|
-
if (title !== null && title.trim() !== "")
|
|
419
|
-
return title;
|
|
420
|
-
return null;
|
|
421
|
-
}
|
|
422
|
-
finally {
|
|
423
|
-
await element.dispose().catch(() => { });
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
catch {
|
|
427
|
-
return null;
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
/** Identity for reveal accounting. Elements are compared as a multiset, not by ref: a ref is
|
|
431
|
-
* issued per snapshot and two enumerations of the same row never share one. */
|
|
432
|
-
function countKeys(elements) {
|
|
433
|
-
const counts = new Map();
|
|
434
|
-
for (const element of elements) {
|
|
435
|
-
const key = `${element.frame?.id ?? ""}\u0000${element.role}\u0000${element.name}\u0000${element.selector ?? ""}`;
|
|
436
|
-
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
437
|
-
}
|
|
438
|
-
return counts;
|
|
439
|
-
}
|
|
440
|
-
/** How many of `a` have no counterpart in `b`. */
|
|
441
|
-
function surplus(a, b) {
|
|
442
|
-
let total = 0;
|
|
443
|
-
for (const [key, count] of a)
|
|
444
|
-
total += Math.max(0, count - (b.get(key) ?? 0));
|
|
445
|
-
return total;
|
|
446
|
-
}
|
|
447
|
-
export class BrowserBackendMissingError extends Error {
|
|
448
|
-
constructor(cause) {
|
|
449
|
-
super(`Could not load ${RUNTIME_BROWSER_MODULE}: ${cause instanceof Error ? cause.message : String(cause)}. ` +
|
|
450
|
-
"This build of @descryy/mcp does not have the browser backend installed.");
|
|
451
|
-
this.name = "BrowserBackendMissingError";
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
export async function createPlaywrightBrowserDriver(options = {}) {
|
|
455
|
-
let runtime;
|
|
456
|
-
try {
|
|
457
|
-
runtime = (await (options.loadRuntimeModule ?? defaultRuntimeModuleLoader)());
|
|
458
|
-
}
|
|
459
|
-
catch (error) {
|
|
460
|
-
throw new BrowserBackendMissingError(error);
|
|
461
|
-
}
|
|
462
|
-
return {
|
|
463
|
-
async launch(launchOptions) {
|
|
464
|
-
const auth = runtimeAuthFor(launchOptions);
|
|
465
|
-
const launch = await runtime.launchBrowserSessionOrRefuse({
|
|
466
|
-
headless: launchOptions.headless ?? true,
|
|
467
|
-
// B1. This used to be `{ headless }` and nothing else, while the port
|
|
468
|
-
// above already carried `storageState` and a reply note already said
|
|
469
|
-
// it had been seeded. The option reached the fake driver and stopped;
|
|
470
|
-
// against a real browser nothing was ever applied and the note was
|
|
471
|
-
// false. Building the seed here, from the target URL, is also what
|
|
472
|
-
// makes the origin a fact the runtime is *told* rather than one it
|
|
473
|
-
// has to work out.
|
|
474
|
-
...(auth === null ? {} : { auth }),
|
|
475
|
-
});
|
|
476
|
-
if (!launch.launched) {
|
|
477
|
-
const { launched: _launched, ...refusal } = launch;
|
|
478
|
-
return { launched: false, ...refusal };
|
|
479
|
-
}
|
|
480
|
-
const session = await openSession(runtime, launch.session, launchOptions, options);
|
|
481
|
-
return { launched: true, session };
|
|
482
|
-
},
|
|
483
|
-
};
|
|
484
|
-
}
|
|
485
|
-
async function openSession(runtime, session, launchOptions, options) {
|
|
486
|
-
const emitted = [];
|
|
487
|
-
const actions = [];
|
|
488
|
-
const unsettled = [];
|
|
489
|
-
// Never cleared, unlike `actions` — the session's whole-lifetime provenance record for
|
|
490
|
-
// `browser_save_scenario`, not per-drain evidence. Goes out of reach the moment this closure
|
|
491
|
-
// does (session closed/reaped) — there is no other path back to it.
|
|
492
|
-
const performed = [];
|
|
493
|
-
// inFlight/lastEventAt track request activity: counted, not slept through.
|
|
494
|
-
let inFlight = 0;
|
|
495
|
-
let lastEventAt = 0;
|
|
496
|
-
session.page.on("request", () => {
|
|
497
|
-
inFlight += 1;
|
|
498
|
-
lastEventAt = Date.now();
|
|
499
|
-
});
|
|
500
|
-
const settleOne = () => {
|
|
501
|
-
inFlight = Math.max(0, inFlight - 1);
|
|
502
|
-
lastEventAt = Date.now();
|
|
503
|
-
};
|
|
504
|
-
session.page.on("requestfinished", settleOne);
|
|
505
|
-
session.page.on("requestfailed", settleOne);
|
|
506
|
-
let refs = new Map();
|
|
507
|
-
let generation = 0;
|
|
508
|
-
let closed = false;
|
|
509
|
-
let finalObservation = null;
|
|
510
|
-
/** Set when a `saveStorageStateTo` write failed. Read by `storageStateSaveFailure()`. */
|
|
511
|
-
let storageStateSaveError = null;
|
|
512
|
-
/** Set when `groundIdentity` was asked for and installed successfully. Every snapshot
|
|
513
|
-
* element with a selector is grounded through it; one with none is never attempted
|
|
514
|
-
* (§B1's `identity: null` distinction — see `driver.ts`). */
|
|
515
|
-
let grounder = null;
|
|
516
|
-
const identityWarnings = [];
|
|
517
|
-
const context = {
|
|
518
|
-
executionId: `mcp-browser-${session.sessionId}`,
|
|
519
|
-
configuration: { environmentTier: "tier-2-container", fidelityLevel: 2, timeoutMs: 30_000, services: {} },
|
|
520
|
-
emit: (evidence) => {
|
|
521
|
-
emitted.push(evidence);
|
|
522
|
-
},
|
|
523
|
-
...(options.resolveSourceRoot === undefined ? {} : { resolveSourceRoot: options.resolveSourceRoot }),
|
|
524
|
-
};
|
|
525
|
-
const actionCollector = runtime.createBrowserActionCollector({
|
|
526
|
-
session,
|
|
527
|
-
...(options.service === undefined ? {} : { service: options.service }),
|
|
528
|
-
});
|
|
529
|
-
const consoleCollector = runtime.createBrowserConsoleCollector({
|
|
530
|
-
session,
|
|
531
|
-
...(options.service === undefined ? {} : { service: options.service }),
|
|
532
|
-
...(options.stackParser === undefined ? {} : { stackTraceParser: options.stackParser }),
|
|
533
|
-
});
|
|
534
|
-
const networkCollector = runtime.createBrowserNetworkCollector({
|
|
535
|
-
session,
|
|
536
|
-
...(options.service === undefined ? {} : { service: options.service }),
|
|
537
|
-
...(options.stackParser === undefined ? {} : { stackTraceParser: options.stackParser }),
|
|
538
|
-
});
|
|
539
|
-
// Started before first navigation: Playwright's init-script contract means
|
|
540
|
-
// a collector started later misses instrumentation, reporting an
|
|
541
|
-
// honest-looking but empty stack.
|
|
542
|
-
await actionCollector.start(context);
|
|
543
|
-
await consoleCollector.start(context);
|
|
544
|
-
await networkCollector.start(context);
|
|
545
|
-
// Same contract as the collectors above, and for the same reason: the identity probe is
|
|
546
|
-
// an init script too, so it must install before the first navigation or it reports
|
|
547
|
-
// `probe-not-installed` for the document's whole lifetime. Off by default (B1) — a session
|
|
548
|
-
// that never asks for it pays no page.evaluate and loads no module.
|
|
549
|
-
if (options.groundIdentity === true) {
|
|
550
|
-
try {
|
|
551
|
-
const identityModule = (await (options.loadIdentityGroundingModule ?? defaultIdentityGroundingModuleLoader)());
|
|
552
|
-
grounder = identityModule.createFrameworkAwareGrounder(options.resolveSourceRoot === undefined ? {} : { resolveSourceRoot: options.resolveSourceRoot });
|
|
553
|
-
await grounder.install(session.page);
|
|
554
|
-
}
|
|
555
|
-
catch (error) {
|
|
556
|
-
// Honest degradation (rule 7): identity grounding could not be installed, so every
|
|
557
|
-
// element on this session reports `identity: null` — never a guess — and the reason
|
|
558
|
-
// travels on the observation rather than being swallowed.
|
|
559
|
-
grounder = null;
|
|
560
|
-
identityWarnings.push(`identity grounding was requested (groundIdentity) but could not be installed: ` +
|
|
561
|
-
`${error instanceof Error ? error.message : String(error)}. Every element on this session ` +
|
|
562
|
-
"reports identity: null rather than a guess.");
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
await actionCollector.navigate(launchOptions.targetUrl);
|
|
566
|
-
function assertUsable() {
|
|
567
|
-
if (closed || !session.isUsable())
|
|
568
|
-
throw new BrowserSessionCrashedError(session.sessionId);
|
|
569
|
-
}
|
|
570
|
-
function resolve(ref) {
|
|
571
|
-
const found = refs.get(ref);
|
|
572
|
-
if (found === undefined)
|
|
573
|
-
throw new UnknownElementRefError(ref);
|
|
574
|
-
return found;
|
|
575
|
-
}
|
|
576
|
-
function recordAction(action, ref, mode) {
|
|
577
|
-
actions.push({ action, ref, mode, url: session.page.url(), at: new Date().toISOString() });
|
|
578
|
-
}
|
|
579
|
-
function recordPerformed(action, target, url, text) {
|
|
580
|
-
performed.push({ action, target, url, text, at: new Date().toISOString() });
|
|
581
|
-
}
|
|
582
|
-
/** Waits for the action's network effect (started within `ACTIVITY_GRACE_MS`, then quiet for `NETWORK_QUIET_MS`), capped at `SETTLE_TIMEOUT_MS` — reaching the cap is recorded, never swallowed. */
|
|
583
|
-
async function settleAfterAction(action, ref) {
|
|
584
|
-
const startedAt = Date.now();
|
|
585
|
-
lastEventAt = startedAt;
|
|
586
|
-
let sawActivity = inFlight > 0;
|
|
587
|
-
for (;;) {
|
|
588
|
-
const now = Date.now();
|
|
589
|
-
const waitedMs = now - startedAt;
|
|
590
|
-
if (inFlight > 0)
|
|
591
|
-
sawActivity = true;
|
|
592
|
-
if (waitedMs >= SETTLE_TIMEOUT_MS) {
|
|
593
|
-
unsettled.push({ action, ref, waitedMs });
|
|
594
|
-
return;
|
|
595
|
-
}
|
|
596
|
-
if (sawActivity) {
|
|
597
|
-
if (inFlight === 0 && now - lastEventAt >= NETWORK_QUIET_MS)
|
|
598
|
-
return;
|
|
599
|
-
}
|
|
600
|
-
else if (waitedMs >= ACTIVITY_GRACE_MS) {
|
|
601
|
-
return;
|
|
602
|
-
}
|
|
603
|
-
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
async function dropRefs() {
|
|
607
|
-
const handles = [...refs.values()].map((r) => r.handle);
|
|
608
|
-
refs = new Map();
|
|
609
|
-
await Promise.all(handles.map((h) => h.dispose().catch(() => { })));
|
|
610
|
-
}
|
|
611
|
-
/** Every document to read, main first. When the backend exposes no frame list the page
|
|
612
|
-
* itself is the only one, and that limitation is reported as an unread frame rather than
|
|
613
|
-
* passed off as "this page has no iframes". */
|
|
614
|
-
function documentsToRead() {
|
|
615
|
-
if (typeof session.page.frames !== "function")
|
|
616
|
-
return { frames: [{ frame: session.page, ref: null }], frameListUnavailable: true };
|
|
617
|
-
const all = session.page.frames();
|
|
618
|
-
const main = typeof session.page.mainFrame === "function" ? session.page.mainFrame() : all[0];
|
|
619
|
-
const out = [];
|
|
620
|
-
let index = 0;
|
|
621
|
-
for (const frame of all) {
|
|
622
|
-
if (frame === main) {
|
|
623
|
-
out.unshift({ frame, ref: null });
|
|
624
|
-
continue;
|
|
625
|
-
}
|
|
626
|
-
index += 1;
|
|
627
|
-
out.push({ frame, ref: { id: `frame-${index}`, url: safeUrl(frame), name: null } });
|
|
628
|
-
}
|
|
629
|
-
if (out.length === 0 || out[0]?.ref !== null)
|
|
630
|
-
out.unshift({ frame: session.page, ref: null });
|
|
631
|
-
return { frames: out, frameListUnavailable: false };
|
|
632
|
-
}
|
|
633
|
-
/** Reads the elements of one document, describing each in a single round trip. */
|
|
634
|
-
async function enumerateFrame(frame, frameRef) {
|
|
635
|
-
const handles = await frame.$$(INTERACTIVE);
|
|
636
|
-
const elements = [];
|
|
637
|
-
let ambiguousInDocument = 0;
|
|
638
|
-
let hidden = 0;
|
|
639
|
-
let untested = 0;
|
|
640
|
-
for (const handle of handles) {
|
|
641
|
-
const described = await handle.evaluate(describeElement).catch(() => null);
|
|
642
|
-
if (described === null) {
|
|
643
|
-
await handle.dispose().catch(() => { });
|
|
644
|
-
continue;
|
|
645
|
-
}
|
|
646
|
-
// The CSS query matches what is in the document; this is what is on the page. A
|
|
647
|
-
// `display:none` menu, a closed modal and an inactive tab panel all match the query,
|
|
648
|
-
// and reporting them as usable controls is Descry stating something false about the
|
|
649
|
-
// user's screen. Withheld and counted, never dropped in silence (rule 7).
|
|
650
|
-
if (described.rendered === false) {
|
|
651
|
-
hidden += 1;
|
|
652
|
-
await handle.dispose().catch(() => { });
|
|
653
|
-
continue;
|
|
654
|
-
}
|
|
655
|
-
// `null` is "nothing could answer". The element is kept — omitting on an unanswered
|
|
656
|
-
// question would hide real controls behind a test that did not run — and the fact that
|
|
657
|
-
// the question went unanswered travels with the snapshot.
|
|
658
|
-
if (described.rendered === null)
|
|
659
|
-
untested += 1;
|
|
660
|
-
let selector = described.selector;
|
|
661
|
-
// A hook unique inside its own shadow root is not necessarily unique in the document:
|
|
662
|
-
// two instances of the same component each carry `[data-testid="submit"]`. Playwright's
|
|
663
|
-
// own CSS resolution pierces, so asking it is the same question a replay would ask.
|
|
664
|
-
if (selector !== null && described.inShadowRoot === true) {
|
|
665
|
-
const matches = await frame.$$(selector).catch(() => null);
|
|
666
|
-
if (matches !== null)
|
|
667
|
-
await Promise.all(matches.map((m) => m.dispose().catch(() => { })));
|
|
668
|
-
if (matches === null || matches.length !== 1) {
|
|
669
|
-
selector = null;
|
|
670
|
-
ambiguousInDocument += 1;
|
|
671
|
-
}
|
|
672
|
-
}
|
|
673
|
-
elements.push({
|
|
674
|
-
handle,
|
|
675
|
-
role: described.role,
|
|
676
|
-
name: described.name,
|
|
677
|
-
// An element inside a frame has no saveable selector: the scenario format carries no
|
|
678
|
-
// field to name the frame, so a stored selector would replay against the main document.
|
|
679
|
-
selector: frameRef === null ? selector : null,
|
|
680
|
-
inShadowRoot: described.inShadowRoot === true,
|
|
681
|
-
frame: frameRef,
|
|
682
|
-
});
|
|
683
|
-
}
|
|
684
|
-
return { elements, ambiguousInDocument, hidden, untested };
|
|
685
|
-
}
|
|
686
|
-
/** Counts custom elements that look like closed shadow hosts. Returns null when the probe could not run. */
|
|
687
|
-
async function probeClosedHosts(frame) {
|
|
688
|
-
if (typeof frame.evaluate !== "function")
|
|
689
|
-
return null;
|
|
690
|
-
return frame
|
|
691
|
-
.evaluate(() => {
|
|
692
|
-
const page = globalThis;
|
|
693
|
-
const all = page.document.querySelectorAll("*");
|
|
694
|
-
let suspected = 0;
|
|
695
|
-
for (let i = 0; i < all.length; i += 1) {
|
|
696
|
-
const element = all.item(i);
|
|
697
|
-
if (element === null)
|
|
698
|
-
continue;
|
|
699
|
-
// A dash in the tag name is what the custom-element spec requires of every one of
|
|
700
|
-
// them; it names no language or framework (rule 1), only the HTML naming rule.
|
|
701
|
-
if (!element.tagName.toLowerCase().includes("-"))
|
|
702
|
-
continue;
|
|
703
|
-
if (element.shadowRoot !== null && element.shadowRoot !== undefined)
|
|
704
|
-
continue;
|
|
705
|
-
if ((element.childElementCount ?? 0) > 0)
|
|
706
|
-
continue;
|
|
707
|
-
suspected += 1;
|
|
708
|
-
}
|
|
709
|
-
return suspected;
|
|
710
|
-
})
|
|
711
|
-
.catch(() => null);
|
|
712
|
-
}
|
|
713
|
-
/** Reads every document, and reports each one it could not read with the reason. */
|
|
714
|
-
async function enumerateAll() {
|
|
715
|
-
const { frames: documents, frameListUnavailable } = documentsToRead();
|
|
716
|
-
const elements = [];
|
|
717
|
-
const reach = [];
|
|
718
|
-
let ambiguousInDocument = 0;
|
|
719
|
-
let suspectedClosedHosts = 0;
|
|
720
|
-
let probed = false;
|
|
721
|
-
let hidden = 0;
|
|
722
|
-
let untested = 0;
|
|
723
|
-
for (const { frame, ref } of documents) {
|
|
724
|
-
let frameRef = ref;
|
|
725
|
-
if (frameRef !== null)
|
|
726
|
-
frameRef = { ...frameRef, name: await frameLabel(frame) };
|
|
727
|
-
try {
|
|
728
|
-
const read = await enumerateFrame(frame, frameRef);
|
|
729
|
-
elements.push(...read.elements);
|
|
730
|
-
ambiguousInDocument += read.ambiguousInDocument;
|
|
731
|
-
hidden += read.hidden;
|
|
732
|
-
untested += read.untested;
|
|
733
|
-
reach.push({ frame: frameRef, enumerated: true, elements: read.elements.length, reason: null });
|
|
734
|
-
}
|
|
735
|
-
catch (error) {
|
|
736
|
-
reach.push({
|
|
737
|
-
frame: frameRef,
|
|
738
|
-
enumerated: false,
|
|
739
|
-
elements: 0,
|
|
740
|
-
reason: `this frame could not be read: ${error instanceof Error ? error.message : String(error)}`,
|
|
741
|
-
});
|
|
742
|
-
continue;
|
|
743
|
-
}
|
|
744
|
-
const closed = await probeClosedHosts(frame);
|
|
745
|
-
if (closed !== null) {
|
|
746
|
-
probed = true;
|
|
747
|
-
suspectedClosedHosts += closed;
|
|
748
|
-
}
|
|
749
|
-
}
|
|
750
|
-
if (frameListUnavailable) {
|
|
751
|
-
reach.push({
|
|
752
|
-
frame: { id: "frames-unavailable", url: "", name: null },
|
|
753
|
-
enumerated: false,
|
|
754
|
-
elements: 0,
|
|
755
|
-
reason: "this browser backend exposes no frame list, so any iframe on this page was not read. " +
|
|
756
|
-
"Nothing here claims the page has none.",
|
|
757
|
-
});
|
|
758
|
-
}
|
|
759
|
-
return {
|
|
760
|
-
elements,
|
|
761
|
-
frames: reach,
|
|
762
|
-
shadow: {
|
|
763
|
-
piercedElements: elements.filter((e) => e.inShadowRoot).length,
|
|
764
|
-
ambiguousInDocument,
|
|
765
|
-
suspectedClosedHosts,
|
|
766
|
-
probed,
|
|
767
|
-
},
|
|
768
|
-
hidden: { withheld: hidden, untested },
|
|
769
|
-
};
|
|
770
|
-
}
|
|
771
|
-
/** Scrolls the document **and every scrollable container in it** a viewport at a time until
|
|
772
|
-
* nothing can move further, or the pass cap stops it. Containers are the half a window-only
|
|
773
|
-
* scroll never reached: a list inside `overflow: auto` does not move when the page does. */
|
|
774
|
-
async function scrollToReveal() {
|
|
775
|
-
const page = session.page;
|
|
776
|
-
if (typeof page.evaluate !== "function") {
|
|
777
|
-
return {
|
|
778
|
-
passes: 0,
|
|
779
|
-
stoppedAtCap: false,
|
|
780
|
-
finalScrollY: 0,
|
|
781
|
-
containersScrolled: 0,
|
|
782
|
-
reason: "this browser backend exposes no page evaluation, so nothing below the fold could be revealed",
|
|
783
|
-
};
|
|
784
|
-
}
|
|
785
|
-
const evaluate = page.evaluate.bind(page);
|
|
786
|
-
let passes = 0;
|
|
787
|
-
let containers = 0;
|
|
788
|
-
let lastY = 0;
|
|
789
|
-
let scanCapped = false;
|
|
790
|
-
for (;;) {
|
|
791
|
-
const step = await evaluate(revealStep).catch(() => null);
|
|
792
|
-
if (step === null) {
|
|
793
|
-
return {
|
|
794
|
-
passes,
|
|
795
|
-
stoppedAtCap: false,
|
|
796
|
-
finalScrollY: lastY,
|
|
797
|
-
containersScrolled: containers,
|
|
798
|
-
reason: "the page stopped answering mid-reveal",
|
|
799
|
-
};
|
|
800
|
-
}
|
|
801
|
-
lastY = step.y;
|
|
802
|
-
containers = Math.max(containers, step.containers);
|
|
803
|
-
scanCapped = scanCapped || step.scanCapped;
|
|
804
|
-
// Nothing left to move: the page and every container it holds are at their end, or
|
|
805
|
-
// none of them can scroll at all. Either way there is nothing further to reveal.
|
|
806
|
-
if (!step.scrolled) {
|
|
807
|
-
return {
|
|
808
|
-
passes,
|
|
809
|
-
stoppedAtCap: false,
|
|
810
|
-
finalScrollY: step.y,
|
|
811
|
-
containersScrolled: containers,
|
|
812
|
-
reason: scanCapped ? CONTAINER_SCAN_NOTE : null,
|
|
813
|
-
};
|
|
814
|
-
}
|
|
815
|
-
passes += 1;
|
|
816
|
-
if (passes >= REVEAL_MAX_PASSES) {
|
|
817
|
-
return {
|
|
818
|
-
passes,
|
|
819
|
-
stoppedAtCap: true,
|
|
820
|
-
finalScrollY: step.y,
|
|
821
|
-
containersScrolled: containers,
|
|
822
|
-
reason: scanCapped ? CONTAINER_SCAN_NOTE : null,
|
|
823
|
-
};
|
|
824
|
-
}
|
|
825
|
-
await new Promise((done) => setTimeout(done, REVEAL_SETTLE_MS));
|
|
826
|
-
}
|
|
827
|
-
}
|
|
828
|
-
/**
|
|
829
|
-
* Refuses a control the page has put out of reach.
|
|
830
|
-
*
|
|
831
|
-
* `click` never calls this: Playwright's own actionability checks already
|
|
832
|
-
* cover it, and duplicating them here would mean two rules that can
|
|
833
|
-
* disagree. `select` and `submit` do, because neither of their underlying
|
|
834
|
-
* operations hit-tests — see `NotOperableError`.
|
|
835
|
-
*
|
|
836
|
-
* A browser that cannot answer is treated as operable rather than refused.
|
|
837
|
-
* Refusing on an unanswered question would turn a missing capability into a
|
|
838
|
-
* false negative on every element, which is the same trade rule 2 rejects
|
|
839
|
-
* pointed the other way.
|
|
840
|
-
*/
|
|
841
|
-
async function assertOperable(ref, handle) {
|
|
842
|
-
const state = await handle
|
|
843
|
-
.evaluate((el) => ({
|
|
844
|
-
// `inert` applies to a whole subtree and `HTMLElement.inert` reflects only the
|
|
845
|
-
// attribute on the node it is read from, so this has to walk ancestors.
|
|
846
|
-
inert: el.closest === undefined ? null : el.closest("[inert]") !== null,
|
|
847
|
-
disabled: el.disabled === true,
|
|
848
|
-
}))
|
|
849
|
-
.catch(() => null);
|
|
850
|
-
if (state === null)
|
|
851
|
-
return;
|
|
852
|
-
if (state.inert === true)
|
|
853
|
-
throw new NotOperableError(ref, "inert");
|
|
854
|
-
if (state.disabled)
|
|
855
|
-
throw new NotOperableError(ref, "disabled");
|
|
856
|
-
}
|
|
857
|
-
// Named rather than returned inline: `waitFor` re-enumerates the page each poll and does it
|
|
858
|
-
// by calling this same `snapshot()`, so the refs it hands back come from the one enumeration
|
|
859
|
-
// path instead of a second copy that could drift from it.
|
|
860
|
-
const api = {
|
|
861
|
-
sessionId: session.sessionId,
|
|
862
|
-
startedAt: session.startedAt,
|
|
863
|
-
isUsable: () => !closed && session.isUsable(),
|
|
864
|
-
currentUrl: () => session.page.url(),
|
|
865
|
-
async navigate(url) {
|
|
866
|
-
assertUsable();
|
|
867
|
-
await dropRefs();
|
|
868
|
-
await actionCollector.navigate(url);
|
|
869
|
-
recordAction("navigate", null, null);
|
|
870
|
-
recordPerformed("navigate", null, url, null);
|
|
871
|
-
},
|
|
872
|
-
async snapshot(options) {
|
|
873
|
-
assertUsable();
|
|
874
|
-
await dropRefs();
|
|
875
|
-
generation += 1;
|
|
876
|
-
let read = await enumerateAll();
|
|
877
|
-
let reveal = null;
|
|
878
|
-
if (options?.reveal === true) {
|
|
879
|
-
const before = countKeys(read.elements);
|
|
880
|
-
const scrolled = await scrollToReveal();
|
|
881
|
-
if (scrolled.passes > 0) {
|
|
882
|
-
// Handles taken before the scroll may point at rows the list has since destroyed;
|
|
883
|
-
// the page is re-read rather than merged, so every ref handed back is one that
|
|
884
|
-
// exists now. What the scroll cost is reported, not hidden.
|
|
885
|
-
await Promise.all(read.elements.map((e) => e.handle.dispose().catch(() => { })));
|
|
886
|
-
read = await enumerateAll();
|
|
887
|
-
}
|
|
888
|
-
const after = countKeys(read.elements);
|
|
889
|
-
reveal = {
|
|
890
|
-
passes: scrolled.passes,
|
|
891
|
-
containersScrolled: scrolled.containersScrolled,
|
|
892
|
-
revealedElements: surplus(after, before),
|
|
893
|
-
recycledElements: surplus(before, after),
|
|
894
|
-
stoppedAtCap: scrolled.stoppedAtCap,
|
|
895
|
-
finalScrollY: scrolled.finalScrollY,
|
|
896
|
-
framesNotScrolled: read.frames
|
|
897
|
-
.filter((f) => f.frame !== null)
|
|
898
|
-
.map((f) => f.frame?.name ?? f.frame?.url ?? f.frame?.id ?? ""),
|
|
899
|
-
reason: scrolled.reason,
|
|
900
|
-
};
|
|
901
|
-
}
|
|
902
|
-
const elements = [];
|
|
903
|
-
for (const [index, found] of read.elements.entries()) {
|
|
904
|
-
const ref = `ref-${generation}-${index}`;
|
|
905
|
-
const element = {
|
|
906
|
-
ref,
|
|
907
|
-
role: found.role,
|
|
908
|
-
name: found.name,
|
|
909
|
-
selector: found.selector,
|
|
910
|
-
frame: found.frame,
|
|
911
|
-
inShadowRoot: found.inShadowRoot,
|
|
912
|
-
// Grounding resolves a *document* selector, so a framed or hookless element
|
|
913
|
-
// (selector === null) is never attempted — `identity` stays null rather than
|
|
914
|
-
// guessed at, same reasoning `driver.ts` documents on the field itself.
|
|
915
|
-
identity: grounder === null || found.selector === null
|
|
916
|
-
? null
|
|
917
|
-
: await groundOne(grounder, session.page, found.selector),
|
|
918
|
-
};
|
|
919
|
-
refs.set(ref, { element, handle: found.handle });
|
|
920
|
-
elements.push(element);
|
|
921
|
-
}
|
|
922
|
-
return {
|
|
923
|
-
url: session.page.url(),
|
|
924
|
-
title: await session.page.title().catch(() => ""),
|
|
925
|
-
elements,
|
|
926
|
-
totalElements: elements.length,
|
|
927
|
-
frames: read.frames,
|
|
928
|
-
shadow: read.shadow,
|
|
929
|
-
hidden: read.hidden,
|
|
930
|
-
reveal,
|
|
931
|
-
};
|
|
932
|
-
},
|
|
933
|
-
async click(ref) {
|
|
934
|
-
assertUsable();
|
|
935
|
-
const { element, handle } = resolve(ref);
|
|
936
|
-
await handle.click();
|
|
937
|
-
recordAction("click", ref, null);
|
|
938
|
-
recordPerformed("click", { role: element.role, name: element.name, selector: element.selector }, null, null);
|
|
939
|
-
// requestfinished fires after the page's handler resolves; draining immediately would miss the very request being watched for.
|
|
940
|
-
await settleAfterAction("click", ref);
|
|
941
|
-
},
|
|
942
|
-
async type(ref, text) {
|
|
943
|
-
assertUsable();
|
|
944
|
-
const { element, handle } = resolve(ref);
|
|
945
|
-
if (handle.pressSequentially !== undefined)
|
|
946
|
-
await handle.pressSequentially(text);
|
|
947
|
-
else if (handle.type !== undefined)
|
|
948
|
-
await handle.type(text);
|
|
949
|
-
else
|
|
950
|
-
throw new Error("this Playwright build exposes no keystroke-level typing method");
|
|
951
|
-
recordAction("type", ref, "append");
|
|
952
|
-
recordPerformed("type", { role: element.role, name: element.name, selector: element.selector }, null, text);
|
|
953
|
-
await settleAfterAction("type", ref);
|
|
954
|
-
},
|
|
955
|
-
async fill(ref, text) {
|
|
956
|
-
assertUsable();
|
|
957
|
-
const { element, handle } = resolve(ref);
|
|
958
|
-
await handle.fill(text);
|
|
959
|
-
recordAction("fill", ref, "replace");
|
|
960
|
-
recordPerformed("fill", { role: element.role, name: element.name, selector: element.selector }, null, text);
|
|
961
|
-
await settleAfterAction("fill", ref);
|
|
962
|
-
},
|
|
963
|
-
async select(ref, value) {
|
|
964
|
-
assertUsable();
|
|
965
|
-
const { element, handle } = resolve(ref);
|
|
966
|
-
await assertOperable(ref, handle);
|
|
967
|
-
// Read what the element actually offers before touching it. Playwright's own
|
|
968
|
-
// selectOption would time out on a non-match and say nothing useful about why; the
|
|
969
|
-
// refusal a caller can act on has to carry the real option list, and that means one
|
|
970
|
-
// round trip to read it.
|
|
971
|
-
const offered = await handle.evaluate((el) => {
|
|
972
|
-
if (el.options === undefined)
|
|
973
|
-
return null;
|
|
974
|
-
const out = [];
|
|
975
|
-
for (let i = 0; i < el.options.length; i += 1) {
|
|
976
|
-
const option = el.options[i];
|
|
977
|
-
out.push({ value: option.value, label: option.label === "" ? option.text : option.label });
|
|
978
|
-
}
|
|
979
|
-
return out;
|
|
980
|
-
});
|
|
981
|
-
if (offered === null)
|
|
982
|
-
throw new NotSelectableError(ref, element.role);
|
|
983
|
-
// By stored value first, then by the label a person reads — both exact. A caller
|
|
984
|
-
// reading a snapshot sees labels and a caller reading the page's source sees values;
|
|
985
|
-
// neither should have to know which one this element uses.
|
|
986
|
-
const matched = offered.find((o) => o.value === value) ?? offered.find((o) => o.label.trim() === value.trim());
|
|
987
|
-
if (matched === undefined) {
|
|
988
|
-
throw new NoSuchOptionError(ref, value, offered.map((o) => (o.label.trim() === "" ? o.value : o.label)));
|
|
989
|
-
}
|
|
990
|
-
if (handle.selectOption === undefined)
|
|
991
|
-
throw new Error("this Playwright build exposes no option-selecting method");
|
|
992
|
-
await handle.selectOption(matched.value);
|
|
993
|
-
recordAction("select", ref, "replace");
|
|
994
|
-
recordPerformed("select", { role: element.role, name: element.name, selector: element.selector }, null, matched.value);
|
|
995
|
-
await settleAfterAction("select", ref);
|
|
996
|
-
},
|
|
997
|
-
async submit(ref) {
|
|
998
|
-
assertUsable();
|
|
999
|
-
const { element, handle } = resolve(ref);
|
|
1000
|
-
await assertOperable(ref, handle);
|
|
1001
|
-
// Submitted through the form's own submit control rather than by synthesising a bare
|
|
1002
|
-
// submit event: that is what a person's click does, it names the submitter the page's
|
|
1003
|
-
// handler reads, and it runs the form's validation. A form with no such control is
|
|
1004
|
-
// submitted by script on some other control — refused, not guessed at.
|
|
1005
|
-
const outcome = await handle.evaluate((el) => {
|
|
1006
|
-
const form = el.closest === undefined ? null : el.closest("form");
|
|
1007
|
-
if (form === null)
|
|
1008
|
-
return "noForm";
|
|
1009
|
-
const control = form.querySelector('button[type="submit"], input[type="submit"], button:not([type])');
|
|
1010
|
-
if (control === null)
|
|
1011
|
-
return "noSubmitControl";
|
|
1012
|
-
form.requestSubmit(control);
|
|
1013
|
-
return "submitted";
|
|
1014
|
-
});
|
|
1015
|
-
if (outcome !== "submitted")
|
|
1016
|
-
throw new NotSubmittableError(ref, outcome);
|
|
1017
|
-
recordAction("submit", ref, null);
|
|
1018
|
-
recordPerformed("submit", { role: element.role, name: element.name, selector: element.selector }, null, null);
|
|
1019
|
-
await settleAfterAction("submit", ref);
|
|
1020
|
-
},
|
|
1021
|
-
async waitFor(target, waitOptions = {}) {
|
|
1022
|
-
assertUsable();
|
|
1023
|
-
if (isEmptyWaitTarget(target)) {
|
|
1024
|
-
throw new Error("browser_wait_for needs a target: give a role, a name, or both. Waiting for nothing in particular " +
|
|
1025
|
-
`would return the first element on the page, which is not what any caller means. Got: ${describeWaitTarget(target)}.`);
|
|
1026
|
-
}
|
|
1027
|
-
const timeoutMs = clampWaitTimeout(waitOptions.timeoutMs);
|
|
1028
|
-
const startedAt = Date.now();
|
|
1029
|
-
for (;;) {
|
|
1030
|
-
assertUsable();
|
|
1031
|
-
// Re-enumerates each poll rather than watching one selector: the port has no
|
|
1032
|
-
// selector to watch, and a fresh snapshot is what makes the matched element
|
|
1033
|
-
// actionable at all. That cost is real, and is why the interval is not tighter.
|
|
1034
|
-
const snapshot = await api.snapshot();
|
|
1035
|
-
const matches = snapshot.elements.filter((e) => matchesWaitTarget(e, target));
|
|
1036
|
-
const waitedMs = Date.now() - startedAt;
|
|
1037
|
-
if (matches.length > 0) {
|
|
1038
|
-
return { appeared: true, waitedMs, timeoutMs, matched: matches[0], matchCount: matches.length, snapshot };
|
|
1039
|
-
}
|
|
1040
|
-
if (waitedMs >= timeoutMs) {
|
|
1041
|
-
return { appeared: false, waitedMs, timeoutMs, matched: null, matchCount: 0, snapshot };
|
|
1042
|
-
}
|
|
1043
|
-
await new Promise((done) => setTimeout(done, WAIT_POLL_INTERVAL_MS));
|
|
1044
|
-
}
|
|
1045
|
-
},
|
|
1046
|
-
async injectStorageState(origin, state) {
|
|
1047
|
-
assertUsable();
|
|
1048
|
-
if (typeof session.injectStorageState !== "function") {
|
|
1049
|
-
// Honest degradation (rule 7), same shape the groundIdentity install
|
|
1050
|
-
// failure above uses: an older runtime-browser build predates this
|
|
1051
|
-
// primitive, so nothing was injected and the reason travels on the
|
|
1052
|
-
// result rather than a silent no-op or a thrown TypeError.
|
|
1053
|
-
return {
|
|
1054
|
-
cookiesInjected: 0,
|
|
1055
|
-
localStorageInjected: 0,
|
|
1056
|
-
localStorageSkipped: "this build of @descryy/runtime-browser does not export injectStorageState -- nothing was injected. " +
|
|
1057
|
-
"Republish runtime-browser at a version that includes BrowserSession.injectStorageState.",
|
|
1058
|
-
};
|
|
1059
|
-
}
|
|
1060
|
-
return session.injectStorageState({
|
|
1061
|
-
origin,
|
|
1062
|
-
...(state.cookies === undefined ? {} : { cookies: state.cookies }),
|
|
1063
|
-
...(state.localStorage === undefined ? {} : { localStorage: state.localStorage }),
|
|
1064
|
-
});
|
|
1065
|
-
},
|
|
1066
|
-
async drainEvidence() {
|
|
1067
|
-
const observation = collect(emitted, actions, unsettled);
|
|
1068
|
-
emitted.length = 0;
|
|
1069
|
-
actions.length = 0;
|
|
1070
|
-
unsettled.length = 0;
|
|
1071
|
-
return withIdentityWarnings(observation, identityWarnings);
|
|
1072
|
-
},
|
|
1073
|
-
performedSteps() {
|
|
1074
|
-
return [...performed];
|
|
1075
|
-
},
|
|
1076
|
-
storageStateSaveFailure: () => storageStateSaveError,
|
|
1077
|
-
/**
|
|
1078
|
-
* §4.2's probe. Reads the page and counts; decides nothing — `estimateReach`
|
|
1079
|
-
* is the only thing that turns these counts into a claim, so a change of
|
|
1080
|
-
* policy never touches a driver.
|
|
1081
|
-
*
|
|
1082
|
-
* The body lives in `page-probe.ts` rather than here: this file belongs to
|
|
1083
|
-
* another lane, and a six-line delegation is mergeable where two hundred
|
|
1084
|
-
* lines of DOM walking would not be.
|
|
1085
|
-
*/
|
|
1086
|
-
async fingerprint() {
|
|
1087
|
-
assertUsable();
|
|
1088
|
-
const [root] = await session.page.$$("html");
|
|
1089
|
-
if (root === undefined) {
|
|
1090
|
-
// No document element at all. Refusing is right: a fingerprint of
|
|
1091
|
-
// zeroes would read as a page with nothing on it.
|
|
1092
|
-
throw new Error("the page exposed no document element to probe");
|
|
1093
|
-
}
|
|
1094
|
-
try {
|
|
1095
|
-
return await root.evaluate(probeInPage, PROBE_CONSTANTS);
|
|
1096
|
-
}
|
|
1097
|
-
finally {
|
|
1098
|
-
await root.dispose();
|
|
1099
|
-
}
|
|
1100
|
-
},
|
|
1101
|
-
async close() {
|
|
1102
|
-
if (finalObservation !== null)
|
|
1103
|
-
return finalObservation;
|
|
1104
|
-
closed = true;
|
|
1105
|
-
// Stopped before observing: a stop flushes what each collector was
|
|
1106
|
-
// still holding, which observing first would drop.
|
|
1107
|
-
await actionCollector.stop().catch(() => { });
|
|
1108
|
-
await consoleCollector.stop().catch(() => { });
|
|
1109
|
-
await networkCollector.stop().catch(() => { });
|
|
1110
|
-
const observation = withIdentityWarnings(collect(emitted, actions, unsettled), identityWarnings);
|
|
1111
|
-
emitted.length = 0;
|
|
1112
|
-
actions.length = 0;
|
|
1113
|
-
unsettled.length = 0;
|
|
1114
|
-
await dropRefs();
|
|
1115
|
-
// Before `session.close()`, not after: a closed context holds no
|
|
1116
|
-
// cookies to read, and a save that ran afterwards would write a file
|
|
1117
|
-
// that looks like a session with nothing in it — which is worse than no
|
|
1118
|
-
// file, because the next run would seed from it and silently drive the
|
|
1119
|
-
// logged-out surface. A failure here is reported as a note by the
|
|
1120
|
-
// caller rather than swallowed, but it never fails the close: evidence
|
|
1121
|
-
// already collected must not be lost to a write that did not work.
|
|
1122
|
-
if (launchOptions.saveStorageStateTo !== undefined && session.saveStorageState !== undefined) {
|
|
1123
|
-
try {
|
|
1124
|
-
await session.saveStorageState(launchOptions.saveStorageStateTo);
|
|
1125
|
-
}
|
|
1126
|
-
catch (error) {
|
|
1127
|
-
storageStateSaveError = error instanceof Error ? error.message : String(error);
|
|
1128
|
-
}
|
|
1129
|
-
}
|
|
1130
|
-
await session.close().catch(() => { });
|
|
1131
|
-
finalObservation = observation;
|
|
1132
|
-
return observation;
|
|
1133
|
-
},
|
|
1134
|
-
};
|
|
1135
|
-
return api;
|
|
1136
|
-
}
|
|
1137
|
-
/** Turns collector evidence into the port's vocabulary. Joins request/response on `requestId`, keeping `status: null` (not guessed) when unanswered. Exported for `browser-request-outcome-mapping.test.ts`. */
|
|
1138
|
-
export function collect(emitted, actions, unsettled) {
|
|
1139
|
-
// `requestId` is a correlation header the app must itself propagate, not a
|
|
1140
|
-
// transport id — an id-only join silently reported `status: null` for
|
|
1141
|
-
// every request without one, i.e. most of them, hence the URL fallback
|
|
1142
|
-
// below. `status` and `outcome` are read from the *same* terminal event and
|
|
1143
|
-
// stored together: two independent last-write-wins maps let a later
|
|
1144
|
-
// terminal event for a *different* request overwrite one field but not the
|
|
1145
|
-
// other (e.g. an id-less "unanswered" HTTP_ERROR with no numeric status
|
|
1146
|
-
// updates only `outcome`), so a third, genuinely-still-pending id-less
|
|
1147
|
-
// request sharing that URL could read back `status: 200` from one sibling
|
|
1148
|
-
// and `outcome: "unanswered"` from another — a self-contradictory pair
|
|
1149
|
-
// that is a real, reported defect (P6-D5), not a hypothetical one.
|
|
1150
|
-
//
|
|
1151
|
-
// **B4: that pairing was necessary and was not sufficient.** Three routes to
|
|
1152
|
-
// a false fact survived it, each verified here rather than reasoned about,
|
|
1153
|
-
// and each fixed at the line it occurs on below: the pair itself was never
|
|
1154
|
-
// checked for self-consistency across a boundary that versions separately;
|
|
1155
|
-
// "paired" was a set-membership test on the URL rather than a record of what
|
|
1156
|
-
// this request consumed, so an in-flight sibling read as failed; and the
|
|
1157
|
-
// HTTP_ERROR `descry-runtime` emits *alongside* a >= 400 NETWORK_RESPONSE
|
|
1158
|
-
// took a second queue slot, handing one exchange's 500 to two requests.
|
|
1159
|
-
const byRequestId = new Map();
|
|
1160
|
-
// C3. A **transport-level** identity, when the collector carries one: an id
|
|
1161
|
-
// the browser's own request object owns, not a header an application chose
|
|
1162
|
-
// to propagate. It outranks `requestId` because it cannot be absent for
|
|
1163
|
-
// application reasons, and where it is present the whole out-of-order
|
|
1164
|
-
// question below simply does not arise. `descry-runtime`'s network
|
|
1165
|
-
// collector already holds this identity — its pending map is keyed by the
|
|
1166
|
-
// Playwright `Request` object itself, "not a URL or an id, which could
|
|
1167
|
-
// collide across concurrent requests to the same endpoint" — and does not
|
|
1168
|
-
// yet emit it. Read here the same way `outcome` was read before its
|
|
1169
|
-
// producer shipped: validated, optional, and absent degrades to the join
|
|
1170
|
-
// below rather than to a guess. See
|
|
1171
|
-
// `DEC-NEXT-transport-level-exchange-id-for-browser-requests`.
|
|
1172
|
-
const byExchangeId = new Map();
|
|
1173
|
-
// FIFO per URL, fed only by unidentified terminal events. A URL is not
|
|
1174
|
-
// unique the way an id is — a page can issue the same URL more than once —
|
|
1175
|
-
// so a shared cache must hand each terminal event's (status, outcome) pair
|
|
1176
|
-
// to at most one request, in arrival order, rather than broadcasting the
|
|
1177
|
-
// most recent one to every request sharing that URL. An id-bearing terminal
|
|
1178
|
-
// event is deliberately excluded from this pool: it is already claimed
|
|
1179
|
-
// evidence for one specific request, and folding it into the shared pool
|
|
1180
|
-
// would just relocate the same contamination.
|
|
1181
|
-
//
|
|
1182
|
-
// **C3: arrival order is not an answer, so it is no longer used as one.**
|
|
1183
|
-
// Two unidentified requests to one URL whose responses arrive out of order
|
|
1184
|
-
// swap records, and nothing at this layer can tell an in-order pair from a
|
|
1185
|
-
// swapped one. A FIFO is therefore right about half the time and silent
|
|
1186
|
-
// when it is wrong — the exact shape rule 2 forbids, because the failure
|
|
1187
|
-
// leaves no trace in the reply. Where the assignment is genuinely
|
|
1188
|
-
// undetermined the queue is not consulted at all and every request in that
|
|
1189
|
-
// bucket is reported `unattributed` with the terminal events disclosed by
|
|
1190
|
-
// URL instead. Where it is determined — one unidentified request, or
|
|
1191
|
-
// several whose records are indistinguishable from one another, so that
|
|
1192
|
-
// every possible assignment yields the same answer — it still pairs, since
|
|
1193
|
-
// refusing there would trade a silent wrong answer for a loud missing one
|
|
1194
|
-
// that nothing required.
|
|
1195
|
-
const queueByUrl = new Map();
|
|
1196
|
-
const warnings = new Set();
|
|
1197
|
-
// URLs that produced an id-less NETWORK_RESPONSE, so the duplicate-HTTP_ERROR
|
|
1198
|
-
// exclusion below can require its sibling rather than assume it. A build that
|
|
1199
|
-
// ever emitted a numeric-status HTTP_ERROR *alone* would otherwise have it
|
|
1200
|
-
// silently dropped, turning a real 4xx into "pending".
|
|
1201
|
-
const respondedUrls = new Set();
|
|
1202
|
-
for (const evidence of emitted) {
|
|
1203
|
-
if (evidence.eventType !== "NETWORK_RESPONSE" || evidence.requestId !== null)
|
|
1204
|
-
continue;
|
|
1205
|
-
if (exchangeIdOf(evidence.payload) !== null)
|
|
1206
|
-
continue;
|
|
1207
|
-
const url = evidence.payload["url"];
|
|
1208
|
-
if (typeof url === "string")
|
|
1209
|
-
respondedUrls.add(url);
|
|
1210
|
-
}
|
|
1211
|
-
for (const evidence of emitted) {
|
|
1212
|
-
if (evidence.eventType !== "NETWORK_RESPONSE" && evidence.eventType !== "HTTP_ERROR")
|
|
1213
|
-
continue;
|
|
1214
|
-
const url = evidence.payload["url"];
|
|
1215
|
-
const rawStatus = evidence.payload["status"];
|
|
1216
|
-
const status = typeof rawStatus === "number" ? rawStatus : null;
|
|
1217
|
-
// B4. The pair is validated, not just each field: this payload crosses a
|
|
1218
|
-
// package boundary that versions separately, which is the same reason
|
|
1219
|
-
// `requestOutcomeOf` validates instead of casting. A numeric status means
|
|
1220
|
-
// a response arrived; "unanswered" means none ever did. Both cannot be
|
|
1221
|
-
// true of one request, and forwarding the pair verbatim is what produced
|
|
1222
|
-
// the `status: 200` + `outcome: "unanswered"` record the cal.com pass
|
|
1223
|
-
// reported. Neither field can be trusted to correct the other, so the
|
|
1224
|
-
// directly-observed status is kept and the outcome degrades to the same
|
|
1225
|
-
// "not reported" state an older collector build produces — and it is
|
|
1226
|
-
// named in the reply rather than dropped quietly (rule 7).
|
|
1227
|
-
const outcome = reconcile(status, requestOutcomeOf(evidence.payload), url, warnings);
|
|
1228
|
-
const record = { status, outcome };
|
|
1229
|
-
const exchangeId = exchangeIdOf(evidence.payload);
|
|
1230
|
-
if (exchangeId !== null) {
|
|
1231
|
-
byExchangeId.set(exchangeId, record);
|
|
1232
|
-
continue;
|
|
1233
|
-
}
|
|
1234
|
-
if (evidence.requestId !== null) {
|
|
1235
|
-
byRequestId.set(evidence.requestId, record);
|
|
1236
|
-
continue;
|
|
1237
|
-
}
|
|
1238
|
-
if (typeof url !== "string")
|
|
1239
|
-
continue;
|
|
1240
|
-
// B4. One exchange, one slot. `descry-runtime`'s collector emits
|
|
1241
|
-
// NETWORK_RESPONSE *and* a payload-sharing HTTP_ERROR for a single
|
|
1242
|
-
// response with status >= 400, and a numeric status is the only shape in
|
|
1243
|
-
// which it ever emits HTTP_ERROR — every other HTTP_ERROR it produces
|
|
1244
|
-
// (requestfailed, its own timeout, a websocket socketerror, a session
|
|
1245
|
-
// closing mid-flight) carries `status: null` and has no NETWORK_RESPONSE
|
|
1246
|
-
// beside it. Keyed by requestId that duplicate is harmless (same content,
|
|
1247
|
-
// one map entry); in this queue it took a second slot, so one 500
|
|
1248
|
-
// exchange handed a 500 to a second request that never got a response at
|
|
1249
|
-
// all. Excluded here only — an id-keyed one still records, in case a
|
|
1250
|
-
// build ever emits it without its sibling.
|
|
1251
|
-
if (evidence.eventType === "HTTP_ERROR" && status !== null && respondedUrls.has(url))
|
|
1252
|
-
continue;
|
|
1253
|
-
const queue = queueByUrl.get(url);
|
|
1254
|
-
if (queue === undefined)
|
|
1255
|
-
queueByUrl.set(url, [record]);
|
|
1256
|
-
else
|
|
1257
|
-
queue.push(record);
|
|
1258
|
-
}
|
|
1259
|
-
/** The record this evidence owns outright, or undefined when it must draw from the shared
|
|
1260
|
-
* URL queue. Exchange id first: it identifies the transport exchange, where `requestId`
|
|
1261
|
-
* identifies whatever the application decided to label. */
|
|
1262
|
-
const keyedRecordFor = (evidence) => {
|
|
1263
|
-
const exchangeId = exchangeIdOf(evidence.payload);
|
|
1264
|
-
if (exchangeId !== null) {
|
|
1265
|
-
const byExchange = byExchangeId.get(exchangeId);
|
|
1266
|
-
if (byExchange !== undefined)
|
|
1267
|
-
return byExchange;
|
|
1268
|
-
}
|
|
1269
|
-
return evidence.requestId === null ? undefined : byRequestId.get(evidence.requestId);
|
|
1270
|
-
};
|
|
1271
|
-
// C3. How many requests to each URL have no record of their own — exactly the set that
|
|
1272
|
-
// draws from the shared queue, and therefore exactly the set the queue can be wrong about.
|
|
1273
|
-
// Counted after the maps are built, not from the presence of an id: a request carrying an
|
|
1274
|
-
// id that never came back on a terminal event falls through to the queue like any other,
|
|
1275
|
-
// and counting it as identified would leave the queue one short of the requests using it.
|
|
1276
|
-
const unkeyedByUrl = new Map();
|
|
1277
|
-
for (const evidence of emitted) {
|
|
1278
|
-
if (evidence.eventType !== "NETWORK_REQUEST")
|
|
1279
|
-
continue;
|
|
1280
|
-
const url = evidence.payload["url"];
|
|
1281
|
-
if (typeof url !== "string")
|
|
1282
|
-
continue;
|
|
1283
|
-
if (keyedRecordFor(evidence) !== undefined)
|
|
1284
|
-
continue;
|
|
1285
|
-
unkeyedByUrl.set(url, (unkeyedByUrl.get(url) ?? 0) + 1);
|
|
1286
|
-
}
|
|
1287
|
-
// C3. The URLs whose assignment is undetermined. Not "has duplicates" — having two requests
|
|
1288
|
-
// to one URL is ordinary and usually harmless. It is undetermined only when two different
|
|
1289
|
-
// assignments would produce two different replies.
|
|
1290
|
-
const undeterminedUrls = new Set();
|
|
1291
|
-
for (const [url, records] of queueByUrl) {
|
|
1292
|
-
const unkeyed = unkeyedByUrl.get(url) ?? 0;
|
|
1293
|
-
if (unkeyed <= 1)
|
|
1294
|
-
continue;
|
|
1295
|
-
const first = records[0];
|
|
1296
|
-
if (first === undefined)
|
|
1297
|
-
continue;
|
|
1298
|
-
const alike = records.every((record) => record.status === first.status && record.outcome === first.outcome);
|
|
1299
|
-
// Enough alike records to go round means every request gets the same one, so which is
|
|
1300
|
-
// which never comes up. One short means at least one request is still in flight and
|
|
1301
|
-
// nothing can say which — and "pending" on the wrong one is a false fact about a healthy
|
|
1302
|
-
// request, which is the defect B4 fixed in its first costume.
|
|
1303
|
-
if (alike && records.length >= unkeyed)
|
|
1304
|
-
continue;
|
|
1305
|
-
undeterminedUrls.add(url);
|
|
1306
|
-
warnings.add(undeterminedJoinWarning(url, unkeyed, records));
|
|
1307
|
-
}
|
|
1308
|
-
const requests = [];
|
|
1309
|
-
const consoleErrors = [];
|
|
1310
|
-
for (const evidence of emitted) {
|
|
1311
|
-
if (evidence.eventType === "NETWORK_REQUEST") {
|
|
1312
|
-
const method = evidence.payload["method"];
|
|
1313
|
-
const url = evidence.payload["url"];
|
|
1314
|
-
if (typeof method !== "string" || typeof url !== "string")
|
|
1315
|
-
continue;
|
|
1316
|
-
// The keyed match wins when it exists; only a request with no record of its own draws
|
|
1317
|
-
// from the shared URL queue, and it draws once. C3: on an undetermined URL the queue is
|
|
1318
|
-
// not consulted at all — consuming from it is what produced the swap.
|
|
1319
|
-
const keyed = keyedRecordFor(evidence);
|
|
1320
|
-
const undetermined = keyed === undefined && undeterminedUrls.has(url);
|
|
1321
|
-
const matched = keyed ?? (undetermined ? undefined : queueByUrl.get(url)?.shift());
|
|
1322
|
-
// B4. Paired is what this request *consumed*, not what its URL was seen
|
|
1323
|
-
// in. It used to be a set-membership test on the URL, so a second,
|
|
1324
|
-
// genuinely in-flight request to an already-answered URL came out with
|
|
1325
|
-
// no outcome at all — which `isFailedRequest`'s older-build fallback
|
|
1326
|
-
// reads as a failure, because its status is null. A healthy in-flight
|
|
1327
|
-
// request reported as a failed one. No record consumed means no terminal
|
|
1328
|
-
// event was ever attributable to this request, and "pending" is exactly
|
|
1329
|
-
// that fact. An unrecognised outcome on a record that *was* consumed
|
|
1330
|
-
// still leaves the field undefined, which is the older-build case the
|
|
1331
|
-
// fallback exists for.
|
|
1332
|
-
// C3. `unattributed` before `pending`: a terminal event for this URL exists, so claiming
|
|
1333
|
-
// the request is still in flight would be as wrong as handing it a sibling's status —
|
|
1334
|
-
// and wrong in the direction that reads as healthy.
|
|
1335
|
-
const outcome = undetermined
|
|
1336
|
-
? "unattributed"
|
|
1337
|
-
: matched === undefined
|
|
1338
|
-
? "pending"
|
|
1339
|
-
: matched.outcome;
|
|
1340
|
-
requests.push({
|
|
1341
|
-
method,
|
|
1342
|
-
url,
|
|
1343
|
-
status: undetermined ? null : (matched?.status ?? null),
|
|
1344
|
-
...(outcome === undefined ? {} : { outcome }),
|
|
1345
|
-
stackText: renderStack(evidence.stackTrace),
|
|
1346
|
-
stackTrace: evidence.stackTrace,
|
|
1347
|
-
scriptUrlMapping: scriptUrlOutcomesOf(evidence.payload),
|
|
1348
|
-
at: evidence.timestamp,
|
|
1349
|
-
});
|
|
1350
|
-
continue;
|
|
1351
|
-
}
|
|
1352
|
-
// Uncaught page error and console error both mean "something went wrong"; callers don't need to know which listener fired.
|
|
1353
|
-
if (evidence.eventType === "EXCEPTION" || evidence.eventType === "CONSOLE_MESSAGE") {
|
|
1354
|
-
const level = evidence.payload["level"] ?? evidence.payload["type"];
|
|
1355
|
-
if (evidence.eventType === "CONSOLE_MESSAGE" && level !== "error")
|
|
1356
|
-
continue;
|
|
1357
|
-
const text = evidence.payload["text"] ?? evidence.payload["message"];
|
|
1358
|
-
consoleErrors.push({
|
|
1359
|
-
text: typeof text === "string" ? text : JSON.stringify(evidence.payload),
|
|
1360
|
-
stackText: renderStack(evidence.stackTrace),
|
|
1361
|
-
scriptUrlMapping: scriptUrlOutcomesOf(evidence.payload),
|
|
1362
|
-
at: evidence.timestamp,
|
|
1363
|
-
});
|
|
1364
|
-
}
|
|
1365
|
-
}
|
|
1366
|
-
return { actions: [...actions], consoleErrors, requests, unsettled: [...unsettled], evidenceWarnings: [...warnings] };
|
|
1367
|
-
}
|
|
1368
|
-
/** B4's pair check. Returns the outcome that may be published alongside `status`, dropping it —
|
|
1369
|
-
* and naming the drop in `warnings` — when the two cannot both be true. Deliberately one-sided:
|
|
1370
|
-
* a status is a number the collector read off a real response object, an outcome is a label it
|
|
1371
|
-
* derived, so when they disagree the derived half is the one that goes. `url` only names the
|
|
1372
|
-
* request in the warning; a missing one degrades the text, never the check. */
|
|
1373
|
-
function reconcile(status, outcome, url, warnings) {
|
|
1374
|
-
const impossible = (outcome === "unanswered" && status !== null) || (outcome === "answered" && status === null);
|
|
1375
|
-
if (!impossible)
|
|
1376
|
-
return outcome;
|
|
1377
|
-
const where = typeof url === "string" ? ` for ${url}` : "";
|
|
1378
|
-
warnings.add(`Descry's browser collector reported a request${where} as both "${outcome}" and ` +
|
|
1379
|
-
`status ${status === null ? "null" : String(status)}, which cannot both be true. The status is ` +
|
|
1380
|
-
"what was read off the response, so it is kept and the outcome is dropped — this request is " +
|
|
1381
|
-
"reported without one rather than with a contradiction. Usually a version mismatch between " +
|
|
1382
|
-
"@descryy/mcp and @descryy/runtime-browser.");
|
|
1383
|
-
return undefined;
|
|
1384
|
-
}
|
|
1385
|
-
/** Reads `outcome` off an untyped cross-package payload; validated not cast, since this build doesn't depend on `@descryy/runtime-browser`'s types — unrecognised values degrade to undefined, never a guessed literal. `"unattributed"` is deliberately not accepted here: it is this package's verdict about a join it could not make, and a collector is in no position to assert it. */
|
|
1386
|
-
function requestOutcomeOf(payload) {
|
|
1387
|
-
const raw = payload["outcome"];
|
|
1388
|
-
if (raw === "answered" || raw === "unanswered" || raw === "pending")
|
|
1389
|
-
return raw;
|
|
1390
|
-
return undefined;
|
|
1391
|
-
}
|
|
1392
|
-
/** Reads the collector's transport-level exchange id, when it carries one. Validated not cast,
|
|
1393
|
-
* and an empty string is treated as absent — an id that identifies nothing would put every
|
|
1394
|
-
* request into one bucket, which is worse than having none. */
|
|
1395
|
-
function exchangeIdOf(payload) {
|
|
1396
|
-
const raw = payload["exchangeId"];
|
|
1397
|
-
return typeof raw === "string" && raw !== "" ? raw : null;
|
|
1398
|
-
}
|
|
1399
|
-
/** C3's disclosure. Says what was observed and refuses to say whose it was — the terminal
|
|
1400
|
-
* events are named in full so a real 500 is never lost, only left unassigned. */
|
|
1401
|
-
function undeterminedJoinWarning(url, unkeyed, records) {
|
|
1402
|
-
const describe = (record) => `${record.status === null ? "no status" : `status ${record.status}`}${record.outcome === undefined ? "" : ` (${record.outcome})`}`;
|
|
1403
|
-
const shortfall = records.length < unkeyed
|
|
1404
|
-
? ` Fewer terminal events arrived than requests were issued, so at least one of them was still in flight, and which one is the same unanswerable question.`
|
|
1405
|
-
: "";
|
|
1406
|
-
return (`Descry saw ${unkeyed} requests to ${url} carrying no id of their own, and ${records.length} terminal ` +
|
|
1407
|
-
`event(s) for that URL that do not all say the same thing: ${records.map(describe).join("; ")}.${shortfall} ` +
|
|
1408
|
-
"Pairing them by arrival order would be right roughly half the time and silent when it was wrong, so none " +
|
|
1409
|
-
"of these requests is reported with a status or an outcome. The events just listed did happen — Descry " +
|
|
1410
|
-
"cannot say which request each one belongs to. A transport-level exchange id from the browser collector " +
|
|
1411
|
-
"would remove this; the collector in this build does not emit one.");
|
|
1412
|
-
}
|
|
1413
|
-
/** Reads `scriptUrlMapping` off an untyped cross-package payload; validated not cast, so a malformed/absent field degrades to "no mapping reported" rather than a half-populated result. */
|
|
1414
|
-
function scriptUrlOutcomesOf(payload) {
|
|
1415
|
-
const raw = payload["scriptUrlMapping"];
|
|
1416
|
-
if (!Array.isArray(raw))
|
|
1417
|
-
return [];
|
|
1418
|
-
const outcomes = [];
|
|
1419
|
-
for (const entry of raw) {
|
|
1420
|
-
if (typeof entry !== "object" || entry === null)
|
|
1421
|
-
continue;
|
|
1422
|
-
const record = entry;
|
|
1423
|
-
const url = record["url"];
|
|
1424
|
-
if (typeof url !== "string")
|
|
1425
|
-
continue;
|
|
1426
|
-
const mapped = record["mapped"];
|
|
1427
|
-
const reason = record["reason"];
|
|
1428
|
-
outcomes.push({
|
|
1429
|
-
url,
|
|
1430
|
-
mapped: typeof mapped === "string" ? mapped : null,
|
|
1431
|
-
reason: typeof reason === "string" ? reason : null,
|
|
1432
|
-
});
|
|
1433
|
-
}
|
|
1434
|
-
return outcomes;
|
|
1435
|
-
}
|
|
1436
|
-
/** Human-readable rendering of parsed stack frames, for the reply; the R4 write uses `stackTrace` itself, never this. Primary frame marked since it isn't always the innermost. */
|
|
1437
|
-
function renderStack(stack) {
|
|
1438
|
-
if (stack === null || stack.frames.length === 0)
|
|
1439
|
-
return null;
|
|
1440
|
-
return stack.frames
|
|
1441
|
-
.slice(0, 5)
|
|
1442
|
-
.map((frame, index) => {
|
|
1443
|
-
const where = `${frame.location.file}:${frame.location.line}`;
|
|
1444
|
-
const name = frame.location.functionName ?? "(anonymous)";
|
|
1445
|
-
return `${index === stack.primaryFrameIndex ? "> " : " "}at ${name} (${where})`;
|
|
1446
|
-
})
|
|
1447
|
-
.join("\n");
|
|
1448
|
-
}
|
|
1449
|
-
/**
|
|
1450
|
-
* The runtime's auth seed for this launch, or null when the caller named none.
|
|
1451
|
-
*
|
|
1452
|
-
* The origin comes from `targetUrl` and from nowhere else — it is the same
|
|
1453
|
-
* value the registry records as the session's consented origin, so "the seed
|
|
1454
|
-
* is scoped to what was consented" is a property of one derivation rather
|
|
1455
|
-
* than of two that have to agree.
|
|
1456
|
-
*/
|
|
1457
|
-
function runtimeAuthFor(launchOptions) {
|
|
1458
|
-
const state = launchOptions.storageState;
|
|
1459
|
-
const headers = launchOptions.extraHttpHeaders;
|
|
1460
|
-
if (state === undefined && headers === undefined)
|
|
1461
|
-
return null;
|
|
1462
|
-
let origin;
|
|
1463
|
-
try {
|
|
1464
|
-
origin = new URL(launchOptions.targetUrl).origin;
|
|
1465
|
-
}
|
|
1466
|
-
catch {
|
|
1467
|
-
// Unparseable targets are refused far above this — the tool cannot mint
|
|
1468
|
-
// consent for a URL it could not read an origin from. Carrying the raw
|
|
1469
|
-
// string keeps this total rather than throwing from inside a launch.
|
|
1470
|
-
origin = launchOptions.targetUrl;
|
|
1471
|
-
}
|
|
1472
|
-
return {
|
|
1473
|
-
origin,
|
|
1474
|
-
...(state?.cookies === undefined ? {} : { cookies: state.cookies }),
|
|
1475
|
-
...(state?.localStorage === undefined ? {} : { localStorage: state.localStorage }),
|
|
1476
|
-
...(headers === undefined ? {} : { extraHttpHeaders: headers }),
|
|
1477
|
-
};
|
|
1478
|
-
}
|
|
1479
|
-
//# sourceMappingURL=playwright-driver.js.map
|