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,165 @@
|
|
|
1
|
+
import { type BrowserContext } from '@playwright/test';
|
|
2
|
+
import { ExtensionHost } from './extension-host.js';
|
|
3
|
+
import { ExtensionLink } from './extension-link.js';
|
|
4
|
+
import type { ConnectChoice, HostReports, SkipReport, ErrorReport, StartReport, RequestInterceptor } from './host.js';
|
|
5
|
+
import { type PageAttachment } from './page-watch.js';
|
|
6
|
+
import { type PreparedScript, type PrepareOptions } from './prepare.js';
|
|
7
|
+
export interface LoadOptions extends Omit<PrepareOptions, 'file' | 'text'> {
|
|
8
|
+
/** Seed the script's `GM_*` storage before it runs. */
|
|
9
|
+
values?: Record<string, unknown>;
|
|
10
|
+
/**
|
|
11
|
+
* Load the same file a second time as a separate copy: its own
|
|
12
|
+
* script id and its own storage, as two installed copies would be.
|
|
13
|
+
* Pass any name you can refer to it by afterwards.
|
|
14
|
+
*/
|
|
15
|
+
copy?: string;
|
|
16
|
+
/**
|
|
17
|
+
* Check the page URL against the script's header now and throw if it
|
|
18
|
+
* would not run there, rather than finding out from a silent page.
|
|
19
|
+
*/
|
|
20
|
+
url?: string;
|
|
21
|
+
}
|
|
22
|
+
export interface GmApi {
|
|
23
|
+
/** A stored value, decoded. */
|
|
24
|
+
get(key: string, script?: string): unknown;
|
|
25
|
+
/**
|
|
26
|
+
* Write a value as another tab of the script would: a script already
|
|
27
|
+
* running sees it on its next `GM_getValue` and its value-change
|
|
28
|
+
* listeners fire with `remote` set. Before the first navigation it is
|
|
29
|
+
* simply seeded.
|
|
30
|
+
*/
|
|
31
|
+
set(key: string, value: unknown, script?: string): Promise<void>;
|
|
32
|
+
list(script?: string): Record<string, unknown>;
|
|
33
|
+
/**
|
|
34
|
+
* The menu commands' labels. A manual script (`@run-at context-menu`)
|
|
35
|
+
* that can run on the page lists "Run this script" first.
|
|
36
|
+
*/
|
|
37
|
+
menuCommands(script?: string): string[];
|
|
38
|
+
/**
|
|
39
|
+
* Fire a menu command by its label. Resolves false if none matches.
|
|
40
|
+
* "Run this script" runs a manual script, as `runScript` does.
|
|
41
|
+
*/
|
|
42
|
+
fireMenu(label: string, script?: string): Promise<boolean>;
|
|
43
|
+
/**
|
|
44
|
+
* Run a manual script (`@run-at context-menu`) on the test's page, as
|
|
45
|
+
* its popup "Run this script" does. Each call is another run. Rejects
|
|
46
|
+
* with SourceMonkey's reason when it can't run there.
|
|
47
|
+
*/
|
|
48
|
+
runScript(script?: string): Promise<void>;
|
|
49
|
+
openedTabs(): HostReports['tabs'];
|
|
50
|
+
notifications(): HostReports['notifications'];
|
|
51
|
+
clickNotification(requestId: string): Promise<boolean>;
|
|
52
|
+
closeNotification(requestId: string): Promise<boolean>;
|
|
53
|
+
downloads(): HostReports['downloads'];
|
|
54
|
+
requests(): HostReports['requests'];
|
|
55
|
+
/** SourceMonkey's prompts to let a script connect to a host its `@connect` lines don't cover. */
|
|
56
|
+
connectPrompts(): HostReports['connectPrompts'];
|
|
57
|
+
/** Answer a waiting connection prompt. Resolves false if it isn't waiting. */
|
|
58
|
+
answerConnectPrompt(promptId: string, choice: ConnectChoice): Promise<boolean>;
|
|
59
|
+
/**
|
|
60
|
+
* Answer `GM_xmlhttpRequest`s from the test instead of the network:
|
|
61
|
+
* return an event (`{ kind: 'error', message }` or
|
|
62
|
+
* `{ kind: 'load', response }`) to use it, null to let the request
|
|
63
|
+
* go. `failHosts(...)` is the common case, a synthetic 503 for those
|
|
64
|
+
* hosts. Resolves once requests are being intercepted. The extension
|
|
65
|
+
* makes the request, so:
|
|
66
|
+
* - an `error`, `timeout` or `abort` answer reaches the script as a
|
|
67
|
+
* network failure, with the extension's own message;
|
|
68
|
+
* - a `load` answer's status, headers, `responseText` and `finalUrl`
|
|
69
|
+
* are what the script sees, and the rest is derived from them;
|
|
70
|
+
* - the interceptor's script id is set only when one script is loaded.
|
|
71
|
+
*/
|
|
72
|
+
interceptRequests(fn: RequestInterceptor | null): Promise<void>;
|
|
73
|
+
failHosts(...hosts: string[]): Promise<void>;
|
|
74
|
+
}
|
|
75
|
+
export interface ReportsApi {
|
|
76
|
+
started(): StartReport[];
|
|
77
|
+
skipped(): SkipReport[];
|
|
78
|
+
errors(): ErrorReport[];
|
|
79
|
+
/**
|
|
80
|
+
* Resolve once a script has reported that it started; `script`
|
|
81
|
+
* selects one as in `GmApi`. An earlier start in the same test
|
|
82
|
+
* counts, so after a second navigation clear the reports first or
|
|
83
|
+
* wait on the page instead.
|
|
84
|
+
*/
|
|
85
|
+
waitForStart(script?: string, timeoutMs?: number): Promise<StartReport>;
|
|
86
|
+
}
|
|
87
|
+
export interface HarnessFixtures {
|
|
88
|
+
host: ExtensionHost;
|
|
89
|
+
attachment: PageAttachment;
|
|
90
|
+
loadUserscript: (file: string, options?: LoadOptions) => Promise<PreparedScript>;
|
|
91
|
+
gm: GmApi;
|
|
92
|
+
reports: ReportsApi;
|
|
93
|
+
/**
|
|
94
|
+
* Serve a saved HTML file at `url` on this page, so `@match` and
|
|
95
|
+
* relative URLs behave as on the live site. Without `url`, the one
|
|
96
|
+
* the file's header records (`saveHtml` writes it). The URL is
|
|
97
|
+
* matched exactly, query string included, fragment ignored. Other
|
|
98
|
+
* requests fall through to the page's other routes and then the
|
|
99
|
+
* network, unless `block` is set, which aborts them. Resolves to the
|
|
100
|
+
* URL served, for the spec to go to.
|
|
101
|
+
*/
|
|
102
|
+
serveHtml: (file: string, url?: string, options?: {
|
|
103
|
+
block?: boolean;
|
|
104
|
+
}) => Promise<string>;
|
|
105
|
+
/**
|
|
106
|
+
* Write the page's current DOM to `file`, for `serveHtml` to
|
|
107
|
+
* replay later. The same file the `savehtml` run command writes: run a spec
|
|
108
|
+
* once against the live site to record it, then serve it and the
|
|
109
|
+
* spec needs no login.
|
|
110
|
+
*/
|
|
111
|
+
saveHtml: (file: string) => Promise<void>;
|
|
112
|
+
}
|
|
113
|
+
interface InternalFixtures {
|
|
114
|
+
_smLink: ExtensionLink;
|
|
115
|
+
_smLoaded: PreparedScript[];
|
|
116
|
+
_smRequests: RequestFakes;
|
|
117
|
+
}
|
|
118
|
+
export declare const test: import("@playwright/test").TestType<import("@playwright/test").PlaywrightTestArgs & import("@playwright/test").PlaywrightTestOptions & HarnessFixtures & InternalFixtures, import("@playwright/test").PlaywrightWorkerArgs & import("@playwright/test").PlaywrightWorkerOptions>;
|
|
119
|
+
export { expect } from '@playwright/test';
|
|
120
|
+
/**
|
|
121
|
+
* A test's answers to its scripts' `GM_xmlhttpRequest`s, which the
|
|
122
|
+
* extension's worker makes.
|
|
123
|
+
*
|
|
124
|
+
* - Paused with CDP's Fetch domain in the worker's own target, over a
|
|
125
|
+
* browser-level session (Playwright opens none on a worker). Not
|
|
126
|
+
* Playwright's `context.route`, which sees the worker's requests but
|
|
127
|
+
* not the hop after a redirect, which a response ending at another
|
|
128
|
+
* URL (`finalUrl`) needs.
|
|
129
|
+
* - Only while a test has set an interceptor; a worker that restarts
|
|
130
|
+
* is attached to again, and requests it makes before that go out.
|
|
131
|
+
* - Only the worker's `fetch`es, which `GM_xmlhttpRequest` makes and a
|
|
132
|
+
* download doesn't. The ones a push makes (a script's `@require`s)
|
|
133
|
+
* are let go, and the interceptor's null lets one go.
|
|
134
|
+
* - The worker doesn't say which tab or script a request is for. In a
|
|
135
|
+
* browser of the repo's own, other tabs' scripts' requests are seen
|
|
136
|
+
* too, and the interceptor gets a script id only when one script is
|
|
137
|
+
* loaded.
|
|
138
|
+
*/
|
|
139
|
+
declare class RequestFakes {
|
|
140
|
+
private readonly context;
|
|
141
|
+
private readonly workerUrl;
|
|
142
|
+
private readonly loaded;
|
|
143
|
+
private interceptor;
|
|
144
|
+
private session;
|
|
145
|
+
private attached;
|
|
146
|
+
private attaching;
|
|
147
|
+
private listening;
|
|
148
|
+
private pausedCount;
|
|
149
|
+
private nextId;
|
|
150
|
+
private readonly waiting;
|
|
151
|
+
private readonly redirected;
|
|
152
|
+
private readonly onWorker;
|
|
153
|
+
constructor(context: BrowserContext, workerUrl: string, loaded: () => readonly PreparedScript[]);
|
|
154
|
+
set(fn: RequestInterceptor | null): Promise<void>;
|
|
155
|
+
/** Runs `work` with every request let go. */
|
|
156
|
+
paused<T>(work: () => Promise<T>): Promise<T>;
|
|
157
|
+
stop(): Promise<void>;
|
|
158
|
+
private attach;
|
|
159
|
+
private attachNow;
|
|
160
|
+
private detached;
|
|
161
|
+
private send;
|
|
162
|
+
private onMessage;
|
|
163
|
+
private answer;
|
|
164
|
+
private fulfill;
|
|
165
|
+
}
|
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
// The Playwright fixture a repo of userscripts imports:
|
|
2
|
+
//
|
|
3
|
+
// import { test, expect } from 'sourcemonkey/harness';
|
|
4
|
+
//
|
|
5
|
+
// It extends `@playwright/test` so each test's scripts run in the real
|
|
6
|
+
// SourceMonkey extension, as sm-dev's runs do: `loadUserscript` pushes a
|
|
7
|
+
// script from disk into a test collection of the test's own, `gm` seeds
|
|
8
|
+
// and reads what the script did, `reports` has what it reported, and
|
|
9
|
+
// `serveHtml` runs it against a saved page.
|
|
10
|
+
//
|
|
11
|
+
// - The browser: by default one per worker process, launched with the
|
|
12
|
+
// package's built extension (extension-launch.ts), and reused by that
|
|
13
|
+
// worker's tests. A repo that drives a browser of its own (a logged-in
|
|
14
|
+
// one, over CDP) overrides `browser`, `context` and `page` as it
|
|
15
|
+
// always has; that browser needs SourceMonkey loaded, with "Allow
|
|
16
|
+
// User Scripts" on.
|
|
17
|
+
// - Per test: an `ExtensionLink` with its own run id, which claims the
|
|
18
|
+
// test's page so only the test's scripts run there, and an
|
|
19
|
+
// `ExtensionHost` filled from the extension's events. The test
|
|
20
|
+
// collection goes when the test ends.
|
|
21
|
+
//
|
|
22
|
+
// See docs/harness-guide.md for the user-facing description.
|
|
23
|
+
import { test as base, chromium } from '@playwright/test';
|
|
24
|
+
import { readFile } from 'node:fs/promises';
|
|
25
|
+
import path from 'node:path';
|
|
26
|
+
import { savePageHtml, userscriptLabel } from './save-html.js';
|
|
27
|
+
import { companionFiles, loadSavedPage, routeSavedPage, savedPageUrl } from './saved-pages.js';
|
|
28
|
+
import { ExtensionHost } from './extension-host.js';
|
|
29
|
+
import { launchExtensionBrowser } from './extension-launch.js';
|
|
30
|
+
import { ExtensionLink, linkRunId } from './extension-link.js';
|
|
31
|
+
import { RUN_SCRIPT_LABEL } from '../userscript-header.js';
|
|
32
|
+
import { attachPage } from './page-watch.js';
|
|
33
|
+
import { prepareScript, scriptRoots, pushedFiles, NO_RULE_COVERS_URL, explainMatch, preparedScriptMatches, } from './prepare.js';
|
|
34
|
+
import { visibleCommands } from '../menu-captions.js';
|
|
35
|
+
export const test = base.extend({
|
|
36
|
+
// A browser of the worker's own, with the extension. Over CDP, so
|
|
37
|
+
// `browser` is a Playwright `Browser` as specs expect.
|
|
38
|
+
browser: [async ({ headless }, use) => {
|
|
39
|
+
const launched = await launchExtensionBrowser({ headed: !headless, connectedElsewhere: true });
|
|
40
|
+
try {
|
|
41
|
+
const browser = await chromium.connectOverCDP(launched.endpoint);
|
|
42
|
+
await use(browser);
|
|
43
|
+
await browser.close().catch(() => undefined);
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
await launched.close();
|
|
47
|
+
}
|
|
48
|
+
}, { scope: 'worker' }],
|
|
49
|
+
// The browser's own context, where the extension is: one for the
|
|
50
|
+
// worker's tests, whose pages each test opens and closes.
|
|
51
|
+
context: async ({ browser }, use) => {
|
|
52
|
+
await use(browser.contexts()[0]);
|
|
53
|
+
},
|
|
54
|
+
// A page of the test's own, closed at its end with the tabs it opened,
|
|
55
|
+
// since the context outlives the test.
|
|
56
|
+
page: async ({ context }, use) => {
|
|
57
|
+
const before = new Set(context.pages());
|
|
58
|
+
const page = await context.newPage();
|
|
59
|
+
await use(page);
|
|
60
|
+
for (const p of context.pages())
|
|
61
|
+
if (!before.has(p))
|
|
62
|
+
await p.close().catch(() => undefined);
|
|
63
|
+
},
|
|
64
|
+
_smLink: async ({ context }, use) => {
|
|
65
|
+
const link = await ExtensionLink.open(context, linkRunId('test'));
|
|
66
|
+
await use(link);
|
|
67
|
+
await link.close();
|
|
68
|
+
},
|
|
69
|
+
_smLoaded: async ({}, use) => {
|
|
70
|
+
await use([]);
|
|
71
|
+
},
|
|
72
|
+
_smRequests: async ({ context, _smLink, _smLoaded }, use) => {
|
|
73
|
+
const fakes = new RequestFakes(context, _smLink.workerUrl, () => _smLoaded);
|
|
74
|
+
await use(fakes);
|
|
75
|
+
await fakes.stop();
|
|
76
|
+
},
|
|
77
|
+
host: async ({ _smLink }, use) => {
|
|
78
|
+
// Log entries other than reports, which `GM_log` adds to.
|
|
79
|
+
const host = new ExtensionHost(_smLink, () => undefined);
|
|
80
|
+
await use(host);
|
|
81
|
+
},
|
|
82
|
+
// The test's page, claimed for the test so only its scripts run
|
|
83
|
+
// there, and watched for what they report. Set up with every test, so
|
|
84
|
+
// a page a test serves without loading a script runs none of the
|
|
85
|
+
// extension's either.
|
|
86
|
+
attachment: [async ({ page, host, _smLink }, use) => {
|
|
87
|
+
const tabId = await _smLink.claimPage(page);
|
|
88
|
+
const attachment = await attachPage(page, host, { watchOnly: { extensionOrigin: _smLink.origin } });
|
|
89
|
+
host.addTab(attachment, tabId);
|
|
90
|
+
await use(attachment);
|
|
91
|
+
await attachment.dispose();
|
|
92
|
+
}, { auto: true }],
|
|
93
|
+
loadUserscript: async ({ attachment, host, _smLink, _smLoaded, _smRequests }, use, testInfo) => {
|
|
94
|
+
await use(async (file, options = {}) => {
|
|
95
|
+
const { values, url, ...prepareOptions } = options;
|
|
96
|
+
const abs = path.isAbsolute(file) ? file : path.resolve(testInfo.config.rootDir, file);
|
|
97
|
+
const { collectionRoot, localRoots } = scriptRoots(abs, prepareOptions.collectionRoot ?? testInfo.config.rootDir, prepareOptions.localRoots);
|
|
98
|
+
const prepared = await prepareScript({ ...prepareOptions, file: abs, collectionRoot, localRoots });
|
|
99
|
+
if (prepared.status !== 'ok' || !prepared.id) {
|
|
100
|
+
throw new Error(`This file cannot be loaded: ${prepared.diagnostic}`);
|
|
101
|
+
}
|
|
102
|
+
if (url) {
|
|
103
|
+
const verdict = explainMatch(prepared.matching, url);
|
|
104
|
+
if (!verdict.matches) {
|
|
105
|
+
throw new Error(`${path.basename(file)} would not run on ${url}: ${verdict.rule ?? NO_RULE_COVERS_URL}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// The extension keeps the test's scripts by this name: the path
|
|
109
|
+
// under the collection root, with the copy's name for a copy.
|
|
110
|
+
const relative = path.relative(collectionRoot, abs).split(path.sep).join('/');
|
|
111
|
+
const name = prepareOptions.copy ? relative.replace(/(\.user\.js)?$/, `-copy-${prepareOptions.copy}$1`) : relative;
|
|
112
|
+
const text = await readFile(abs, 'utf8');
|
|
113
|
+
// Its `@require`s are fetched as it installs, which a test's
|
|
114
|
+
// request fakes are not for.
|
|
115
|
+
const result = await _smRequests.paused(() => _smLink.push(text, name, pushedFiles(prepared)));
|
|
116
|
+
if (result.status !== 'ok')
|
|
117
|
+
throw new Error(`This file cannot be loaded: ${result.errorMessage ?? 'SourceMonkey refused it'}`);
|
|
118
|
+
host.setPushed(prepared.id, result.scriptId);
|
|
119
|
+
host.setLayout(result.scriptId, await _smLink.stackLayout(result.scriptId).catch(() => null));
|
|
120
|
+
await attachment.inject(prepared);
|
|
121
|
+
_smLoaded.push(prepared);
|
|
122
|
+
if (values)
|
|
123
|
+
await host.setValues(prepared.id, values);
|
|
124
|
+
return prepared;
|
|
125
|
+
});
|
|
126
|
+
},
|
|
127
|
+
gm: async ({ host, attachment, _smLoaded, _smRequests }, use) => {
|
|
128
|
+
const idOf = (script) => {
|
|
129
|
+
if (_smLoaded.length === 0)
|
|
130
|
+
throw new Error('No userscript is loaded');
|
|
131
|
+
if (script === undefined) {
|
|
132
|
+
if (_smLoaded.length > 1)
|
|
133
|
+
throw new Error('Multiple userscripts are loaded; name one');
|
|
134
|
+
return _smLoaded[0].id;
|
|
135
|
+
}
|
|
136
|
+
const found = _smLoaded.find((p) => preparedScriptMatches(p, script));
|
|
137
|
+
if (!found)
|
|
138
|
+
throw new Error(`No loaded userscript is named ${script}`);
|
|
139
|
+
return found.id;
|
|
140
|
+
};
|
|
141
|
+
await use({
|
|
142
|
+
get: (key, script) => host.getValue(idOf(script), key),
|
|
143
|
+
set: (key, value, script) => host.setValues(idOf(script), { [key]: value }),
|
|
144
|
+
list: (script) => host.getValues(idOf(script)),
|
|
145
|
+
menuCommands: (script) => {
|
|
146
|
+
const id = script === undefined ? undefined : idOf(script);
|
|
147
|
+
const runnable = host.manualScripts(attachment).filter((m) => id === undefined || m === id);
|
|
148
|
+
return [
|
|
149
|
+
...runnable.map(() => RUN_SCRIPT_LABEL),
|
|
150
|
+
// As the popup lists them: a repeated caption once.
|
|
151
|
+
...visibleCommands(host.reports.menuCommands.filter((m) => id === undefined || m.scriptId === id), (m) => `${m.scriptId}\0${m.name}`).map((m) => m.name),
|
|
152
|
+
];
|
|
153
|
+
},
|
|
154
|
+
fireMenu: async (label, script) => {
|
|
155
|
+
const id = script === undefined ? undefined : idOf(script);
|
|
156
|
+
if (label === RUN_SCRIPT_LABEL) {
|
|
157
|
+
const runnable = host.manualScripts(attachment).filter((m) => id === undefined || m === id);
|
|
158
|
+
if (runnable.length === 1) {
|
|
159
|
+
await host.runManualScript(attachment, runnable[0]);
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return host.fireMenuCommand(label, id);
|
|
164
|
+
},
|
|
165
|
+
runScript: (script) => host.runManualScript(attachment, idOf(script)),
|
|
166
|
+
openedTabs: () => host.reports.tabs,
|
|
167
|
+
notifications: () => host.reports.notifications,
|
|
168
|
+
clickNotification: (requestId) => host.clickNotification(requestId),
|
|
169
|
+
closeNotification: (requestId) => host.closeNotification(requestId),
|
|
170
|
+
downloads: () => host.reports.downloads,
|
|
171
|
+
requests: () => host.reports.requests,
|
|
172
|
+
connectPrompts: () => host.reports.connectPrompts,
|
|
173
|
+
answerConnectPrompt: (promptId, choice) => host.answerConnectPrompt(promptId, choice),
|
|
174
|
+
interceptRequests: (fn) => _smRequests.set(fn),
|
|
175
|
+
failHosts: (...hosts) => {
|
|
176
|
+
const set = new Set(hosts);
|
|
177
|
+
return _smRequests.set((request) => {
|
|
178
|
+
let hostname = '';
|
|
179
|
+
try {
|
|
180
|
+
hostname = new URL(request.url).hostname;
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
if (!set.has(hostname))
|
|
186
|
+
return null;
|
|
187
|
+
return {
|
|
188
|
+
kind: 'load',
|
|
189
|
+
response: {
|
|
190
|
+
status: 503, statusText: 'Service Unavailable', responseURL: request.url,
|
|
191
|
+
responseText: '', response: '', responseHeaders: '', finalUrl: request.url,
|
|
192
|
+
},
|
|
193
|
+
};
|
|
194
|
+
});
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
},
|
|
198
|
+
reports: async ({ host, _smLoaded }, use) => {
|
|
199
|
+
await use({
|
|
200
|
+
started: () => host.reports.started,
|
|
201
|
+
skipped: () => host.reports.skipped,
|
|
202
|
+
errors: () => host.reports.errors,
|
|
203
|
+
waitForStart: (script, timeoutMs = 10000) => new Promise((resolve, reject) => {
|
|
204
|
+
// The same selection `gm` uses, via the loaded script's id.
|
|
205
|
+
const wanted = script === undefined
|
|
206
|
+
? undefined
|
|
207
|
+
: _smLoaded.find((p) => preparedScriptMatches(p, script))?.id ?? script;
|
|
208
|
+
const matches = (r) => wanted === undefined || r.scriptId === wanted;
|
|
209
|
+
const already = host.reports.started.find(matches);
|
|
210
|
+
if (already) {
|
|
211
|
+
resolve(already);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
const timer = setTimeout(() => {
|
|
215
|
+
host.off('start', onStart);
|
|
216
|
+
const skipped = host.reports.skipped.map((s) => `${s.name} on ${s.url}: ${s.reason}`).join('; ');
|
|
217
|
+
reject(new Error(skipped
|
|
218
|
+
? `No userscript started within ${timeoutMs} ms. Skipped: ${skipped.replace(/\.$/, '')}.`
|
|
219
|
+
: `No userscript started within ${timeoutMs} ms`));
|
|
220
|
+
}, timeoutMs);
|
|
221
|
+
const onStart = (r) => {
|
|
222
|
+
if (!matches(r))
|
|
223
|
+
return;
|
|
224
|
+
clearTimeout(timer);
|
|
225
|
+
host.off('start', onStart);
|
|
226
|
+
resolve(r);
|
|
227
|
+
};
|
|
228
|
+
host.on('start', onStart);
|
|
229
|
+
}),
|
|
230
|
+
});
|
|
231
|
+
},
|
|
232
|
+
saveHtml: async ({ page, attachment, host, _smLoaded }, use) => {
|
|
233
|
+
await use(async (file) => {
|
|
234
|
+
const running = _smLoaded.filter((s) => s.id && host.isRunning(attachment, s.id));
|
|
235
|
+
await savePageHtml(page, file, running.map(userscriptLabel));
|
|
236
|
+
});
|
|
237
|
+
},
|
|
238
|
+
serveHtml: async ({ page }, use) => {
|
|
239
|
+
await use(async (file, url, options = {}) => {
|
|
240
|
+
const { html, header } = await loadSavedPage(file);
|
|
241
|
+
const found = savedPageUrl(file, header, url, 'Pass one to serveHtml.');
|
|
242
|
+
if ('problem' in found)
|
|
243
|
+
throw new Error(found.problem);
|
|
244
|
+
const at = found.url;
|
|
245
|
+
await routeSavedPage(page, () => ({
|
|
246
|
+
url: at,
|
|
247
|
+
html: async () => html,
|
|
248
|
+
block: options.block ? () => true : undefined,
|
|
249
|
+
name: file,
|
|
250
|
+
companion: companionFiles(file, at),
|
|
251
|
+
}));
|
|
252
|
+
return at;
|
|
253
|
+
});
|
|
254
|
+
},
|
|
255
|
+
});
|
|
256
|
+
export { expect } from '@playwright/test';
|
|
257
|
+
/**
|
|
258
|
+
* A test's answers to its scripts' `GM_xmlhttpRequest`s, which the
|
|
259
|
+
* extension's worker makes.
|
|
260
|
+
*
|
|
261
|
+
* - Paused with CDP's Fetch domain in the worker's own target, over a
|
|
262
|
+
* browser-level session (Playwright opens none on a worker). Not
|
|
263
|
+
* Playwright's `context.route`, which sees the worker's requests but
|
|
264
|
+
* not the hop after a redirect, which a response ending at another
|
|
265
|
+
* URL (`finalUrl`) needs.
|
|
266
|
+
* - Only while a test has set an interceptor; a worker that restarts
|
|
267
|
+
* is attached to again, and requests it makes before that go out.
|
|
268
|
+
* - Only the worker's `fetch`es, which `GM_xmlhttpRequest` makes and a
|
|
269
|
+
* download doesn't. The ones a push makes (a script's `@require`s)
|
|
270
|
+
* are let go, and the interceptor's null lets one go.
|
|
271
|
+
* - The worker doesn't say which tab or script a request is for. In a
|
|
272
|
+
* browser of the repo's own, other tabs' scripts' requests are seen
|
|
273
|
+
* too, and the interceptor gets a script id only when one script is
|
|
274
|
+
* loaded.
|
|
275
|
+
*/
|
|
276
|
+
class RequestFakes {
|
|
277
|
+
context;
|
|
278
|
+
workerUrl;
|
|
279
|
+
loaded;
|
|
280
|
+
interceptor = null;
|
|
281
|
+
session = null;
|
|
282
|
+
// The worker target attached to, and its session id there.
|
|
283
|
+
attached = null;
|
|
284
|
+
attaching = null;
|
|
285
|
+
listening = false;
|
|
286
|
+
pausedCount = 0;
|
|
287
|
+
nextId = 0;
|
|
288
|
+
waiting = new Map();
|
|
289
|
+
// Bodies for the URL a faked redirect leads to, served as the worker
|
|
290
|
+
// follows it.
|
|
291
|
+
redirected = new Map();
|
|
292
|
+
onWorker = (worker) => {
|
|
293
|
+
if (worker.url() === this.workerUrl && this.interceptor)
|
|
294
|
+
void this.attach().catch(() => undefined);
|
|
295
|
+
};
|
|
296
|
+
constructor(context, workerUrl, loaded) {
|
|
297
|
+
this.context = context;
|
|
298
|
+
this.workerUrl = workerUrl;
|
|
299
|
+
this.loaded = loaded;
|
|
300
|
+
}
|
|
301
|
+
set(fn) {
|
|
302
|
+
this.interceptor = fn;
|
|
303
|
+
if (!fn)
|
|
304
|
+
return Promise.resolve();
|
|
305
|
+
if (!this.listening) {
|
|
306
|
+
this.listening = true;
|
|
307
|
+
this.context.on('serviceworker', this.onWorker);
|
|
308
|
+
}
|
|
309
|
+
return this.attach();
|
|
310
|
+
}
|
|
311
|
+
/** Runs `work` with every request let go. */
|
|
312
|
+
async paused(work) {
|
|
313
|
+
this.pausedCount++;
|
|
314
|
+
try {
|
|
315
|
+
return await work();
|
|
316
|
+
}
|
|
317
|
+
finally {
|
|
318
|
+
this.pausedCount--;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
async stop() {
|
|
322
|
+
this.interceptor = null;
|
|
323
|
+
this.listening = false;
|
|
324
|
+
this.context.off('serviceworker', this.onWorker);
|
|
325
|
+
this.redirected.clear();
|
|
326
|
+
const session = this.session;
|
|
327
|
+
this.session = null;
|
|
328
|
+
this.detached();
|
|
329
|
+
await session?.detach().catch(() => undefined);
|
|
330
|
+
}
|
|
331
|
+
// Attaches to the worker running now, unless already attached to it.
|
|
332
|
+
attach() {
|
|
333
|
+
this.attaching ??= this.attachNow().finally(() => { this.attaching = null; });
|
|
334
|
+
return this.attaching;
|
|
335
|
+
}
|
|
336
|
+
async attachNow() {
|
|
337
|
+
const browser = this.context.browser();
|
|
338
|
+
if (!browser) {
|
|
339
|
+
throw new Error('Requests can\'t be intercepted in a launchPersistentContext context. Connect to the browser (chromium.connectOverCDP) and use its context instead.');
|
|
340
|
+
}
|
|
341
|
+
if (!this.session) {
|
|
342
|
+
this.session = await browser.newBrowserCDPSession();
|
|
343
|
+
this.session.on('Target.receivedMessageFromTarget', (ev) => this.onMessage(ev.sessionId, ev.message));
|
|
344
|
+
// The worker stopped: its calls in flight get no answer.
|
|
345
|
+
this.session.on('Target.detachedFromTarget', (ev) => {
|
|
346
|
+
if (ev.sessionId === this.attached?.sessionId)
|
|
347
|
+
this.detached();
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
const { targetInfos } = await this.session.send('Target.getTargets');
|
|
351
|
+
const target = targetInfos.find((t) => t.type === 'service_worker' && t.url === this.workerUrl);
|
|
352
|
+
if (!target)
|
|
353
|
+
throw new Error('Internal error: SourceMonkey\'s background script is not running, so requests can\'t be intercepted');
|
|
354
|
+
if (this.attached?.targetId === target.targetId)
|
|
355
|
+
return;
|
|
356
|
+
// Not flattened: a flat session's messages need a session id
|
|
357
|
+
// Playwright's CDP session can't send with.
|
|
358
|
+
const { sessionId } = await this.session.send('Target.attachToTarget', { targetId: target.targetId, flatten: false });
|
|
359
|
+
this.attached = { targetId: target.targetId, sessionId };
|
|
360
|
+
try {
|
|
361
|
+
await this.send('Fetch.enable', { patterns: [{ urlPattern: '*', resourceType: 'Fetch' }] });
|
|
362
|
+
}
|
|
363
|
+
catch (err) {
|
|
364
|
+
// Not attached after all, so the next `set` tries again.
|
|
365
|
+
this.detached();
|
|
366
|
+
throw err;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
// No worker attached: the calls waiting on one are answered as failed.
|
|
370
|
+
detached() {
|
|
371
|
+
this.attached = null;
|
|
372
|
+
for (const answer of this.waiting.values())
|
|
373
|
+
answer({ error: { message: 'The background script stopped' } });
|
|
374
|
+
this.waiting.clear();
|
|
375
|
+
}
|
|
376
|
+
// A call to the worker's target, over the attached session.
|
|
377
|
+
async send(method, params) {
|
|
378
|
+
const session = this.session;
|
|
379
|
+
const attached = this.attached;
|
|
380
|
+
if (!session || !attached)
|
|
381
|
+
return;
|
|
382
|
+
const id = ++this.nextId;
|
|
383
|
+
const reply = new Promise((resolve) => this.waiting.set(id, resolve));
|
|
384
|
+
await session.send('Target.sendMessageToTarget', { sessionId: attached.sessionId, message: JSON.stringify({ id, method, params }) });
|
|
385
|
+
const { error } = await reply;
|
|
386
|
+
if (error)
|
|
387
|
+
throw new Error(`Internal error: Intercepting the background script's requests failed (${method}): ${error.message}`);
|
|
388
|
+
}
|
|
389
|
+
onMessage(sessionId, text) {
|
|
390
|
+
if (sessionId !== this.attached?.sessionId)
|
|
391
|
+
return;
|
|
392
|
+
let message;
|
|
393
|
+
try {
|
|
394
|
+
message = JSON.parse(text);
|
|
395
|
+
}
|
|
396
|
+
catch {
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
if (message.id !== undefined) {
|
|
400
|
+
this.waiting.get(message.id)?.(message);
|
|
401
|
+
this.waiting.delete(message.id);
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
if (message.method === 'Fetch.requestPaused' && message.params) {
|
|
405
|
+
void this.answer(message.params).catch(() => this.send('Fetch.continueRequest', { requestId: message.params.requestId }).catch(() => undefined));
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
async answer({ requestId, request }) {
|
|
409
|
+
const fn = this.interceptor;
|
|
410
|
+
const pending = this.redirected.get(request.url);
|
|
411
|
+
if (pending) {
|
|
412
|
+
this.redirected.delete(request.url);
|
|
413
|
+
await this.fulfill(requestId, pending);
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
if (!fn || this.pausedCount > 0) {
|
|
417
|
+
await this.send('Fetch.continueRequest', { requestId });
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
const payload = {
|
|
421
|
+
url: request.url, method: request.method, headers: request.headers, ...(request.postData !== undefined ? { data: request.postData } : {}),
|
|
422
|
+
};
|
|
423
|
+
// The worker doesn't say which script asked; with one loaded, it did.
|
|
424
|
+
const loaded = this.loaded();
|
|
425
|
+
const event = await fn(payload, loaded.length === 1 ? loaded[0].id : '');
|
|
426
|
+
if (!event) {
|
|
427
|
+
await this.send('Fetch.continueRequest', { requestId });
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
if (event.kind === 'load') {
|
|
431
|
+
const finalUrl = event.response.finalUrl;
|
|
432
|
+
// A redirect to the final URL, whose answer is the response, so
|
|
433
|
+
// the script sees the request end where the test said.
|
|
434
|
+
if (finalUrl && finalUrl !== request.url) {
|
|
435
|
+
this.redirected.set(finalUrl, event.response);
|
|
436
|
+
await this.send('Fetch.fulfillRequest', { requestId, responseCode: 302, responseHeaders: [{ name: 'location', value: finalUrl }] });
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
await this.fulfill(requestId, event.response);
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
await this.send('Fetch.failRequest', {
|
|
443
|
+
requestId, errorReason: event.kind === 'timeout' ? 'TimedOut' : event.kind === 'abort' ? 'Aborted' : 'Failed',
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
async fulfill(requestId, response) {
|
|
447
|
+
const responseHeaders = [];
|
|
448
|
+
for (const line of response.responseHeaders.split(/\r?\n/)) {
|
|
449
|
+
const colon = line.indexOf(':');
|
|
450
|
+
if (colon > 0)
|
|
451
|
+
responseHeaders.push({ name: line.slice(0, colon).trim(), value: line.slice(colon + 1).trim() });
|
|
452
|
+
}
|
|
453
|
+
await this.send('Fetch.fulfillRequest', {
|
|
454
|
+
requestId,
|
|
455
|
+
responseCode: response.status,
|
|
456
|
+
...(response.statusText ? { responsePhrase: response.statusText } : {}),
|
|
457
|
+
responseHeaders,
|
|
458
|
+
body: Buffer.from(response.responseText).toString('base64'),
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
}
|