@itmetsam/samurai-framework 0.1.0 → 0.2.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/README.md +129 -0
- package/bin/samurai.mjs +31 -0
- package/dist/api.js +3 -0
- package/dist/assert/assertion-error.d.ts +23 -0
- package/dist/assert/assertion-error.js +45 -0
- package/dist/assert/expect.d.ts +55 -0
- package/dist/assert/expect.js +188 -0
- package/dist/browser/browser-context.d.ts +91 -0
- package/dist/browser/browser-context.js +146 -0
- package/dist/browser/browser-finder.d.ts +2 -0
- package/dist/browser/browser-finder.js +95 -0
- package/dist/browser/browser.d.ts +95 -0
- package/dist/browser/browser.js +372 -0
- package/dist/browser/context-tree.d.ts +23 -0
- package/dist/browser/context-tree.js +60 -0
- package/dist/browser/dialog.d.ts +37 -0
- package/dist/browser/dialog.js +61 -0
- package/dist/browser/download.d.ts +71 -0
- package/dist/browser/download.js +137 -0
- package/dist/browser/downloads-dir.d.ts +8 -0
- package/dist/browser/downloads-dir.js +38 -0
- package/dist/browser/emulation.d.ts +61 -0
- package/dist/browser/emulation.js +144 -0
- package/dist/browser/file-chooser.d.ts +39 -0
- package/dist/browser/file-chooser.js +86 -0
- package/dist/browser/file-paths.d.ts +4 -0
- package/dist/browser/file-paths.js +18 -0
- package/dist/browser/index.d.ts +4 -0
- package/dist/browser/index.js +4 -0
- package/dist/browser/navigation-error.d.ts +13 -0
- package/dist/browser/navigation-error.js +19 -0
- package/dist/browser/page-logs.d.ts +86 -0
- package/dist/browser/page-logs.js +149 -0
- package/dist/browser/page-wait.d.ts +9 -0
- package/dist/browser/page-wait.js +17 -0
- package/dist/browser/page.d.ts +370 -0
- package/dist/browser/page.js +1038 -0
- package/dist/browser/pdf.d.ts +72 -0
- package/dist/browser/pdf.js +37 -0
- package/dist/browser/screenshot.d.ts +29 -0
- package/dist/browser/screenshot.js +42 -0
- package/dist/cli/args.d.ts +35 -0
- package/dist/cli/args.js +69 -0
- package/dist/cli/cli.cli-test.d.ts +1 -0
- package/dist/cli/cli.cli-test.js +170 -0
- package/dist/cli/console-reporter.d.ts +15 -0
- package/dist/cli/console-reporter.js +52 -0
- package/dist/cli/init.d.ts +7 -0
- package/dist/cli/init.js +61 -0
- package/dist/cli/main.d.ts +12 -0
- package/dist/cli/main.js +156 -0
- package/dist/config/config.d.ts +24 -0
- package/dist/config/config.js +42 -0
- package/dist/config/default.d.ts +2 -0
- package/dist/config/default.js +3 -0
- package/dist/config/mask.d.ts +11 -0
- package/dist/config/mask.js +85 -0
- package/dist/config/run-settings.d.ts +28 -0
- package/dist/config/run-settings.js +61 -0
- package/dist/config/secrets.d.ts +10 -0
- package/dist/config/secrets.js +67 -0
- package/dist/config/variables.d.ts +11 -0
- package/dist/config/variables.js +26 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +15 -0
- package/dist/locator/action-timeout-error.d.ts +25 -0
- package/dist/locator/action-timeout-error.js +35 -0
- package/dist/locator/element-state.d.ts +44 -0
- package/dist/locator/element-state.js +109 -0
- package/dist/locator/label-locator.d.ts +13 -0
- package/dist/locator/label-locator.js +49 -0
- package/dist/locator/locator.d.ts +168 -0
- package/dist/locator/locator.js +591 -0
- package/dist/locator/selector-errors.d.ts +9 -0
- package/dist/locator/selector-errors.js +15 -0
- package/dist/locator/selector.d.ts +60 -0
- package/dist/locator/selector.js +113 -0
- package/dist/locator/text-locator.d.ts +13 -0
- package/dist/logger/index.d.ts +2 -0
- package/dist/logger/index.js +30 -0
- package/dist/network/data-collector.d.ts +26 -0
- package/dist/network/data-collector.js +77 -0
- package/dist/network/network-tracker.d.ts +94 -0
- package/dist/network/network-tracker.js +198 -0
- package/dist/network/network-wait.d.ts +16 -0
- package/dist/network/network-wait.js +24 -0
- package/dist/network/response.d.ts +22 -0
- package/dist/network/response.js +42 -0
- package/dist/network/router.d.ts +120 -0
- package/dist/network/router.js +343 -0
- package/dist/network/url-match.d.ts +8 -0
- package/dist/network/url-match.js +38 -0
- package/dist/recorder/apply.d.ts +3 -0
- package/dist/recorder/apply.js +10 -0
- package/dist/recorder/capture.d.ts +18 -0
- package/dist/recorder/capture.js +198 -0
- package/dist/recorder/index.d.ts +3 -0
- package/dist/recorder/index.js +3 -0
- package/dist/recorder/normalise.d.ts +68 -0
- package/dist/recorder/normalise.js +104 -0
- package/dist/recorder/ranking.d.ts +9 -0
- package/dist/recorder/ranking.js +21 -0
- package/dist/recorder/recorder.d.ts +52 -0
- package/dist/recorder/recorder.js +143 -0
- package/dist/recorder/session.d.ts +49 -0
- package/dist/recorder/session.js +113 -0
- package/dist/runner/module-type.d.ts +6 -0
- package/dist/runner/module-type.js +29 -0
- package/dist/runner/page-logs-report.d.ts +40 -0
- package/dist/runner/page-logs-report.js +44 -0
- package/dist/runner/prepare-run.d.ts +14 -0
- package/dist/runner/prepare-run.js +20 -0
- package/dist/runner/project.d.ts +17 -0
- package/dist/runner/project.js +33 -0
- package/dist/runner/registry.d.ts +7 -0
- package/dist/runner/registry.js +40 -0
- package/dist/runner/reporter.d.ts +43 -0
- package/dist/runner/reporter.js +83 -0
- package/dist/runner/run.d.ts +27 -0
- package/dist/runner/run.js +34 -0
- package/dist/runner/test-error.d.ts +3 -0
- package/dist/runner/test-error.js +18 -0
- package/dist/runner/test-runner.d.ts +44 -0
- package/dist/runner/test-runner.js +205 -0
- package/dist/script/call-function.d.ts +34 -0
- package/dist/script/call-function.js +71 -0
- package/dist/script/element-handle.d.ts +10 -0
- package/{src/script/element-handle.ts → dist/script/element-handle.js} +4 -4
- package/dist/script/helpers.d.ts +28 -0
- package/dist/script/helpers.js +50 -0
- package/dist/script/preload.d.ts +31 -0
- package/dist/script/preload.js +71 -0
- package/dist/script/serialize.d.ts +29 -0
- package/dist/script/serialize.js +207 -0
- package/dist/steps/build.d.ts +5 -0
- package/dist/steps/build.js +38 -0
- package/dist/steps/edit.d.ts +30 -0
- package/dist/steps/edit.js +80 -0
- package/dist/steps/emit.d.ts +6 -0
- package/dist/steps/emit.js +73 -0
- package/dist/steps/index.d.ts +5 -0
- package/dist/steps/index.js +4 -0
- package/dist/steps/model.d.ts +103 -0
- package/dist/steps/model.js +1 -0
- package/dist/steps/parse.d.ts +3 -0
- package/dist/steps/parse.js +272 -0
- package/dist/transport/bidi-connection.d.ts +46 -0
- package/dist/transport/bidi-connection.js +176 -0
- package/dist/transport/bidi-error.d.ts +10 -0
- package/dist/transport/bidi-error.js +10 -0
- package/dist/transport/subscriptions.d.ts +36 -0
- package/dist/transport/subscriptions.js +81 -0
- package/{src → dist}/types/test.d.ts +10 -4
- package/dist/wait/wait-until.d.ts +20 -0
- package/dist/wait/wait-until.js +63 -0
- package/package.json +44 -7
- package/.claude/settings.local.json +0 -7
- package/.env.example +0 -3
- package/.github/actions/setup/action.yml +0 -16
- package/.github/workflows/ci.yml +0 -71
- package/.trunk/configs/.markdownlint.yaml +0 -2
- package/.trunk/configs/.yamllint.yaml +0 -7
- package/.trunk/configs/svgo.config.mjs +0 -14
- package/.trunk/trunk.yaml +0 -34
- package/docs/superpowers/bidi-roadmap.md +0 -60
- package/docs/superpowers/plans/2026-09-29-auto-wait.md +0 -1827
- package/docs/superpowers/plans/2026-09-29-expect.md +0 -1350
- package/docs/superpowers/plans/2026-10-01-ci.md +0 -350
- package/docs/superpowers/plans/2026-10-01-test-api-alignment.md +0 -1821
- package/docs/superpowers/recorder-roadmap.md +0 -52
- package/docs/superpowers/specs/2026-09-29-auto-wait-design.md +0 -185
- package/docs/superpowers/specs/2026-09-29-bidi-foundation-design.md +0 -130
- package/docs/superpowers/specs/2026-09-29-contexts-and-storage-design.md +0 -88
- package/docs/superpowers/specs/2026-09-29-downloads-and-file-dialogs-design.md +0 -94
- package/docs/superpowers/specs/2026-09-29-emulation-design.md +0 -88
- package/docs/superpowers/specs/2026-09-29-expect-design.md +0 -130
- package/docs/superpowers/specs/2026-09-29-locate-nodes-design.md +0 -90
- package/docs/superpowers/specs/2026-09-29-logs-and-errors-design.md +0 -87
- package/docs/superpowers/specs/2026-09-29-navigation-waits-design.md +0 -83
- package/docs/superpowers/specs/2026-09-29-network-data-design.md +0 -78
- package/docs/superpowers/specs/2026-09-29-network-interception-design.md +0 -91
- package/docs/superpowers/specs/2026-09-29-network-tracking-design.md +0 -86
- package/docs/superpowers/specs/2026-09-29-page-api-design.md +0 -90
- package/docs/superpowers/specs/2026-09-29-preload-scripts-design.md +0 -82
- package/docs/superpowers/specs/2026-09-29-script-call-function-design.md +0 -107
- package/docs/superpowers/specs/2026-09-29-set-files-design.md +0 -62
- package/docs/superpowers/specs/2026-09-29-user-prompts-design.md +0 -75
- package/docs/superpowers/specs/2026-10-01-ci-design.md +0 -106
- package/docs/superpowers/specs/2026-10-01-test-api-alignment-design.md +0 -241
- package/public/logo-dark.svg +0 -236
- package/public/logo-light.svg +0 -262
- package/samurai.config.ts +0 -7
- package/src/assert/assertion-error.test.ts +0 -55
- package/src/assert/assertion-error.ts +0 -62
- package/src/assert/expect-locator.test.ts +0 -217
- package/src/assert/expect.test.ts +0 -73
- package/src/assert/expect.ts +0 -299
- package/src/browser/browser-context.browser-test.ts +0 -201
- package/src/browser/browser-context.test.ts +0 -407
- package/src/browser/browser-context.ts +0 -208
- package/src/browser/browser-finder.ts +0 -110
- package/src/browser/browser.test.ts +0 -88
- package/src/browser/browser.ts +0 -515
- package/src/browser/context-tree.test.ts +0 -86
- package/src/browser/context-tree.ts +0 -70
- package/src/browser/dialog.browser-test.ts +0 -144
- package/src/browser/dialog.test.ts +0 -322
- package/src/browser/dialog.ts +0 -83
- package/src/browser/download.test.ts +0 -161
- package/src/browser/download.ts +0 -172
- package/src/browser/downloads-dir.test.ts +0 -75
- package/src/browser/downloads-dir.ts +0 -39
- package/src/browser/emulation.browser-test.ts +0 -186
- package/src/browser/emulation.test.ts +0 -200
- package/src/browser/emulation.ts +0 -204
- package/src/browser/file-chooser.test.ts +0 -142
- package/src/browser/file-chooser.ts +0 -99
- package/src/browser/file-paths.ts +0 -18
- package/src/browser/goto.browser-test.ts +0 -44
- package/src/browser/navigation-error.ts +0 -26
- package/src/browser/navigation.browser-test.ts +0 -172
- package/src/browser/page-api.browser-test.ts +0 -228
- package/src/browser/page-logs.browser-test.ts +0 -119
- package/src/browser/page-logs.test.ts +0 -212
- package/src/browser/page-logs.ts +0 -208
- package/src/browser/page-wait.ts +0 -19
- package/src/browser/page.test.ts +0 -1567
- package/src/browser/page.ts +0 -1176
- package/src/browser/pdf.ts +0 -62
- package/src/browser/screenshot.ts +0 -65
- package/src/browser/transfers.browser-test.ts +0 -180
- package/src/config/config.ts +0 -33
- package/src/config/default.ts +0 -5
- package/src/config/fixtures.test.ts +0 -137
- package/src/config/mask.test.ts +0 -143
- package/src/config/mask.ts +0 -87
- package/src/config/run-settings.test.ts +0 -163
- package/src/config/run-settings.ts +0 -114
- package/src/config/secrets.ts +0 -70
- package/src/config/variables.ts +0 -31
- package/src/index.ts +0 -10
- package/src/locator/action-timeout-error.test.ts +0 -83
- package/src/locator/action-timeout-error.ts +0 -72
- package/src/locator/element-state.test.ts +0 -181
- package/src/locator/element-state.ts +0 -159
- package/src/locator/locate-nodes.browser-test.ts +0 -169
- package/src/locator/locator-actions.test.ts +0 -355
- package/src/locator/locator-chain.test.ts +0 -292
- package/src/locator/locator.browser-test.ts +0 -137
- package/src/locator/locator.test.ts +0 -179
- package/src/locator/locator.ts +0 -656
- package/src/locator/selector-errors.ts +0 -18
- package/src/locator/selector.test.ts +0 -124
- package/src/locator/selector.ts +0 -107
- package/src/locator/set-files.browser-test.ts +0 -141
- package/src/locator/set-files.test.ts +0 -191
- package/src/locator/text-locator.test.ts +0 -118
- package/src/logger/index.test.ts +0 -28
- package/src/logger/index.ts +0 -40
- package/src/network/data-collector.test.ts +0 -111
- package/src/network/data-collector.ts +0 -92
- package/src/network/network-data.browser-test.ts +0 -192
- package/src/network/network-tracker.browser-test.ts +0 -151
- package/src/network/network-tracker.test.ts +0 -307
- package/src/network/network-tracker.ts +0 -286
- package/src/network/network-wait.ts +0 -28
- package/src/network/response.test.ts +0 -93
- package/src/network/response.ts +0 -58
- package/src/network/router.browser-test.ts +0 -260
- package/src/network/router.test.ts +0 -528
- package/src/network/router.ts +0 -420
- package/src/network/url-match.test.ts +0 -69
- package/src/network/url-match.ts +0 -41
- package/src/runner/page-logs-report.test.ts +0 -144
- package/src/runner/page-logs-report.ts +0 -82
- package/src/runner/prepare-run.test.ts +0 -33
- package/src/runner/prepare-run.ts +0 -36
- package/src/runner/registry.test.ts +0 -59
- package/src/runner/registry.ts +0 -46
- package/src/runner/reporter.test.ts +0 -73
- package/src/runner/reporter.ts +0 -109
- package/src/runner/test-error.test.ts +0 -39
- package/src/runner/test-error.ts +0 -20
- package/src/runner/test-runner.ts +0 -184
- package/src/script/call-function.browser-test.ts +0 -112
- package/src/script/call-function.test.ts +0 -174
- package/src/script/call-function.ts +0 -113
- package/src/script/helpers.test.ts +0 -116
- package/src/script/helpers.ts +0 -68
- package/src/script/preload.browser-test.ts +0 -144
- package/src/script/preload.test.ts +0 -180
- package/src/script/preload.ts +0 -102
- package/src/script/serialize.test.ts +0 -182
- package/src/script/serialize.ts +0 -203
- package/src/testing/browser-fixture.ts +0 -18
- package/src/testing/fake-websocket.ts +0 -72
- package/src/testing/stub-connector.ts +0 -154
- package/src/tests/index.spec.ts +0 -30
- package/src/transport/bidi-connection.test.ts +0 -155
- package/src/transport/bidi-connection.ts +0 -256
- package/src/transport/bidi-error.ts +0 -23
- package/src/transport/foundation.browser-test.ts +0 -73
- package/src/transport/subscriptions.test.ts +0 -140
- package/src/transport/subscriptions.ts +0 -110
- package/src/types/bidi-commands.test-d.ts +0 -55
- package/src/wait/wait-until.test.ts +0 -117
- package/src/wait/wait-until.ts +0 -86
- package/tsconfig.json +0 -44
- /package/{src/api.ts → dist/api.d.ts} +0 -0
- /package/{src/locator/text-locator.ts → dist/locator/text-locator.js} +0 -0
- /package/{src → dist}/types/bidi-modules/browser.d.ts +0 -0
- /package/{src → dist}/types/bidi-modules/browsing-context.d.ts +0 -0
- /package/{src → dist}/types/bidi-modules/emulation.d.ts +0 -0
- /package/{src → dist}/types/bidi-modules/input.d.ts +0 -0
- /package/{src → dist}/types/bidi-modules/log.d.ts +0 -0
- /package/{src → dist}/types/bidi-modules/network.d.ts +0 -0
- /package/{src → dist}/types/bidi-modules/permissions.d.ts +0 -0
- /package/{src → dist}/types/bidi-modules/script.d.ts +0 -0
- /package/{src → dist}/types/bidi-modules/session.d.ts +0 -0
- /package/{src → dist}/types/bidi-modules/storage.d.ts +0 -0
- /package/{src → dist}/types/bidi-modules/web-extensions.d.ts +0 -0
- /package/{src → dist}/types/bidi-protocols/command.d.ts +0 -0
- /package/{src → dist}/types/bidi-protocols/error.d.ts +0 -0
- /package/{src → dist}/types/bidi-protocols/event.d.ts +0 -0
- /package/{src → dist}/types/bidi.d.ts +0 -0
- /package/{src → dist}/types/browser.d.ts +0 -0
- /package/{src → dist}/types/config.d.ts +0 -0
- /package/{src → dist}/types/env.d.ts +0 -0
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { describe, it } from "node:test";
|
|
3
|
-
import { ResponseBodyUnavailableError } from "./data-collector.js";
|
|
4
|
-
import type { NetworkRequest, NetworkResponse } from "./network-tracker.js";
|
|
5
|
-
import { Response } from "./response.js";
|
|
6
|
-
|
|
7
|
-
const request: NetworkRequest = {
|
|
8
|
-
id: "r1",
|
|
9
|
-
url: "https://e.test/api",
|
|
10
|
-
method: "GET",
|
|
11
|
-
headers: {},
|
|
12
|
-
resourceType: "fetch",
|
|
13
|
-
navigation: null,
|
|
14
|
-
redirectedFrom: null,
|
|
15
|
-
isBlocked: false,
|
|
16
|
-
};
|
|
17
|
-
const data = (extra: Partial<NetworkResponse> = {}): NetworkResponse => ({
|
|
18
|
-
url: "https://e.test/api",
|
|
19
|
-
status: 200,
|
|
20
|
-
statusText: "OK",
|
|
21
|
-
headers: { "content-type": "application/json" },
|
|
22
|
-
fromCache: false,
|
|
23
|
-
request,
|
|
24
|
-
...extra,
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
function response(body: Buffer | Error, extra: Partial<NetworkResponse> = {}) {
|
|
28
|
-
let loads = 0;
|
|
29
|
-
const wrapped = new Response(data(extra), async () => {
|
|
30
|
-
loads++;
|
|
31
|
-
if (body instanceof Error) throw body;
|
|
32
|
-
return body;
|
|
33
|
-
});
|
|
34
|
-
return { wrapped, loads: () => loads };
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
describe("Response", () => {
|
|
38
|
-
it("carries what the network tracker knew", () => {
|
|
39
|
-
const { wrapped } = response(Buffer.alloc(0));
|
|
40
|
-
assert.equal(wrapped.id, "r1");
|
|
41
|
-
assert.equal(wrapped.url, "https://e.test/api");
|
|
42
|
-
assert.equal(wrapped.status, 200);
|
|
43
|
-
assert.equal(wrapped.statusText, "OK");
|
|
44
|
-
assert.deepEqual(wrapped.headers, { "content-type": "application/json" });
|
|
45
|
-
assert.equal(wrapped.fromCache, false);
|
|
46
|
-
assert.equal(wrapped.request, request);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it("body, text and json read the same bytes", async () => {
|
|
50
|
-
const { wrapped } = response(Buffer.from('{"a":"héllo"}'));
|
|
51
|
-
assert.deepEqual([...(await wrapped.body())], [...Buffer.from('{"a":"héllo"}')]);
|
|
52
|
-
assert.equal(await wrapped.text(), '{"a":"héllo"}');
|
|
53
|
-
assert.deepEqual(await wrapped.json(), { a: "héllo" });
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it("loads the body once, however often or concurrently it is read", async () => {
|
|
57
|
-
const { wrapped, loads } = response(Buffer.from("1"));
|
|
58
|
-
await Promise.all([wrapped.body(), wrapped.text(), wrapped.json()]);
|
|
59
|
-
await wrapped.body();
|
|
60
|
-
assert.equal(loads(), 1);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it("json names the url when the body is not JSON", async () => {
|
|
64
|
-
const { wrapped } = response(Buffer.from("<html>"));
|
|
65
|
-
await assert.rejects(wrapped.json(), /Response body of https:\/\/e\.test\/api is not JSON: /);
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it("a failed load can be tried again", async () => {
|
|
69
|
-
let attempts = 0;
|
|
70
|
-
const wrapped = new Response(data(), async () => {
|
|
71
|
-
if (++attempts === 1) throw new Error("first fails");
|
|
72
|
-
return Buffer.from("ok");
|
|
73
|
-
});
|
|
74
|
-
await assert.rejects(wrapped.body(), /first fails/);
|
|
75
|
-
assert.equal(await wrapped.text(), "ok");
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it("a redirect has no body, and the browser is not asked", async () => {
|
|
79
|
-
const { wrapped, loads } = response(Buffer.from("x"), { status: 302, headers: { location: "/next" } });
|
|
80
|
-
await assert.rejects(wrapped.body(), (err: unknown) => {
|
|
81
|
-
assert.ok(err instanceof ResponseBodyUnavailableError);
|
|
82
|
-
assert.equal(err.reason, "redirect response");
|
|
83
|
-
return true;
|
|
84
|
-
});
|
|
85
|
-
assert.equal(loads(), 0);
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it("a 3xx without Location (304 Not Modified) is asked for", async () => {
|
|
89
|
-
const { wrapped, loads } = response(Buffer.from("cached"), { status: 304 });
|
|
90
|
-
assert.equal(await wrapped.text(), "cached");
|
|
91
|
-
assert.equal(loads(), 1);
|
|
92
|
-
});
|
|
93
|
-
});
|
package/src/network/response.ts
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import { ResponseBodyUnavailableError } from "./data-collector.js";
|
|
2
|
-
import type { NetworkRequest, NetworkResponse } from "./network-tracker.js";
|
|
3
|
-
|
|
4
|
-
/** A response of the page, with access to its body */
|
|
5
|
-
export class Response {
|
|
6
|
-
public readonly url: string;
|
|
7
|
-
public readonly status: number;
|
|
8
|
-
public readonly statusText: string;
|
|
9
|
-
/** Header names lower-cased */
|
|
10
|
-
public readonly headers: Record<string, string>;
|
|
11
|
-
public readonly fromCache: boolean;
|
|
12
|
-
public readonly request: NetworkRequest;
|
|
13
|
-
private body_: Promise<Buffer> | undefined;
|
|
14
|
-
|
|
15
|
-
constructor(
|
|
16
|
-
data: NetworkResponse,
|
|
17
|
-
private loadBody: (response: Response) => Promise<Buffer>,
|
|
18
|
-
) {
|
|
19
|
-
this.url = data.url;
|
|
20
|
-
this.status = data.status;
|
|
21
|
-
this.statusText = data.statusText;
|
|
22
|
-
this.headers = data.headers;
|
|
23
|
-
this.fromCache = data.fromCache;
|
|
24
|
-
this.request = data.request;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/** The browser's id of the request; the same for every hop of a redirect chain */
|
|
28
|
-
public get id(): string {
|
|
29
|
-
return this.request.id;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** The body as bytes. Read once from the browser, then kept */
|
|
33
|
-
public body(): Promise<Buffer> {
|
|
34
|
-
if (this.status >= 300 && this.status < 400 && "location" in this.headers) {
|
|
35
|
-
return Promise.reject(new ResponseBodyUnavailableError(this.url, "redirect response"));
|
|
36
|
-
}
|
|
37
|
-
this.body_ ??= this.loadBody(this).catch((err) => {
|
|
38
|
-
this.body_ = undefined; // let a later call try again
|
|
39
|
-
throw err;
|
|
40
|
-
});
|
|
41
|
-
return this.body_;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/** The body as UTF-8 text */
|
|
45
|
-
public async text(): Promise<string> {
|
|
46
|
-
return (await this.body()).toString("utf8");
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/** The body parsed as JSON */
|
|
50
|
-
public async json<T = unknown>(): Promise<T> {
|
|
51
|
-
const text = await this.text();
|
|
52
|
-
try {
|
|
53
|
-
return JSON.parse(text) as T;
|
|
54
|
-
} catch (err) {
|
|
55
|
-
throw new Error(`Response body of ${this.url} is not JSON: ${(err as Error).message}`);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
}
|
|
@@ -1,260 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { createServer, type Server } from "node:http";
|
|
3
|
-
import type { AddressInfo, Socket } from "node:net";
|
|
4
|
-
import { after, before, beforeEach, test } from "node:test";
|
|
5
|
-
import { Browser } from "../browser/browser.js";
|
|
6
|
-
import type Page from "../browser/page.js";
|
|
7
|
-
import { callFunction } from "../script/call-function.js";
|
|
8
|
-
import type { BiDiConnector } from "../transport/bidi-connection.js";
|
|
9
|
-
|
|
10
|
-
let browser: Browser;
|
|
11
|
-
let page: Page;
|
|
12
|
-
let server: Server;
|
|
13
|
-
let base: string;
|
|
14
|
-
const sockets = new Set<Socket>();
|
|
15
|
-
/** What reached the server: method, url and request headers */
|
|
16
|
-
let hits: Array<{ method: string; url: string; headers: Record<string, string | string[] | undefined>; body: string }> = [];
|
|
17
|
-
|
|
18
|
-
const GIF = Buffer.from("R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==", "base64");
|
|
19
|
-
|
|
20
|
-
before(async () => {
|
|
21
|
-
server = createServer((req, res) => {
|
|
22
|
-
const chunks: Buffer[] = [];
|
|
23
|
-
req.on("data", (chunk) => chunks.push(chunk));
|
|
24
|
-
req.on("end", () => {
|
|
25
|
-
const path = (req.url ?? "").split("?")[0];
|
|
26
|
-
// The browser asks for a favicon after a navigation; that is not what the tests look at
|
|
27
|
-
if (path !== "/favicon.ico") {
|
|
28
|
-
hits.push({ method: req.method ?? "", url: req.url ?? "", headers: req.headers, body: Buffer.concat(chunks).toString() });
|
|
29
|
-
}
|
|
30
|
-
switch (path) {
|
|
31
|
-
case "/api/users":
|
|
32
|
-
res.setHeader("content-type", "application/json").end(JSON.stringify([{ id: 99, from: "server" }]));
|
|
33
|
-
break;
|
|
34
|
-
case "/echo":
|
|
35
|
-
res.setHeader("content-type", "application/json").end(JSON.stringify({ method: req.method, headers: req.headers, body: Buffer.concat(chunks).toString() }));
|
|
36
|
-
break;
|
|
37
|
-
case "/img.gif":
|
|
38
|
-
res.setHeader("content-type", "image/gif").end(GIF);
|
|
39
|
-
break;
|
|
40
|
-
case "/redirect":
|
|
41
|
-
res.writeHead(302, { location: "/api/users" }).end();
|
|
42
|
-
break;
|
|
43
|
-
case "/frame":
|
|
44
|
-
res.setHeader("content-type", "text/html").end('<body>frame<script>fetch("/api/users").then((r) => r.json()).then((d) => parent.postMessage(JSON.stringify(d), "*"))</script></body>');
|
|
45
|
-
break;
|
|
46
|
-
case "/real":
|
|
47
|
-
res.setHeader("content-type", "text/html").end("<!doctype html><body>the real page</body>");
|
|
48
|
-
break;
|
|
49
|
-
default:
|
|
50
|
-
res.setHeader("content-type", "text/plain").end("ok");
|
|
51
|
-
}
|
|
52
|
-
});
|
|
53
|
-
});
|
|
54
|
-
server.on("connection", (socket) => {
|
|
55
|
-
sockets.add(socket);
|
|
56
|
-
socket.on("close", () => sockets.delete(socket));
|
|
57
|
-
});
|
|
58
|
-
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
59
|
-
base = `http://127.0.0.1:${(server.address() as AddressInfo).port}`;
|
|
60
|
-
|
|
61
|
-
({ browser, page } = await Browser.launch("firefox", { port: 9241, headless: true }));
|
|
62
|
-
await page.navigateTo(`${base}/real`);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
after(async () => {
|
|
66
|
-
await page.dispose();
|
|
67
|
-
await browser?.close();
|
|
68
|
-
sockets.forEach((socket) => socket.destroy());
|
|
69
|
-
await new Promise((resolve) => server.close(resolve));
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
beforeEach(async () => {
|
|
73
|
-
await page.unrouteAll();
|
|
74
|
-
hits = [];
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
const OPTIONS = { timeout: 30000 };
|
|
78
|
-
const connector = () => (page as unknown as { biDiConnector: BiDiConnector }).biDiConnector;
|
|
79
|
-
const run = <T>(source: string) => callFunction<T>(connector(), page.contextId, source);
|
|
80
|
-
const fetchJson = (path: string, init = "{}") =>
|
|
81
|
-
run<{ status: number; type: string | null; body: unknown }>(`async () => {
|
|
82
|
-
const response = await fetch(${JSON.stringify(base + path)}, ${init});
|
|
83
|
-
return { status: response.status, type: response.headers.get("content-type"), body: await response.json() };
|
|
84
|
-
}`);
|
|
85
|
-
|
|
86
|
-
test("fulfill answers a request with JSON, and the server never sees it", OPTIONS, async () => {
|
|
87
|
-
await page.route(`${base}/api/users`, (route) => route.fulfill({ json: [{ id: 1, from: "mock" }] }));
|
|
88
|
-
const result = await fetchJson("/api/users");
|
|
89
|
-
assert.deepEqual(result, { status: 200, type: "application/json", body: [{ id: 1, from: "mock" }] });
|
|
90
|
-
assert.deepEqual(hits, []);
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
test("fulfill sets the status, headers and text of the answer", OPTIONS, async () => {
|
|
94
|
-
await page.route("**/teapot", (route) =>
|
|
95
|
-
route.fulfill({ status: 418, contentType: "text/plain", headers: { "x-mock": "yes" }, body: "short and stout" }),
|
|
96
|
-
);
|
|
97
|
-
const result = await run<{ status: number; text: string; mock: string | null; length: string | null }>(`async () => {
|
|
98
|
-
const response = await fetch(${JSON.stringify(base + "/teapot")});
|
|
99
|
-
return { status: response.status, text: await response.text(), mock: response.headers.get("x-mock"), length: response.headers.get("content-length") };
|
|
100
|
-
}`);
|
|
101
|
-
assert.deepEqual(result, { status: 418, text: "short and stout", mock: "yes", length: "15" });
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
test("a Buffer body arrives byte for byte", OPTIONS, async () => {
|
|
105
|
-
const bytes = Buffer.from([0, 1, 2, 3, 250, 251, 252, 255]);
|
|
106
|
-
await page.route("**/binary", (route) => route.fulfill({ contentType: "application/octet-stream", body: bytes }));
|
|
107
|
-
const received = await run<number[]>(`async () => {
|
|
108
|
-
const response = await fetch(${JSON.stringify(base + "/binary")});
|
|
109
|
-
return Array.from(new Uint8Array(await response.arrayBuffer()));
|
|
110
|
-
}`);
|
|
111
|
-
assert.deepEqual(received, [...bytes]);
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
test("routes that do not match leave requests alone", OPTIONS, async () => {
|
|
115
|
-
await page.route(`${base}/api/users`, (route) => route.fulfill({ json: [] }));
|
|
116
|
-
const other = await fetchJson("/echo");
|
|
117
|
-
assert.equal((other.body as { method: string }).method, "GET");
|
|
118
|
-
assert.deepEqual(hits.map((hit) => hit.url), ["/echo"]);
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
test("globs and regular expressions match", OPTIONS, async () => {
|
|
122
|
-
await page.route("**/glob/*", (route) => route.fulfill({ json: "glob" }));
|
|
123
|
-
await page.route(/\/regex\/\d+$/, (route) => route.fulfill({ json: "regex" }));
|
|
124
|
-
assert.equal((await fetchJson("/glob/anything")).body, "glob");
|
|
125
|
-
assert.equal((await fetchJson("/regex/42")).body, "regex");
|
|
126
|
-
assert.deepEqual(hits, []);
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
test("abort makes an image fail to load", OPTIONS, async () => {
|
|
130
|
-
await page.route(/\.gif\?abort$/, (route) => route.abort());
|
|
131
|
-
const outcome = await run<string>(`() => new Promise((resolve) => {
|
|
132
|
-
const image = new Image();
|
|
133
|
-
image.onload = () => resolve("loaded");
|
|
134
|
-
image.onerror = () => resolve("error");
|
|
135
|
-
image.src = ${JSON.stringify(base + "/img.gif?abort")};
|
|
136
|
-
})`);
|
|
137
|
-
assert.equal(outcome, "error");
|
|
138
|
-
assert.deepEqual(hits, []);
|
|
139
|
-
await page.unrouteAll();
|
|
140
|
-
const after = await run<string>(`() => new Promise((resolve) => {
|
|
141
|
-
const image = new Image();
|
|
142
|
-
image.onload = () => resolve("loaded");
|
|
143
|
-
image.onerror = () => resolve("error");
|
|
144
|
-
image.src = ${JSON.stringify(base + "/img.gif?ok")};
|
|
145
|
-
})`);
|
|
146
|
-
assert.equal(after, "loaded", "without the route the image loads");
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
test("abort makes fetch reject", OPTIONS, async () => {
|
|
150
|
-
await page.route("**/api/**", (route) => route.abort());
|
|
151
|
-
const outcome = await run<string>(`async () => { try { await fetch(${JSON.stringify(base + "/api/users")}); return "resolved"; } catch { return "rejected"; } }`);
|
|
152
|
-
assert.equal(outcome, "rejected");
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
test("continue adds a header the server sees", OPTIONS, async () => {
|
|
156
|
-
await page.route("**/echo", (route) => route.continue({ headers: { ...route.request().headers, "x-test": "1" } }));
|
|
157
|
-
const result = await fetchJson("/echo");
|
|
158
|
-
assert.equal((result.body as { headers: Record<string, string> }).headers["x-test"], "1");
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
test("continue can send a navigation to another url", OPTIONS, async () => {
|
|
162
|
-
await page.route(`${base}/navigate-me`, (route) => route.continue({ url: `${base}/real` }));
|
|
163
|
-
await page.navigateTo(`${base}/navigate-me`);
|
|
164
|
-
assert.equal(await page.locator("body").textContent(), "the real page");
|
|
165
|
-
assert.deepEqual(hits.map((hit) => hit.url), ["/real"]);
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
test("continue can change the method", OPTIONS, async () => {
|
|
169
|
-
await page.route("**/echo", (route) => route.continue({ method: "POST" }));
|
|
170
|
-
const result = await fetchJson("/echo");
|
|
171
|
-
assert.equal((result.body as { method: string }).method, "POST");
|
|
172
|
-
assert.deepEqual(hits.map((hit) => `${hit.method} ${hit.url}`), ["POST /echo"]);
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
test("continue can replace the body of a request, also with a longer one", OPTIONS, async () => {
|
|
176
|
-
await page.route("**/echo", (route) => route.continue({ postData: "a much longer replacement body" }));
|
|
177
|
-
const result = await fetchJson("/echo", `{ method: "POST", body: "short" }`);
|
|
178
|
-
assert.equal(result.status, 200);
|
|
179
|
-
assert.equal((result.body as { body: string }).body, "a much longer replacement body");
|
|
180
|
-
assert.equal(hits[0]!.headers["content-length"], String("a much longer replacement body".length));
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
test("continue can replace the body with a shorter one too", OPTIONS, async () => {
|
|
184
|
-
await page.route("**/echo", (route) => route.continue({ postData: "tiny" }));
|
|
185
|
-
const result = await fetchJson("/echo", `{ method: "POST", body: "a rather long original body" }`);
|
|
186
|
-
assert.equal((result.body as { body: string }).body, "tiny");
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
test("a redirect hop is routed on its own", OPTIONS, async () => {
|
|
190
|
-
await page.route(`${base}/api/users`, (route) => route.fulfill({ json: "mocked after redirect" }));
|
|
191
|
-
const result = await fetchJson("/redirect");
|
|
192
|
-
assert.equal(result.body, "mocked after redirect");
|
|
193
|
-
assert.deepEqual(hits.map((hit) => hit.url), ["/redirect"], "only the first hop reached the server");
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
test("a page can be mocked: navigating to a routed url shows the mocked document", OPTIONS, async () => {
|
|
197
|
-
await page.route(`${base}/mocked-page`, (route) =>
|
|
198
|
-
route.fulfill({ contentType: "text/html", body: "<!doctype html><body id='b'>mocked document</body>" }),
|
|
199
|
-
);
|
|
200
|
-
await page.navigateTo(`${base}/mocked-page`);
|
|
201
|
-
assert.equal(await page.locator("body[@id='b']").textContent(), "mocked document");
|
|
202
|
-
assert.deepEqual(hits, []);
|
|
203
|
-
await page.navigateTo(`${base}/real`);
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
test("requests of an iframe are routed too", OPTIONS, async () => {
|
|
207
|
-
await page.route(`${base}/api/users`, (route) => route.fulfill({ json: [{ from: "mock in frame" }] }));
|
|
208
|
-
const message = await run<string>(`() => new Promise((resolve) => {
|
|
209
|
-
window.addEventListener("message", (event) => resolve(event.data), { once: true });
|
|
210
|
-
const frame = document.createElement("iframe");
|
|
211
|
-
frame.src = ${JSON.stringify(base + "/frame")};
|
|
212
|
-
document.body.appendChild(frame);
|
|
213
|
-
})`);
|
|
214
|
-
assert.deepEqual(JSON.parse(message), [{ from: "mock in frame" }]);
|
|
215
|
-
assert.ok(!hits.some((hit) => hit.url === "/api/users"), "the frame's fetch was mocked");
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
test("a handler that does not answer lets the request through instead of hanging", OPTIONS, async () => {
|
|
219
|
-
await page.route("**/echo", () => {});
|
|
220
|
-
const result = await fetchJson("/echo");
|
|
221
|
-
assert.equal((result.body as { method: string }).method, "GET");
|
|
222
|
-
assert.equal(hits.length, 1);
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
test("fallback passes to an older route; the newest is asked first", OPTIONS, async () => {
|
|
226
|
-
await page.route("**/api/users", (route) => route.fulfill({ json: "older" }));
|
|
227
|
-
await page.route("**/api/*", (route) => route.fallback());
|
|
228
|
-
assert.equal((await fetchJson("/api/users")).body, "older");
|
|
229
|
-
await page.route("**/api/users", (route) => route.fulfill({ json: "newest" }));
|
|
230
|
-
assert.equal((await fetchJson("/api/users")).body, "newest");
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
test("unroute stops the mocking", OPTIONS, async () => {
|
|
234
|
-
const handler = (route: { fulfill(o: { json: unknown }): Promise<void> }) => route.fulfill({ json: "mocked" });
|
|
235
|
-
await page.route(`${base}/api/users`, handler);
|
|
236
|
-
assert.equal((await fetchJson("/api/users")).body, "mocked");
|
|
237
|
-
await page.unroute(`${base}/api/users`, handler);
|
|
238
|
-
assert.deepEqual((await fetchJson("/api/users")).body, [{ id: 99, from: "server" }]);
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
test("a request held by a handler keeps the network busy until it is answered", OPTIONS, async () => {
|
|
242
|
-
await page.route("**/slow-mock", async (route) => {
|
|
243
|
-
await new Promise((resolve) => setTimeout(resolve, 400));
|
|
244
|
-
await route.fulfill({ json: "late" });
|
|
245
|
-
});
|
|
246
|
-
await run(`() => { fetch(${JSON.stringify(base + "/slow-mock")}); }`);
|
|
247
|
-
const started = Date.now();
|
|
248
|
-
await page.waitForNetworkIdle({ idleTime: 100, timeout: 10000 });
|
|
249
|
-
assert.ok(Date.now() - started >= 350, `idle came while the request was held: ${Date.now() - started}ms`);
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
test("a handler that throws fails the request and is reported", OPTIONS, async () => {
|
|
253
|
-
await page.route("**/echo", () => {
|
|
254
|
-
throw new Error("handler bug");
|
|
255
|
-
});
|
|
256
|
-
const outcome = await run<string>(`async () => { try { await fetch(${JSON.stringify(base + "/echo")}); return "resolved"; } catch { return "rejected"; } }`);
|
|
257
|
-
assert.equal(outcome, "rejected");
|
|
258
|
-
assert.equal(page.routeErrors().length, 1);
|
|
259
|
-
assert.match(page.routeErrors()[0]!.message, /handler bug/);
|
|
260
|
-
});
|