sourcemonkey 0.0.0-stage → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +87 -2
- package/dist/bytes.js +37 -0
- package/dist/channel-placeholder.js +5 -0
- package/dist/collection-scan.js +17 -0
- package/dist/collections.html +353 -0
- package/dist/compare.html +358 -0
- package/dist/composed-source.js +205 -0
- package/dist/connect-prompt.html +47 -0
- package/dist/debug/icons.html +211 -0
- package/dist/debug/storage.html +163 -0
- package/dist/debug-log.js +75 -0
- package/dist/diagnostic-location.js +64 -0
- package/dist/eval-hook.js +308 -0
- package/dist/extension/background.js +5263 -0
- package/dist/extension/channel.js +133 -0
- package/dist/extension/code-links.js +187 -0
- package/dist/extension/code-tabs.js +460 -0
- package/dist/extension/collection-dialog.js +619 -0
- package/dist/extension/collection-display.js +322 -0
- package/dist/extension/collections.js +2905 -0
- package/dist/extension/compare.js +867 -0
- package/dist/extension/concurrency.js +186 -0
- package/dist/extension/connect-grants.js +109 -0
- package/dist/extension/connect-prompt-page.js +70 -0
- package/dist/extension/connect-prompt.js +266 -0
- package/dist/extension/copy-button.js +63 -0
- package/dist/extension/copy-dialog.js +502 -0
- package/dist/extension/copy-preference.js +66 -0
- package/dist/extension/debug/icons.js +203 -0
- package/dist/extension/debug/storage.js +365 -0
- package/dist/extension/deep-link.js +77 -0
- package/dist/extension/delete-dialog.js +432 -0
- package/dist/extension/delete-icon.js +40 -0
- package/dist/extension/details-tooltips.js +66 -0
- package/dist/extension/directories.js +679 -0
- package/dist/extension/export-backup.js +407 -0
- package/dist/extension/export-dialog.js +142 -0
- package/dist/extension/file-access.js +74 -0
- package/dist/extension/file-changes.js +346 -0
- package/dist/extension/frame-info.js +85 -0
- package/dist/extension/github-provider.js +336 -0
- package/dist/extension/glyph.js +84 -0
- package/dist/extension/gm-bindings.js +601 -0
- package/dist/extension/gm-download.js +419 -0
- package/dist/extension/gm-menu.js +228 -0
- package/dist/extension/gm-notification.js +141 -0
- package/dist/extension/gm-originator-map.js +23 -0
- package/dist/extension/gm-tab-data.js +121 -0
- package/dist/extension/gm-tab.js +160 -0
- package/dist/extension/gm-url-change.js +111 -0
- package/dist/extension/gm-url-scheme.js +27 -0
- package/dist/extension/gm-values.js +176 -0
- package/dist/extension/gm-window-control.js +104 -0
- package/dist/extension/gm-xhr-headers.js +333 -0
- package/dist/extension/gm-xhr.js +584 -0
- package/dist/extension/injector.js +3145 -0
- package/dist/extension/install-capture.js +277 -0
- package/dist/extension/line-diff.js +233 -0
- package/dist/extension/local-copy.js +560 -0
- package/dist/extension/local-delete.js +276 -0
- package/dist/extension/location-kinds.js +108 -0
- package/dist/extension/log-format.js +158 -0
- package/dist/extension/managed-changes.js +361 -0
- package/dist/extension/managed-items.js +57 -0
- package/dist/extension/match-index.js +375 -0
- package/dist/extension/missing-scripts.js +66 -0
- package/dist/extension/more-menu.js +109 -0
- package/dist/extension/move-dialog.js +197 -0
- package/dist/extension/offscreen.js +37 -0
- package/dist/extension/options.js +504 -0
- package/dist/extension/page-heading.js +170 -0
- package/dist/extension/page-links.js +199 -0
- package/dist/extension/parse-check-client.js +70 -0
- package/dist/extension/parse-check.js +161 -0
- package/dist/extension/parse-sandbox-host.js +172 -0
- package/dist/extension/parse-sandbox.js +175 -0
- package/dist/extension/popup-commands-open.js +41 -0
- package/dist/extension/popup-view.js +387 -0
- package/dist/extension/popup.js +703 -0
- package/dist/extension/prism-userscript.js +53 -0
- package/dist/extension/refresh-error.js +136 -0
- package/dist/extension/refresh-targets.js +160 -0
- package/dist/extension/related-versions.js +1144 -0
- package/dist/extension/resize-observer-loop.js +19 -0
- package/dist/extension/row-cache.js +387 -0
- package/dist/extension/script-blobs.js +413 -0
- package/dist/extension/script-details.js +658 -0
- package/dist/extension/script-enablement.js +285 -0
- package/dist/extension/script-identity.js +310 -0
- package/dist/extension/script-keys.js +54 -0
- package/dist/extension/script-log-id.js +155 -0
- package/dist/extension/script-log.js +290 -0
- package/dist/extension/script-rejection.js +104 -0
- package/dist/extension/script-sites.js +158 -0
- package/dist/extension/script-store.js +3507 -0
- package/dist/extension/script-updater.js +991 -0
- package/dist/extension/script-url-hash.js +61 -0
- package/dist/extension/select-all.js +37 -0
- package/dist/extension/select-open-guard.js +90 -0
- package/dist/extension/site-icon.js +1261 -0
- package/dist/extension/sm-dev-link.js +302 -0
- package/dist/extension/solo-click.js +58 -0
- package/dist/extension/sticky-numbers.js +188 -0
- package/dist/extension/storage-breakdown.js +253 -0
- package/dist/extension/storage-scan.js +86 -0
- package/dist/extension/storage-usage.js +194 -0
- package/dist/extension/structural-keys.js +29 -0
- package/dist/extension/sw-teardown.js +36 -0
- package/dist/extension/tab-status.js +403 -0
- package/dist/extension/toolbar-summary.js +105 -0
- package/dist/extension/user-scripts-access.js +43 -0
- package/dist/extension/userscript.js +3672 -0
- package/dist/extension/value-store.js +204 -0
- package/dist/extension/version-probe-fetch.js +68 -0
- package/dist/extension/worker-client.js +109 -0
- package/dist/extension/worker-ops.js +171 -0
- package/dist/extension/zip-writer.js +24 -0
- package/dist/external-files.js +878 -0
- package/dist/file-loader.js +436 -0
- package/dist/file-locations.js +439 -0
- package/dist/generated-wrappers.js +5 -0
- package/dist/github-auth.js +148 -0
- package/dist/github-urls.js +46 -0
- package/dist/gm-api-catalog.js +217 -0
- package/dist/gm-connect.js +273 -0
- package/dist/gm-xhr-types.js +5 -0
- package/dist/http-read.js +142 -0
- package/dist/icons/icon-128.png +0 -0
- package/dist/icons/icon-16.png +0 -0
- package/dist/icons/icon-48.png +0 -0
- package/dist/local-file-overrides.js +558 -0
- package/dist/manifest.json +38 -0
- package/dist/markup-response.js +126 -0
- package/dist/match-targets.js +305 -0
- package/dist/menu-captions.js +22 -0
- package/dist/offscreen.html +39 -0
- package/dist/options.html +197 -0
- package/dist/parse-check-protocol.js +51 -0
- package/dist/parse-location.js +171 -0
- package/dist/parse-sandbox.html +56 -0
- package/dist/phase-timing.js +155 -0
- package/dist/popup.html +197 -0
- package/dist/public-suffixes.js +6957 -0
- package/dist/run-context.js +31 -0
- package/dist/runtime-messages.js +34 -0
- package/dist/sandboxed-page.js +35 -0
- package/dist/script-build.js +787 -0
- package/dist/script-display.js +383 -0
- package/dist/script-icon.js +43 -0
- package/dist/script-links.js +204 -0
- package/dist/script-source-url.js +214 -0
- package/dist/script-world.js +65 -0
- package/dist/sm-dev-protocol.js +84 -0
- package/dist/source-map.js +177 -0
- package/dist/stack-link.js +138 -0
- package/dist/styles.css +825 -0
- package/dist/unsafe-window-use.js +229 -0
- package/dist/url-match.js +734 -0
- package/dist/userscript-header.js +248 -0
- package/dist/userscript.html +1165 -0
- package/dist/vendor/fflate.LICENSE +21 -0
- package/dist/vendor/fflate.js +2692 -0
- package/dist/vendor/prism/prism-line-numbers.css +1 -0
- package/dist/vendor/prism/prism-line-numbers.js +1 -0
- package/dist/vendor/prism/prism.css +1 -0
- package/dist/vendor/prism/prism.js +1946 -0
- package/dist/version-compare.js +141 -0
- package/dist/version-probe.js +120 -0
- package/lib/build-info.json +1 -0
- package/lib/bytes.d.ts +3 -0
- package/lib/bytes.js +37 -0
- package/lib/channel-placeholder.d.ts +1 -0
- package/lib/channel-placeholder.js +5 -0
- package/lib/collection-scan.d.ts +8 -0
- package/lib/collection-scan.js +17 -0
- package/lib/composed-source.d.ts +28 -0
- package/lib/composed-source.js +205 -0
- package/lib/debug-log.d.ts +9 -0
- package/lib/debug-log.js +75 -0
- package/lib/diagnostic-location.d.ts +8 -0
- package/lib/diagnostic-location.js +64 -0
- package/lib/eval-hook.d.ts +34 -0
- package/lib/eval-hook.js +308 -0
- package/lib/external-files.d.ts +111 -0
- package/lib/external-files.js +878 -0
- package/lib/file-loader.d.ts +48 -0
- package/lib/file-loader.js +436 -0
- package/lib/file-locations.d.ts +44 -0
- package/lib/file-locations.js +439 -0
- package/lib/generated-wrappers.d.ts +11 -0
- package/lib/generated-wrappers.js +5 -0
- package/lib/github-auth.d.ts +12 -0
- package/lib/github-auth.js +148 -0
- package/lib/github-urls.d.ts +1 -0
- package/lib/github-urls.js +46 -0
- package/lib/gm-api-catalog.d.ts +24 -0
- package/lib/gm-api-catalog.js +217 -0
- package/lib/gm-connect.d.ts +34 -0
- package/lib/gm-connect.js +273 -0
- package/lib/gm-xhr-types.d.ts +68 -0
- package/lib/gm-xhr-types.js +5 -0
- package/lib/harness/browser-extensions.d.ts +73 -0
- package/lib/harness/browser-extensions.js +285 -0
- package/lib/harness/browser-lang.d.ts +20 -0
- package/lib/harness/browser-lang.js +86 -0
- package/lib/harness/browser-launch.d.ts +14 -0
- package/lib/harness/browser-launch.js +184 -0
- package/lib/harness/checks.d.ts +110 -0
- package/lib/harness/checks.js +321 -0
- package/lib/harness/cli.d.ts +2 -0
- package/lib/harness/cli.js +2259 -0
- package/lib/harness/command-args.d.ts +91 -0
- package/lib/harness/command-args.js +369 -0
- package/lib/harness/command-lines.d.ts +80 -0
- package/lib/harness/command-lines.js +180 -0
- package/lib/harness/command-list.d.ts +206 -0
- package/lib/harness/command-list.js +1475 -0
- package/lib/harness/console-commands.d.ts +463 -0
- package/lib/harness/console-commands.js +3216 -0
- package/lib/harness/download-watch.d.ts +21 -0
- package/lib/harness/download-watch.js +156 -0
- package/lib/harness/extension-backend.d.ts +41 -0
- package/lib/harness/extension-backend.js +100 -0
- package/lib/harness/extension-host.d.ts +112 -0
- package/lib/harness/extension-host.js +552 -0
- package/lib/harness/extension-launch.d.ts +67 -0
- package/lib/harness/extension-launch.js +285 -0
- package/lib/harness/extension-link.d.ts +315 -0
- package/lib/harness/extension-link.js +607 -0
- package/lib/harness/external-manager.d.ts +146 -0
- package/lib/harness/external-manager.js +414 -0
- package/lib/harness/fixture.d.ts +165 -0
- package/lib/harness/fixture.js +461 -0
- package/lib/harness/frame-sessions.d.ts +71 -0
- package/lib/harness/frame-sessions.js +248 -0
- package/lib/harness/frames.d.ts +68 -0
- package/lib/harness/frames.js +216 -0
- package/lib/harness/help-format.d.ts +17 -0
- package/lib/harness/help-format.js +127 -0
- package/lib/harness/host.d.ts +352 -0
- package/lib/harness/host.js +423 -0
- package/lib/harness/index.d.ts +8 -0
- package/lib/harness/index.js +10 -0
- package/lib/harness/input-file.d.ts +18 -0
- package/lib/harness/input-file.js +61 -0
- package/lib/harness/json-output.d.ts +565 -0
- package/lib/harness/json-output.js +71 -0
- package/lib/harness/manager-actions.d.ts +21 -0
- package/lib/harness/manager-actions.js +129 -0
- package/lib/harness/manager-commands.d.ts +142 -0
- package/lib/harness/manager-commands.js +1196 -0
- package/lib/harness/manager-fetch.d.ts +33 -0
- package/lib/harness/manager-fetch.js +239 -0
- package/lib/harness/managers.d.ts +24 -0
- package/lib/harness/managers.js +27 -0
- package/lib/harness/output-style.d.ts +125 -0
- package/lib/harness/output-style.js +322 -0
- package/lib/harness/page-commands.d.ts +134 -0
- package/lib/harness/page-commands.js +1141 -0
- package/lib/harness/page-output.d.ts +67 -0
- package/lib/harness/page-output.js +139 -0
- package/lib/harness/page-watch.d.ts +171 -0
- package/lib/harness/page-watch.js +607 -0
- package/lib/harness/parse-check-node.d.ts +3 -0
- package/lib/harness/parse-check-node.js +132 -0
- package/lib/harness/path-args.d.ts +22 -0
- package/lib/harness/path-args.js +148 -0
- package/lib/harness/playwright-chromium.d.ts +16 -0
- package/lib/harness/playwright-chromium.js +86 -0
- package/lib/harness/prepare.d.ts +168 -0
- package/lib/harness/prepare.js +364 -0
- package/lib/harness/prompt-editor.d.ts +17 -0
- package/lib/harness/prompt-editor.js +354 -0
- package/lib/harness/prompt-history.d.ts +13 -0
- package/lib/harness/prompt-history.js +89 -0
- package/lib/harness/rebuild-advice.d.ts +6 -0
- package/lib/harness/rebuild-advice.js +39 -0
- package/lib/harness/run-log.d.ts +47 -0
- package/lib/harness/run-log.js +140 -0
- package/lib/harness/run-logs.d.ts +132 -0
- package/lib/harness/run-logs.js +241 -0
- package/lib/harness/run.d.ts +194 -0
- package/lib/harness/run.js +1973 -0
- package/lib/harness/runs.d.ts +229 -0
- package/lib/harness/runs.js +726 -0
- package/lib/harness/save-html.d.ts +39 -0
- package/lib/harness/save-html.js +89 -0
- package/lib/harness/saved-pages.d.ts +120 -0
- package/lib/harness/saved-pages.js +275 -0
- package/lib/harness/script-mark.d.ts +17 -0
- package/lib/harness/script-mark.js +56 -0
- package/lib/harness/snapshot.d.ts +143 -0
- package/lib/harness/snapshot.js +474 -0
- package/lib/harness/stuck-tabs.d.ts +19 -0
- package/lib/harness/stuck-tabs.js +131 -0
- package/lib/harness/tab-refs.d.ts +20 -0
- package/lib/harness/tab-refs.js +80 -0
- package/lib/harness/tampermonkey.d.ts +71 -0
- package/lib/harness/tampermonkey.js +356 -0
- package/lib/harness/tm-ids.d.ts +18 -0
- package/lib/harness/tm-ids.js +88 -0
- package/lib/harness/violentmonkey.d.ts +34 -0
- package/lib/harness/violentmonkey.js +162 -0
- package/lib/harness/wait.d.ts +77 -0
- package/lib/harness/wait.js +458 -0
- package/lib/http-read.d.ts +30 -0
- package/lib/http-read.js +142 -0
- package/lib/local-file-overrides.d.ts +35 -0
- package/lib/local-file-overrides.js +558 -0
- package/lib/manifest.json +38 -0
- package/lib/markup-response.d.ts +11 -0
- package/lib/markup-response.js +126 -0
- package/lib/match-targets.d.ts +18 -0
- package/lib/match-targets.js +305 -0
- package/lib/menu-captions.d.ts +1 -0
- package/lib/menu-captions.js +22 -0
- package/lib/parse-check-protocol.d.ts +27 -0
- package/lib/parse-check-protocol.js +51 -0
- package/lib/parse-location.d.ts +12 -0
- package/lib/parse-location.js +171 -0
- package/lib/phase-timing.d.ts +12 -0
- package/lib/phase-timing.js +155 -0
- package/lib/public-suffixes.d.ts +1 -0
- package/lib/public-suffixes.js +6957 -0
- package/lib/run-context.d.ts +2 -0
- package/lib/run-context.js +31 -0
- package/lib/runtime-messages.d.ts +10 -0
- package/lib/runtime-messages.js +34 -0
- package/lib/sandboxed-page.d.ts +5 -0
- package/lib/sandboxed-page.js +35 -0
- package/lib/script-build.d.ts +111 -0
- package/lib/script-build.js +787 -0
- package/lib/script-display.d.ts +51 -0
- package/lib/script-display.js +383 -0
- package/lib/script-icon.d.ts +16 -0
- package/lib/script-icon.js +43 -0
- package/lib/script-links.d.ts +24 -0
- package/lib/script-links.js +204 -0
- package/lib/script-source-url.d.ts +8 -0
- package/lib/script-source-url.js +214 -0
- package/lib/script-world.d.ts +6 -0
- package/lib/script-world.js +65 -0
- package/lib/sm-dev-protocol.d.ts +251 -0
- package/lib/sm-dev-protocol.js +84 -0
- package/lib/source-map.d.ts +32 -0
- package/lib/source-map.js +177 -0
- package/lib/stack-link.d.ts +15 -0
- package/lib/stack-link.js +138 -0
- package/lib/unsafe-window-use.d.ts +1 -0
- package/lib/unsafe-window-use.js +229 -0
- package/lib/url-match.d.ts +40 -0
- package/lib/url-match.js +734 -0
- package/lib/userscript-header.d.ts +33 -0
- package/lib/userscript-header.js +248 -0
- package/lib/version-compare.d.ts +6 -0
- package/lib/version-compare.js +141 -0
- package/lib/version-probe.d.ts +17 -0
- package/lib/version-probe.js +120 -0
- package/package.json +55 -3
- package/skills/SourceMonkey-dev/SKILL.md +868 -0
- package/skills/SourceMonkey-dev/references/cdp.md +293 -0
- package/skills/SourceMonkey-dev/references/commands.md +297 -0
- package/skills/SourceMonkey-dev/references/other-managers.md +63 -0
- package/skills/SourceMonkey-dev/references/specs.md +46 -0
- package/skills/SourceMonkey-friction/SKILL.md +103 -0
- package/skills/SourceMonkey-friction/references/template.md +59 -0
|
@@ -0,0 +1,607 @@
|
|
|
1
|
+
// Watching a page the extension runs scripts in: a run's tab, or the
|
|
2
|
+
// spec fixture's page. Adds nothing the page or its scripts can see.
|
|
3
|
+
//
|
|
4
|
+
// - Tracks the page's documents (when each began), for the waits and
|
|
5
|
+
// for ordering the extension's events against navigations
|
|
6
|
+
// (extension-host.ts).
|
|
7
|
+
// - Records which scripts the tab runs, for the host's skip reports and
|
|
8
|
+
// the waits; the extension runs them.
|
|
9
|
+
// - Reads what `wait` needs (DOM activity, requests in flight), from a
|
|
10
|
+
// CDP-made world of its own, and evaluates `eval` in the page's world
|
|
11
|
+
// or the scripts' (the extension's user-script world).
|
|
12
|
+
import { FrameSessions } from './frame-sessions.js';
|
|
13
|
+
// The world the activity tracker runs in, created by CDP: its own, so
|
|
14
|
+
// neither the page nor the scripts see it.
|
|
15
|
+
const WATCH_WORLD = 'sm-dev-watch';
|
|
16
|
+
// When the DOM last changed, and where, for `wait` (see wait.ts): a
|
|
17
|
+
// MutationObserver from the document's start, in the watch world.
|
|
18
|
+
const ACTIVITY_KEY = 'sm-dev-activity';
|
|
19
|
+
const ACTIVITY_TRACKER = `(function(){
|
|
20
|
+
if (self !== top) return;
|
|
21
|
+
var key = Symbol.for(${JSON.stringify(ACTIVITY_KEY)});
|
|
22
|
+
if (globalThis[key]) return;
|
|
23
|
+
var state = globalThis[key] = { last: Date.now(), target: '' };
|
|
24
|
+
function describe(node) {
|
|
25
|
+
var el = node.nodeType === 1 ? node : node.parentElement;
|
|
26
|
+
if (!el) return '';
|
|
27
|
+
var classes = typeof el.className === 'string' && el.className.trim()
|
|
28
|
+
? '.' + el.className.trim().split(/\\s+/).slice(0, 2).join('.') : '';
|
|
29
|
+
return el.localName + (el.id ? '#' + el.id : '') + classes;
|
|
30
|
+
}
|
|
31
|
+
new MutationObserver(function(records){
|
|
32
|
+
state.last = Date.now();
|
|
33
|
+
state.target = describe(records[records.length - 1].target);
|
|
34
|
+
}).observe(document, { childList: true, subtree: true, attributes: true, characterData: true });
|
|
35
|
+
})()`;
|
|
36
|
+
// Results JSON can't show, printed as the console describes them.
|
|
37
|
+
const DESCRIBED = new Set(['node', 'error', 'map', 'set', 'weakmap', 'weakset', 'nodelist', 'htmlcollection', 'regexp', 'date', 'proxy', 'promise', 'generator', 'iterator']);
|
|
38
|
+
/** What an expression `evaluate` ran threw, as opposed to evaluate failing. */
|
|
39
|
+
export class ScriptThrew extends Error {
|
|
40
|
+
}
|
|
41
|
+
/** `evaluate` found no eval hook for the script: it hasn't run in this document. */
|
|
42
|
+
export class NoEvalHook extends Error {
|
|
43
|
+
}
|
|
44
|
+
/** `evaluate` found no document to run in: the page stayed between documents. */
|
|
45
|
+
export class NoDocument extends Error {
|
|
46
|
+
}
|
|
47
|
+
// How long `evaluate` waits for a page that is between documents (a
|
|
48
|
+
// reload or navigation under way) to have one again.
|
|
49
|
+
const DOCUMENT_WAIT_MS = 5000;
|
|
50
|
+
// How long `drain` waits for the page to run a timer.
|
|
51
|
+
const DRAIN_WAIT_MS = 500;
|
|
52
|
+
const BETWEEN_DOCUMENTS = 'The page is between documents (reloading or navigating), so there is none to evaluate in';
|
|
53
|
+
// What an expression threw: an Error's description, else the value
|
|
54
|
+
// thrown (`throw 'boom'`).
|
|
55
|
+
function threw(details) {
|
|
56
|
+
const exception = details.exception;
|
|
57
|
+
const thrown = exception?.description ?? (exception && 'value' in exception ? JSON.stringify(exception.value) : details.text);
|
|
58
|
+
return new ScriptThrew(thrown.split('\n')[0]);
|
|
59
|
+
}
|
|
60
|
+
// A top-level `return` in `eval`'s code: V8's syntax error for it.
|
|
61
|
+
// Only as the code compiled, which has no stack trace. One thrown as it
|
|
62
|
+
// ran (by an inner `eval("return")`) had its side effects already.
|
|
63
|
+
function isIllegalReturn(details) {
|
|
64
|
+
const exception = details.exception;
|
|
65
|
+
return !details.stackTrace && exception?.className === 'SyntaxError' && (exception.description ?? '').startsWith('SyntaxError: Illegal return statement');
|
|
66
|
+
}
|
|
67
|
+
// Code that's only valid as a function body (a top-level `return`, or
|
|
68
|
+
// `await`), from the syntax error compiling it as a script gave.
|
|
69
|
+
function needsFunctionBody(details) {
|
|
70
|
+
const exception = details.exception;
|
|
71
|
+
return /Illegal return statement|await is only valid/.test(exception?.description ?? details.text);
|
|
72
|
+
}
|
|
73
|
+
// Pages that already have an attachment.
|
|
74
|
+
const attached = new WeakSet();
|
|
75
|
+
/**
|
|
76
|
+
* One page watched for a host. Create with `attachPage`, then record the
|
|
77
|
+
* scripts its tab runs with `inject`.
|
|
78
|
+
*/
|
|
79
|
+
export class PageAttachment {
|
|
80
|
+
page;
|
|
81
|
+
host;
|
|
82
|
+
cdp;
|
|
83
|
+
options;
|
|
84
|
+
injected = new Map();
|
|
85
|
+
disposing = false;
|
|
86
|
+
closed = false;
|
|
87
|
+
mainFrameId = '';
|
|
88
|
+
// The main frame's recent documents by loader id: one restored from
|
|
89
|
+
// the back/forward cache is the same document again.
|
|
90
|
+
documents = new Map();
|
|
91
|
+
currentDocument = { url: '', spans: [[0, Infinity]] };
|
|
92
|
+
// Execution contexts by id, to find a world's context in the main frame.
|
|
93
|
+
contexts = new Map();
|
|
94
|
+
// Sessions for evaluating in iframes, opened on the first `eval` in one.
|
|
95
|
+
frameSessions;
|
|
96
|
+
// Requests in flight, by when each started; see `network`.
|
|
97
|
+
requests = new Map();
|
|
98
|
+
lastRequestEnd = 0;
|
|
99
|
+
onRequest = (request) => { this.requests.set(request, Date.now()); };
|
|
100
|
+
onRequestEnd = (request) => {
|
|
101
|
+
if (this.requests.delete(request))
|
|
102
|
+
this.lastRequestEnd = Date.now();
|
|
103
|
+
};
|
|
104
|
+
onClose = () => {
|
|
105
|
+
this.closed = true;
|
|
106
|
+
this.host.detachPage(this);
|
|
107
|
+
};
|
|
108
|
+
constructor(page, host, cdp, options) {
|
|
109
|
+
this.page = page;
|
|
110
|
+
this.host = host;
|
|
111
|
+
this.cdp = cdp;
|
|
112
|
+
this.options = options;
|
|
113
|
+
}
|
|
114
|
+
static async create(page, host, options) {
|
|
115
|
+
if (attached.has(page))
|
|
116
|
+
throw new Error('This page already has an attachment');
|
|
117
|
+
// Claimed before the first await, so two concurrent attaches can't
|
|
118
|
+
// both pass the check; released if setup fails, so the page can be
|
|
119
|
+
// attached again.
|
|
120
|
+
attached.add(page);
|
|
121
|
+
let cdp = null;
|
|
122
|
+
try {
|
|
123
|
+
cdp = await page.context().newCDPSession(page);
|
|
124
|
+
const attachment = new PageAttachment(page, host, cdp, options);
|
|
125
|
+
await attachment.install();
|
|
126
|
+
return attachment;
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
attached.delete(page);
|
|
130
|
+
await cdp?.detach().catch(() => undefined);
|
|
131
|
+
throw err;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
async install() {
|
|
135
|
+
// Before `Runtime.enable`, which reports the contexts that already exist.
|
|
136
|
+
this.cdp.on('Runtime.executionContextCreated', ({ context }) => {
|
|
137
|
+
const aux = (context.auxData ?? {});
|
|
138
|
+
this.contexts.set(context.id, { frameId: aux.frameId ?? '', name: context.name, isDefault: aux.isDefault === true, origin: context.origin });
|
|
139
|
+
});
|
|
140
|
+
this.cdp.on('Runtime.executionContextDestroyed', (ev) => { this.contexts.delete(ev.executionContextId); });
|
|
141
|
+
this.cdp.on('Runtime.executionContextsCleared', () => { this.contexts.clear(); });
|
|
142
|
+
await this.cdp.send('Page.enable');
|
|
143
|
+
await this.cdp.send('Runtime.enable');
|
|
144
|
+
await this.cdp.send('Page.addScriptToEvaluateOnNewDocument', {
|
|
145
|
+
source: ACTIVITY_TRACKER,
|
|
146
|
+
runImmediately: true,
|
|
147
|
+
worldName: WATCH_WORLD,
|
|
148
|
+
});
|
|
149
|
+
// The page's requests in flight, and when the last one ended, for
|
|
150
|
+
// `wait`. Playwright's events cover every frame of the page.
|
|
151
|
+
this.page.on('request', this.onRequest);
|
|
152
|
+
this.page.on('requestfinished', this.onRequestEnd);
|
|
153
|
+
this.page.on('requestfailed', this.onRequestEnd);
|
|
154
|
+
// New documents only. Playwright's `framenavigated` also fires for
|
|
155
|
+
// `pushState` and hash changes, where the scripts are still running
|
|
156
|
+
// from the earlier document and a "skipped" would be false.
|
|
157
|
+
this.cdp.on('Page.frameNavigated', (ev) => {
|
|
158
|
+
if (ev.frame.parentId)
|
|
159
|
+
return;
|
|
160
|
+
this.enterDocument(ev.frame.loaderId, ev.frame.url);
|
|
161
|
+
this.host.noteNavigation(this, this.injectedScripts());
|
|
162
|
+
});
|
|
163
|
+
const { frameTree } = await this.cdp.send('Page.getFrameTree');
|
|
164
|
+
this.mainFrameId = frameTree.frame.id;
|
|
165
|
+
this.currentDocument.url = frameTree.frame.url;
|
|
166
|
+
this.documents.set(frameTree.frame.loaderId, this.currentDocument);
|
|
167
|
+
this.page.on('close', this.onClose);
|
|
168
|
+
// Closed during the awaits above, before the listener was on: the
|
|
169
|
+
// event has come and gone.
|
|
170
|
+
if (this.page.isClosed())
|
|
171
|
+
this.onClose();
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* When the main frame entered its current document, by the host's
|
|
175
|
+
* clock, as reports are stamped. Changes only when a new document
|
|
176
|
+
* loads, not on a same-document (hash or `pushState`) navigation.
|
|
177
|
+
*/
|
|
178
|
+
documentStart() {
|
|
179
|
+
return this.currentDocument.spans[this.currentDocument.spans.length - 1][0];
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Resolves once the page has run the tasks it already had queued, and
|
|
183
|
+
* the console lines they printed have arrived. A round trip through
|
|
184
|
+
* Playwright's own session, as those lines are, so none comes after
|
|
185
|
+
* it. Then one through the attachment's session, so the events it
|
|
186
|
+
* was sent meanwhile have arrived too. For a
|
|
187
|
+
* command to end after what it set off in the page, such as a menu
|
|
188
|
+
* handler, or a script body, which runs after its start report.
|
|
189
|
+
*/
|
|
190
|
+
async drain() {
|
|
191
|
+
// Bounded: a page with its own scripting off (a CSP `sandbox`) never
|
|
192
|
+
// runs the timer, and has no console lines of its own to wait for.
|
|
193
|
+
await Promise.race([
|
|
194
|
+
this.page.evaluate(() => new Promise((resolve) => setTimeout(resolve, 0))).catch(() => undefined),
|
|
195
|
+
new Promise((resolve) => setTimeout(resolve, DRAIN_WAIT_MS)),
|
|
196
|
+
]);
|
|
197
|
+
await this.caughtUp();
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Resolves once the events the attachment's session had already been
|
|
201
|
+
* sent have arrived: a navigation's new document. Playwright's own
|
|
202
|
+
* session can run ahead of it.
|
|
203
|
+
*/
|
|
204
|
+
async caughtUp() {
|
|
205
|
+
if (this.closed)
|
|
206
|
+
return;
|
|
207
|
+
await this.cdp.send('Runtime.evaluate', { expression: '0' }).catch(() => undefined);
|
|
208
|
+
}
|
|
209
|
+
// The main frame moved to the document `loaderId` names.
|
|
210
|
+
enterDocument(loaderId, url) {
|
|
211
|
+
const now = Date.now();
|
|
212
|
+
this.currentDocument.spans[this.currentDocument.spans.length - 1][1] = now;
|
|
213
|
+
let doc = this.documents.get(loaderId);
|
|
214
|
+
if (!doc) {
|
|
215
|
+
doc = { url, spans: [] };
|
|
216
|
+
this.documents.set(loaderId, doc);
|
|
217
|
+
// Only the last few: the back/forward cache holds no more.
|
|
218
|
+
for (const key of this.documents.keys()) {
|
|
219
|
+
if (this.documents.size <= 10)
|
|
220
|
+
break;
|
|
221
|
+
this.documents.delete(key);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
doc.spans.push([now, Infinity]);
|
|
225
|
+
this.currentDocument = doc;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* The page's document state and its DOM's last change, for `wait`.
|
|
229
|
+
* Null when there is no document to ask. Can wait on a page busy in
|
|
230
|
+
* a long script, so the caller bounds it.
|
|
231
|
+
*/
|
|
232
|
+
async activity() {
|
|
233
|
+
const contextId = this.contextFor(WATCH_WORLD);
|
|
234
|
+
if (contextId === null || this.closed)
|
|
235
|
+
return null;
|
|
236
|
+
const { result, exceptionDetails } = await this.cdp.send('Runtime.evaluate', {
|
|
237
|
+
contextId,
|
|
238
|
+
returnByValue: true,
|
|
239
|
+
expression: `(() => {
|
|
240
|
+
const s = globalThis[Symbol.for(${JSON.stringify(ACTIVITY_KEY)})];
|
|
241
|
+
return { readyState: document.readyState, sinceDom: s ? Date.now() - s.last : 0, lastChanged: s ? s.target : '' };
|
|
242
|
+
})()`,
|
|
243
|
+
});
|
|
244
|
+
if (exceptionDetails)
|
|
245
|
+
return null;
|
|
246
|
+
return result.value;
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Resolves once the page's main thread has a moment with nothing to
|
|
250
|
+
* do (`requestIdleCallback`), or after `maxMs`.
|
|
251
|
+
*/
|
|
252
|
+
async idle(maxMs) {
|
|
253
|
+
const contextId = this.contextFor(WATCH_WORLD);
|
|
254
|
+
if (contextId === null || this.closed)
|
|
255
|
+
return;
|
|
256
|
+
await this.cdp.send('Runtime.evaluate', {
|
|
257
|
+
contextId,
|
|
258
|
+
awaitPromise: true,
|
|
259
|
+
expression: `new Promise((resolve) => requestIdleCallback(() => resolve(true), { timeout: ${maxMs} }))`,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
/** The page's requests in flight, and milliseconds since one last ended. */
|
|
263
|
+
network() {
|
|
264
|
+
const now = Date.now();
|
|
265
|
+
return {
|
|
266
|
+
pending: [...this.requests].map(([request, at]) => ({ url: request.url(), ms: now - at })),
|
|
267
|
+
sinceLast: this.lastRequestEnd === 0 ? Infinity : now - this.lastRequestEnd,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
// ---------- PageLink ----------
|
|
271
|
+
url() {
|
|
272
|
+
return this.page.url();
|
|
273
|
+
}
|
|
274
|
+
// The top frame's context the isolated scripts (`@inject-into
|
|
275
|
+
// content`, `@sandbox DOM`) run in: the extension's user-script world.
|
|
276
|
+
scriptsContext() {
|
|
277
|
+
const origin = this.options.watchOnly.extensionOrigin;
|
|
278
|
+
let found = null;
|
|
279
|
+
for (const [id, info] of this.contexts) {
|
|
280
|
+
if (info.frameId === this.mainFrameId && !info.isDefault && info.origin === origin)
|
|
281
|
+
found = id;
|
|
282
|
+
}
|
|
283
|
+
return found;
|
|
284
|
+
}
|
|
285
|
+
// The top frame's context for a world: '' is the page's own (MAIN).
|
|
286
|
+
// The newest match, should a stale one linger beside it (Map order is
|
|
287
|
+
// creation order).
|
|
288
|
+
contextFor(world) {
|
|
289
|
+
let found = null;
|
|
290
|
+
for (const [id, info] of this.contexts) {
|
|
291
|
+
if (info.frameId !== this.mainFrameId)
|
|
292
|
+
continue;
|
|
293
|
+
if (world === '' ? info.isDefault : info.name === world)
|
|
294
|
+
found = id;
|
|
295
|
+
}
|
|
296
|
+
return found;
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Evaluates `expression` in the main frame, or in `frame` (one of the
|
|
300
|
+
* page's iframes), in the page's own world or one script's own
|
|
301
|
+
* scope, for `eval`. Resolves to the result as a
|
|
302
|
+
* line to print; rejects with a `ScriptThrew` for what the expression
|
|
303
|
+
* threw, a `NoEvalHook` for a script that hasn't run here, a
|
|
304
|
+
* `NoDocument` for a frame that has gone, or an error of the
|
|
305
|
+
* evaluation's own (the page gone, say).
|
|
306
|
+
*/
|
|
307
|
+
async evaluate(expression, world, documentWaitMs = DOCUMENT_WAIT_MS, frame) {
|
|
308
|
+
const at = frame && frame !== this.page.mainFrame() ? await this.frameContexts(frame, documentWaitMs) : await this.topContexts(documentWaitMs);
|
|
309
|
+
if (typeof world === 'object')
|
|
310
|
+
return this.evaluateInScript(at, expression, world);
|
|
311
|
+
const contextId = at.page;
|
|
312
|
+
// The page context can go again between the wait and here.
|
|
313
|
+
if (contextId === null)
|
|
314
|
+
throw new NoDocument(BETWEEN_DOCUMENTS);
|
|
315
|
+
const { cdp } = at;
|
|
316
|
+
let { result, exceptionDetails } = await cdp.send('Runtime.evaluate', {
|
|
317
|
+
expression, contextId, awaitPromise: true, replMode: true, userGesture: true,
|
|
318
|
+
});
|
|
319
|
+
// Code written as a function body (`return` at the top level) is a
|
|
320
|
+
// syntax error as console input. Nothing ran, so it's safe to run
|
|
321
|
+
// again as an async function's body, whose `return` is the value.
|
|
322
|
+
// The newline keeps a trailing `// comment` off the closing `})`.
|
|
323
|
+
if (exceptionDetails && isIllegalReturn(exceptionDetails)) {
|
|
324
|
+
({ result, exceptionDetails } = await cdp.send('Runtime.evaluate', {
|
|
325
|
+
expression: `(async () => {\n${expression}\n})()`, contextId, awaitPromise: true, userGesture: true,
|
|
326
|
+
}));
|
|
327
|
+
}
|
|
328
|
+
return this.evaluated(cdp, result, exceptionDetails);
|
|
329
|
+
}
|
|
330
|
+
// The top frame's page and scripts' worlds, once it has a document.
|
|
331
|
+
async topContexts(documentWaitMs) {
|
|
332
|
+
await this.documentReady(documentWaitMs);
|
|
333
|
+
return { cdp: this.cdp, page: this.contextFor(''), scripts: this.scriptsContext() };
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* The CDP target id of an iframe in a process of its own, for a raw
|
|
337
|
+
* CDP client; undefined for one in its parent's (see frame-sessions.ts).
|
|
338
|
+
*/
|
|
339
|
+
async frameTargetId(frame) {
|
|
340
|
+
this.frameSessions ??= FrameSessions.attach(this.page);
|
|
341
|
+
return (await this.frameSessions)?.targetIdOf(frame);
|
|
342
|
+
}
|
|
343
|
+
/** An iframe's CDP frame id in the session that covers it, if found. */
|
|
344
|
+
async frameCdpId(frame) {
|
|
345
|
+
this.frameSessions ??= FrameSessions.attach(this.page);
|
|
346
|
+
return (await (await this.frameSessions)?.locate(frame))?.frameId;
|
|
347
|
+
}
|
|
348
|
+
// An iframe's page and scripts' worlds, through the session of the
|
|
349
|
+
// process it runs in (see frame-sessions.ts), opened the first time.
|
|
350
|
+
// A frame between documents (loading, or just moved to a process of
|
|
351
|
+
// its own) is looked for again until one comes, as for the top frame.
|
|
352
|
+
async frameContexts(frame, waitMs) {
|
|
353
|
+
this.frameSessions ??= FrameSessions.attach(this.page);
|
|
354
|
+
const sessions = await this.frameSessions;
|
|
355
|
+
const deadline = Date.now() + waitMs;
|
|
356
|
+
for (;;) {
|
|
357
|
+
const located = frame.isDetached() ? null : await sessions?.locate(frame);
|
|
358
|
+
if (located) {
|
|
359
|
+
const { session, frameId } = located;
|
|
360
|
+
let page = null;
|
|
361
|
+
let scripts = null;
|
|
362
|
+
for (const [id, info] of session.contexts) {
|
|
363
|
+
if (info.frameId !== frameId)
|
|
364
|
+
continue;
|
|
365
|
+
if (info.isDefault)
|
|
366
|
+
page = id;
|
|
367
|
+
else if (info.origin === this.options.watchOnly.extensionOrigin)
|
|
368
|
+
scripts = id;
|
|
369
|
+
}
|
|
370
|
+
if (page !== null)
|
|
371
|
+
return { cdp: session.cdp, page, scripts };
|
|
372
|
+
}
|
|
373
|
+
if (frame.isDetached() || Date.now() >= deadline) {
|
|
374
|
+
const name = `F${this.host.frameIds.number(frame)}`;
|
|
375
|
+
throw new NoDocument(frame.isDetached() ? `${name} has gone from the page` : `${name} has no document now. It is between pages.`);
|
|
376
|
+
}
|
|
377
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
// A page that is reloading or navigating has no document for a moment,
|
|
381
|
+
// which a site that reloads itself often makes likely. Waits for the
|
|
382
|
+
// next one, and throws `NoDocument` if none comes. The new document's
|
|
383
|
+
// scripts may not have run yet; a world or hook they set up is looked
|
|
384
|
+
// for after this.
|
|
385
|
+
async documentReady(waitMs) {
|
|
386
|
+
if (this.contextFor('') !== null)
|
|
387
|
+
return;
|
|
388
|
+
const ready = await new Promise((resolve) => {
|
|
389
|
+
const timer = setTimeout(() => finish(false), waitMs);
|
|
390
|
+
const created = () => {
|
|
391
|
+
if (this.contextFor('') !== null)
|
|
392
|
+
finish(true);
|
|
393
|
+
};
|
|
394
|
+
// The tab closing ends the wait; the caller says it closed.
|
|
395
|
+
const closed = () => finish(false);
|
|
396
|
+
const finish = (found) => {
|
|
397
|
+
clearTimeout(timer);
|
|
398
|
+
this.cdp.off('Runtime.executionContextCreated', created);
|
|
399
|
+
this.page.off('close', closed);
|
|
400
|
+
resolve(found);
|
|
401
|
+
};
|
|
402
|
+
// After the listener install() added, which records the context.
|
|
403
|
+
this.cdp.on('Runtime.executionContextCreated', created);
|
|
404
|
+
this.page.once('close', closed);
|
|
405
|
+
});
|
|
406
|
+
if (!ready)
|
|
407
|
+
throw new NoDocument(`${BETWEEN_DOCUMENTS}. No new one loaded within ${Number((waitMs / 1000).toFixed(1))} seconds.`);
|
|
408
|
+
}
|
|
409
|
+
// Through the script's eval hook, a direct `eval` in its scope. That
|
|
410
|
+
// isn't console input: `await` and a top-level `return` are syntax
|
|
411
|
+
// errors there. So the code is compiled first (which runs nothing),
|
|
412
|
+
// and code that needs it runs in an async arrow: as its expression
|
|
413
|
+
// when it is one (`await x` gives x, as in the console), else as its
|
|
414
|
+
// body, whose `return` is the value.
|
|
415
|
+
//
|
|
416
|
+
// A page-world script's hook `eval`s under the page's Content Security
|
|
417
|
+
// Policy, but within a CDP evaluation, which Chrome exempts from it.
|
|
418
|
+
//
|
|
419
|
+
// A page-world script runs isolated on a page that turns off script in
|
|
420
|
+
// its own world (docs/injection.md#csp-sandbox-pages), so its hook is
|
|
421
|
+
// looked for in the scripts' world too, after its own.
|
|
422
|
+
async evaluateInScript(at, expression, scope) {
|
|
423
|
+
const { cdp } = at;
|
|
424
|
+
const hook = `globalThis[${JSON.stringify(scope.key)}]?.[${JSON.stringify(scope.id)}]`;
|
|
425
|
+
const hasHook = async (id) => {
|
|
426
|
+
if (id === null)
|
|
427
|
+
return false;
|
|
428
|
+
const { result: found } = await cdp.send('Runtime.evaluate', {
|
|
429
|
+
expression: `typeof ${hook} === 'function'`, contextId: id, returnByValue: true,
|
|
430
|
+
});
|
|
431
|
+
return found.value === true;
|
|
432
|
+
};
|
|
433
|
+
const tried = scope.world === 'MAIN' ? [at.page, at.scripts] : [at.scripts];
|
|
434
|
+
let contextId = null;
|
|
435
|
+
for (const id of tried) {
|
|
436
|
+
if (await hasHook(id)) {
|
|
437
|
+
contextId = id;
|
|
438
|
+
break;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
if (contextId === null)
|
|
442
|
+
throw new NoEvalHook();
|
|
443
|
+
const { exceptionDetails: compiled } = await cdp.send('Runtime.compileScript', {
|
|
444
|
+
expression, sourceURL: '', persistScript: false, executionContextId: contextId,
|
|
445
|
+
});
|
|
446
|
+
let code = expression;
|
|
447
|
+
if (compiled && needsFunctionBody(compiled)) {
|
|
448
|
+
const asExpression = `(async () => (\n${expression}\n))()`;
|
|
449
|
+
const { exceptionDetails: notExpression } = await cdp.send('Runtime.compileScript', {
|
|
450
|
+
expression: asExpression, sourceURL: '', persistScript: false, executionContextId: contextId,
|
|
451
|
+
});
|
|
452
|
+
code = notExpression ? `(async () => {\n${expression}\n})()` : asExpression;
|
|
453
|
+
}
|
|
454
|
+
const { result, exceptionDetails } = await cdp.send('Runtime.evaluate', {
|
|
455
|
+
// Called detached, so its stack frame isn't named `[as <script id>]`.
|
|
456
|
+
expression: `(0, ${hook})(${JSON.stringify(code)})`, contextId, awaitPromise: true, userGesture: true,
|
|
457
|
+
});
|
|
458
|
+
return this.evaluated(cdp, result, exceptionDetails);
|
|
459
|
+
}
|
|
460
|
+
// An evaluation's result as a line to print, or what it threw.
|
|
461
|
+
async evaluated(cdp, result, exceptionDetails) {
|
|
462
|
+
if (exceptionDetails)
|
|
463
|
+
throw threw(exceptionDetails);
|
|
464
|
+
// A promise, as `eval fetch(url).then(...)` gives: what it settles
|
|
465
|
+
// to, where the console would show the promise itself.
|
|
466
|
+
if (result.subtype === 'promise' && result.objectId) {
|
|
467
|
+
const settled = await cdp.send('Runtime.awaitPromise', { promiseObjectId: result.objectId });
|
|
468
|
+
if (settled.exceptionDetails)
|
|
469
|
+
throw threw(settled.exceptionDetails);
|
|
470
|
+
return this.describe(cdp, settled.result);
|
|
471
|
+
}
|
|
472
|
+
return this.describe(cdp, result);
|
|
473
|
+
}
|
|
474
|
+
// A result as `eval` prints it: JSON where it has one, else what
|
|
475
|
+
// DevTools' console would show (an element, a Map, a function).
|
|
476
|
+
async describe(cdp, result) {
|
|
477
|
+
if (result.type === 'undefined')
|
|
478
|
+
return 'undefined';
|
|
479
|
+
if (!result.objectId)
|
|
480
|
+
return result.unserializableValue ?? JSON.stringify(result.value);
|
|
481
|
+
try {
|
|
482
|
+
if (result.type !== 'object' || (result.subtype && DESCRIBED.has(result.subtype)))
|
|
483
|
+
return result.description ?? result.type;
|
|
484
|
+
// Elements inside it as the console names them, not `{}`.
|
|
485
|
+
const { result: json } = await cdp.send('Runtime.callFunctionOn', {
|
|
486
|
+
objectId: result.objectId,
|
|
487
|
+
functionDeclaration: `function () {
|
|
488
|
+
return JSON.stringify(this, (key, value) => {
|
|
489
|
+
if (value instanceof Element) {
|
|
490
|
+
const classes = typeof value.className === 'string' && value.className.trim() ? '.' + value.className.trim().split(/\\s+/).join('.') : '';
|
|
491
|
+
return value.localName + (value.id ? '#' + value.id : '') + classes;
|
|
492
|
+
}
|
|
493
|
+
if (value instanceof Node) return value.nodeName;
|
|
494
|
+
if (value instanceof Map) return Object.fromEntries(value);
|
|
495
|
+
if (value instanceof Set) return [...value];
|
|
496
|
+
return value;
|
|
497
|
+
}, 2);
|
|
498
|
+
}`,
|
|
499
|
+
returnByValue: true,
|
|
500
|
+
});
|
|
501
|
+
const text = typeof json.value === 'string' ? json.value : result.description ?? result.type;
|
|
502
|
+
return text.length > 20_000 ? `${text.slice(0, 20_000)}…` : text;
|
|
503
|
+
}
|
|
504
|
+
catch {
|
|
505
|
+
// Circular, or a getter that throws: what the console would show.
|
|
506
|
+
return result.description ?? result.type;
|
|
507
|
+
}
|
|
508
|
+
finally {
|
|
509
|
+
await cdp.send('Runtime.releaseObject', { objectId: result.objectId }).catch(() => undefined);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
// ---------- The tab's scripts ----------
|
|
513
|
+
/**
|
|
514
|
+
* Records a script the tab runs. Re-recording the same script
|
|
515
|
+
* replaces it.
|
|
516
|
+
*/
|
|
517
|
+
async inject(prepared, options = {}) {
|
|
518
|
+
if (!prepared.id)
|
|
519
|
+
throw new Error(`Cannot record an invalid script: ${prepared.diagnostic}`);
|
|
520
|
+
const id = prepared.id;
|
|
521
|
+
if (this.injected.has(id))
|
|
522
|
+
await this.remove(id);
|
|
523
|
+
this.host.addScript(prepared, this);
|
|
524
|
+
this.injected.set(id, { script: prepared, at: Date.now(), into: options.immediately ? this.currentDocument : undefined });
|
|
525
|
+
}
|
|
526
|
+
/** The tab no longer runs a script, from its next navigation. */
|
|
527
|
+
async remove(scriptId) {
|
|
528
|
+
if (!this.injected.delete(scriptId))
|
|
529
|
+
return;
|
|
530
|
+
this.host.removeScript(scriptId, this);
|
|
531
|
+
}
|
|
532
|
+
injectedScripts() {
|
|
533
|
+
return [...this.injected.values()].map((e) => e.script);
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* The scripts the current document runs, and so hears from: recorded
|
|
537
|
+
* before it started, or run in it at once. One added later (a `load`
|
|
538
|
+
* into a tab a dialog kept from reloading) waits for the next load.
|
|
539
|
+
*/
|
|
540
|
+
scriptsInDocument() {
|
|
541
|
+
const start = this.documentFirstStart();
|
|
542
|
+
return [...this.injected.values()]
|
|
543
|
+
.filter((e) => e.at <= start || e.into === this.currentDocument)
|
|
544
|
+
.map((e) => e.script);
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* When the main frame first entered its current document. Unlike
|
|
548
|
+
* `documentStart`, the same after the back/forward cache restores it,
|
|
549
|
+
* when its scripts don't run again.
|
|
550
|
+
*/
|
|
551
|
+
documentFirstStart() {
|
|
552
|
+
return this.currentDocument.spans[0][0];
|
|
553
|
+
}
|
|
554
|
+
/** Stop watching, and drop the CDP session. */
|
|
555
|
+
async dispose() {
|
|
556
|
+
if (this.disposing)
|
|
557
|
+
return;
|
|
558
|
+
this.disposing = true;
|
|
559
|
+
this.page.off('request', this.onRequest);
|
|
560
|
+
this.page.off('requestfinished', this.onRequestEnd);
|
|
561
|
+
this.page.off('requestfailed', this.onRequestEnd);
|
|
562
|
+
this.requests.clear();
|
|
563
|
+
for (const id of [...this.injected.keys()])
|
|
564
|
+
await this.remove(id);
|
|
565
|
+
this.closed = true;
|
|
566
|
+
this.page.off('close', this.onClose);
|
|
567
|
+
this.host.detachPage(this);
|
|
568
|
+
attached.delete(this.page);
|
|
569
|
+
// A page stopped on a dialog answers nothing until it is closed, so
|
|
570
|
+
// this is not waited on for long; the session goes with the page.
|
|
571
|
+
const frames = this.frameSessions?.then((f) => f?.dispose());
|
|
572
|
+
await Promise.race([
|
|
573
|
+
Promise.all([this.cdp.detach().catch(() => undefined), frames?.catch(() => undefined)]),
|
|
574
|
+
new Promise((r) => setTimeout(r, 1000).unref()),
|
|
575
|
+
]);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
// A page's CDP target id, remembered: a page keeps its id for life.
|
|
579
|
+
const targetIds = new WeakMap();
|
|
580
|
+
/**
|
|
581
|
+
* The page's CDP target id (the `id` in `/json/list`, and what CDP's
|
|
582
|
+
* `Target.*` calls take), or null when it can't be read, as once the
|
|
583
|
+
* page has closed.
|
|
584
|
+
*/
|
|
585
|
+
export async function targetIdOf(page) {
|
|
586
|
+
const known = targetIds.get(page);
|
|
587
|
+
if (known)
|
|
588
|
+
return known;
|
|
589
|
+
try {
|
|
590
|
+
const session = await page.context().newCDPSession(page);
|
|
591
|
+
try {
|
|
592
|
+
const { targetInfo } = await session.send('Target.getTargetInfo');
|
|
593
|
+
targetIds.set(page, targetInfo.targetId);
|
|
594
|
+
return targetInfo.targetId;
|
|
595
|
+
}
|
|
596
|
+
finally {
|
|
597
|
+
await session.detach().catch(() => undefined);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
catch {
|
|
601
|
+
// Closed while being asked.
|
|
602
|
+
return null;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
export async function attachPage(page, host, options) {
|
|
606
|
+
return PageAttachment.create(page, host, options);
|
|
607
|
+
}
|