@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,44 +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, test } from "node:test";
|
|
5
|
-
import { Browser } from "./browser.js";
|
|
6
|
-
import type Page from "./page.js";
|
|
7
|
-
|
|
8
|
-
let browser: Browser;
|
|
9
|
-
let page: Page;
|
|
10
|
-
let server: Server;
|
|
11
|
-
let base: string;
|
|
12
|
-
const sockets = new Set<Socket>();
|
|
13
|
-
|
|
14
|
-
before(async () => {
|
|
15
|
-
server = createServer((req, res) => {
|
|
16
|
-
res.setHeader("content-type", "text/html").end(`<!doctype html><title>${req.url}</title>`);
|
|
17
|
-
});
|
|
18
|
-
server.on("connection", (socket) => {
|
|
19
|
-
sockets.add(socket);
|
|
20
|
-
socket.on("close", () => sockets.delete(socket));
|
|
21
|
-
});
|
|
22
|
-
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
23
|
-
base = `http://127.0.0.1:${(server.address() as AddressInfo).port}`;
|
|
24
|
-
({ browser, page } = await Browser.launch("firefox", { port: 9251, headless: true }, AbortSignal.timeout(30000)));
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
after(async () => {
|
|
28
|
-
try {
|
|
29
|
-
await page?.dispose().catch(() => {});
|
|
30
|
-
await browser?.close().catch(() => {});
|
|
31
|
-
} finally {
|
|
32
|
-
sockets.forEach((socket) => socket.destroy());
|
|
33
|
-
if (server) await new Promise((resolve) => server.close(resolve));
|
|
34
|
-
}
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
test("goto resolves a path against the base URL", { timeout: 40000 }, async () => {
|
|
38
|
-
page.setBaseURL(`${base}/shop/`);
|
|
39
|
-
await page.goto("/products?id=1");
|
|
40
|
-
assert.equal(await page.url(), `${base}/products?id=1`);
|
|
41
|
-
assert.equal(await page.title(), "/products?id=1");
|
|
42
|
-
await page.goto("./cart");
|
|
43
|
-
assert.equal(await page.url(), `${base}/shop/cart`);
|
|
44
|
-
});
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { WaitTimeoutError } from "../wait/wait-until.js";
|
|
2
|
-
|
|
3
|
-
/** A navigation did not end in a loaded page */
|
|
4
|
-
export class NavigationError extends Error {
|
|
5
|
-
public readonly url: string;
|
|
6
|
-
public readonly reason: string;
|
|
7
|
-
/** The BiDi error code, or "timeout" */
|
|
8
|
-
public readonly code: string | undefined;
|
|
9
|
-
|
|
10
|
-
constructor(url: string, reason: string, code?: string, operation = "navigateTo") {
|
|
11
|
-
super(`${operation}(): ${url} failed: ${reason}`);
|
|
12
|
-
this.name = "NavigationError";
|
|
13
|
-
this.url = url;
|
|
14
|
-
this.reason = reason;
|
|
15
|
-
this.code = code;
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/** The page did not reach the awaited load state in time */
|
|
20
|
-
export class LoadStateTimeoutError extends WaitTimeoutError<undefined> {
|
|
21
|
-
constructor(state: string, timeout: number) {
|
|
22
|
-
super(timeout, undefined);
|
|
23
|
-
this.name = "LoadStateTimeoutError";
|
|
24
|
-
this.message = `waitForLoadState("${state}") did not finish within ${timeout}ms`;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
@@ -1,172 +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, test } from "node:test";
|
|
5
|
-
import { callFunction } from "../script/call-function.js";
|
|
6
|
-
import type { BiDiConnector } from "../transport/bidi-connection.js";
|
|
7
|
-
import { Browser } from "./browser.js";
|
|
8
|
-
import { LoadStateTimeoutError, NavigationError } from "./navigation-error.js";
|
|
9
|
-
import type Page from "./page.js";
|
|
10
|
-
|
|
11
|
-
let browser: Browser;
|
|
12
|
-
let page: Page;
|
|
13
|
-
let server: Server;
|
|
14
|
-
let base: string;
|
|
15
|
-
const sockets = new Set<Socket>();
|
|
16
|
-
|
|
17
|
-
const GIF = Buffer.from("R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==", "base64");
|
|
18
|
-
|
|
19
|
-
before(async () => {
|
|
20
|
-
server = createServer((req, res) => {
|
|
21
|
-
const url = new URL(req.url ?? "/", "http://localhost");
|
|
22
|
-
switch (url.pathname) {
|
|
23
|
-
case "/late": // DOMContentLoaded at once, load only after the slow image
|
|
24
|
-
res.setHeader("content-type", "text/html").end('<!doctype html><body>late<img src="/img?ms=700"></body>');
|
|
25
|
-
break;
|
|
26
|
-
case "/img":
|
|
27
|
-
setTimeout(() => res.setHeader("content-type", "image/gif").end(GIF), Number(url.searchParams.get("ms")));
|
|
28
|
-
break;
|
|
29
|
-
case "/hang":
|
|
30
|
-
break;
|
|
31
|
-
case "/link":
|
|
32
|
-
res.setHeader("content-type", "text/html").end('<!doctype html><body><a id="go" href="/late">go</a></body>');
|
|
33
|
-
break;
|
|
34
|
-
default:
|
|
35
|
-
res.setHeader("content-type", "text/html").end(`<!doctype html><body>page ${url.pathname}</body>`);
|
|
36
|
-
}
|
|
37
|
-
});
|
|
38
|
-
server.on("connection", (socket) => {
|
|
39
|
-
sockets.add(socket);
|
|
40
|
-
socket.on("close", () => sockets.delete(socket));
|
|
41
|
-
});
|
|
42
|
-
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
43
|
-
base = `http://127.0.0.1:${(server.address() as AddressInfo).port}`;
|
|
44
|
-
|
|
45
|
-
({ browser, page } = await Browser.launch("firefox", { port: 9238, headless: true }));
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
after(async () => {
|
|
49
|
-
await page.dispose();
|
|
50
|
-
await browser?.close();
|
|
51
|
-
sockets.forEach((socket) => socket.destroy());
|
|
52
|
-
await new Promise((resolve) => server.close(resolve));
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
const OPTIONS = { timeout: 40000 };
|
|
56
|
-
const readyState = () =>
|
|
57
|
-
callFunction<string>(
|
|
58
|
-
(page as unknown as { biDiConnector: BiDiConnector }).biDiConnector,
|
|
59
|
-
page.contextId,
|
|
60
|
-
"() => document.readyState",
|
|
61
|
-
);
|
|
62
|
-
const imageLoaded = () =>
|
|
63
|
-
callFunction<boolean>(
|
|
64
|
-
(page as unknown as { biDiConnector: BiDiConnector }).biDiConnector,
|
|
65
|
-
page.contextId,
|
|
66
|
-
"() => Array.from(document.images).every((image) => image.complete)",
|
|
67
|
-
);
|
|
68
|
-
|
|
69
|
-
async function timed<T>(action: () => Promise<T>) {
|
|
70
|
-
const started = Date.now();
|
|
71
|
-
const value = await action();
|
|
72
|
-
return { value, took: Date.now() - started };
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
test("complete (the default) resolves after the slow subresource, interactive before it", OPTIONS, async () => {
|
|
76
|
-
const complete = await timed(() => page.navigateTo(`${base}/late?complete`));
|
|
77
|
-
assert.ok(complete.took >= 650, `resolved before the image loaded: ${complete.took}ms`);
|
|
78
|
-
assert.equal(await imageLoaded(), true);
|
|
79
|
-
assert.equal(typeof complete.value.navigation, "string");
|
|
80
|
-
assert.equal(complete.value.url, `${base}/late?complete`);
|
|
81
|
-
|
|
82
|
-
const interactive = await timed(() => page.navigateTo(`${base}/late?interactive`, { wait: "interactive" }));
|
|
83
|
-
assert.ok(interactive.took < 500, `interactive waited for the image: ${interactive.took}ms`);
|
|
84
|
-
assert.equal(await imageLoaded(), false);
|
|
85
|
-
await page.waitForLoadState("load", { timeout: 5000 });
|
|
86
|
-
assert.equal(await imageLoaded(), true);
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
test("none returns at once; waitForLoadState then waits for that navigation, not the old document", OPTIONS, async () => {
|
|
90
|
-
await page.navigateTo(`${base}/quiet`); // a loaded document to start from
|
|
91
|
-
const none = await timed(() => page.navigateTo(`${base}/late?none`, { wait: "none" }));
|
|
92
|
-
assert.ok(none.took < 400, `none waited: ${none.took}ms`);
|
|
93
|
-
const load = await timed(() => page.waitForLoadState("load", { timeout: 10000 }));
|
|
94
|
-
assert.ok(none.took + load.took >= 600 || load.took >= 400, `resolved on the old document: ${load.took}ms`);
|
|
95
|
-
assert.equal(await imageLoaded(), true);
|
|
96
|
-
assert.equal(await readyState(), "complete");
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
test("domcontentloaded comes before load", OPTIONS, async () => {
|
|
100
|
-
await page.navigateTo(`${base}/quiet`);
|
|
101
|
-
await page.navigateTo(`${base}/late?dcl`, { wait: "none" });
|
|
102
|
-
const dcl = await timed(() => page.waitForLoadState("domcontentloaded", { timeout: 10000 }));
|
|
103
|
-
assert.ok(dcl.took < 600, `domcontentloaded waited for the image: ${dcl.took}ms`);
|
|
104
|
-
await page.waitForLoadState("load", { timeout: 10000 });
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
test("networkidle waits for the load and a quiet network", OPTIONS, async () => {
|
|
108
|
-
await page.navigateTo(`${base}/quiet`);
|
|
109
|
-
await page.navigateTo(`${base}/late?idle`, { wait: "none" });
|
|
110
|
-
const idle = await timed(() => page.waitForLoadState("networkidle", { timeout: 10000 }));
|
|
111
|
-
assert.ok(idle.took >= 500, `did not wait for the image: ${idle.took}ms`);
|
|
112
|
-
assert.equal(await imageLoaded(), true);
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
test("an already loaded page is ready at once", OPTIONS, async () => {
|
|
116
|
-
await page.navigateTo(`${base}/quiet`);
|
|
117
|
-
const load = await timed(() => page.waitForLoadState("load", { timeout: 5000 }));
|
|
118
|
-
assert.ok(load.took < 300, `took ${load.took}ms`);
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
test("a refused connection is a NavigationError naming the url", OPTIONS, async () => {
|
|
122
|
-
const probe = createServer();
|
|
123
|
-
await new Promise<void>((resolve) => probe.listen(0, "127.0.0.1", resolve));
|
|
124
|
-
const closedPort = (probe.address() as AddressInfo).port;
|
|
125
|
-
await new Promise((resolve) => probe.close(resolve));
|
|
126
|
-
const url = `http://127.0.0.1:${closedPort}/`;
|
|
127
|
-
await assert.rejects(page.navigateTo(url), (err) => {
|
|
128
|
-
assert.ok(err instanceof NavigationError, String(err));
|
|
129
|
-
assert.equal(err.url, url);
|
|
130
|
-
assert.ok(err.reason.length > 0);
|
|
131
|
-
assert.match(err.message, /^navigateTo\(\): http:\/\/127\.0\.0\.1:\d+\/ failed: /);
|
|
132
|
-
return true;
|
|
133
|
-
});
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
test("Firefox reports a refused connection from navigate itself, even with wait none", OPTIONS, async () => {
|
|
137
|
-
const probe = createServer();
|
|
138
|
-
await new Promise<void>((resolve) => probe.listen(0, "127.0.0.1", resolve));
|
|
139
|
-
const closedPort = (probe.address() as AddressInfo).port;
|
|
140
|
-
await new Promise((resolve) => probe.close(resolve));
|
|
141
|
-
await page.navigateTo(`${base}/quiet`);
|
|
142
|
-
await assert.rejects(page.navigateTo(`http://127.0.0.1:${closedPort}/`, { wait: "none" }), NavigationError);
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
test("a link click followed by waitForLoadState ends up on the loaded target", OPTIONS, async () => {
|
|
146
|
-
await page.navigateTo(`${base}/link`);
|
|
147
|
-
await page.locator("a[@id='go']").click({ timeout: 5000 });
|
|
148
|
-
// the navigation has started once the URL changes
|
|
149
|
-
await page.locator("img").waitFor({ state: "attached", timeout: 10000 });
|
|
150
|
-
await page.waitForLoadState("load", { timeout: 10000 });
|
|
151
|
-
assert.equal(await imageLoaded(), true);
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
test("a server that never answers hits the navigation timeout", OPTIONS, async () => {
|
|
155
|
-
const { took } = await timed(async () => {
|
|
156
|
-
await assert.rejects(page.navigateTo(`${base}/hang`, { timeout: 800 }), (err) => {
|
|
157
|
-
assert.ok(err instanceof NavigationError);
|
|
158
|
-
assert.equal(err.reason, "timeout after 800ms");
|
|
159
|
-
assert.equal(err.code, "timeout");
|
|
160
|
-
return true;
|
|
161
|
-
});
|
|
162
|
-
});
|
|
163
|
-
assert.ok(took >= 750 && took < 3000, `took ${took}ms`);
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
test("waitForLoadState times out with its own error", OPTIONS, async () => {
|
|
167
|
-
await page.navigateTo(`${base}/quiet`);
|
|
168
|
-
await page.navigateTo(`${base}/late?timeout`, { wait: "none" });
|
|
169
|
-
// the image needs 700ms: the load state is not reached in 250ms
|
|
170
|
-
await assert.rejects(page.waitForLoadState("load", { timeout: 250 }), LoadStateTimeoutError);
|
|
171
|
-
await page.waitForLoadState("load", { timeout: 5000 });
|
|
172
|
-
});
|
|
@@ -1,228 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
3
|
-
import { inflateSync } from "node:zlib";
|
|
4
|
-
import { createServer, type Server } from "node:http";
|
|
5
|
-
import type { AddressInfo, Socket } from "node:net";
|
|
6
|
-
import { tmpdir } from "node:os";
|
|
7
|
-
import path from "node:path";
|
|
8
|
-
import { after, before, test } from "node:test";
|
|
9
|
-
import { UnsupportedOperationError } from "../locator/selector-errors.js";
|
|
10
|
-
import { callFunction } from "../script/call-function.js";
|
|
11
|
-
import { setContent } from "../testing/browser-fixture.js";
|
|
12
|
-
import type { BiDiConnector } from "../transport/bidi-connection.js";
|
|
13
|
-
import { Browser } from "./browser.js";
|
|
14
|
-
import type Page from "./page.js";
|
|
15
|
-
|
|
16
|
-
let browser: Browser;
|
|
17
|
-
let page: Page;
|
|
18
|
-
let server: Server;
|
|
19
|
-
let base: string;
|
|
20
|
-
const sockets = new Set<Socket>();
|
|
21
|
-
let requests = 0;
|
|
22
|
-
const dir = mkdtempSync(path.join(tmpdir(), "samurai-page-api-"));
|
|
23
|
-
|
|
24
|
-
before(async () => {
|
|
25
|
-
server = createServer((req, res) => {
|
|
26
|
-
requests++;
|
|
27
|
-
const url = new URL(req.url ?? "/", "http://localhost");
|
|
28
|
-
res.setHeader("content-type", "text/html").setHeader("cache-control", "no-store").end(
|
|
29
|
-
`<!doctype html><title>Title ${url.pathname}</title><body style="margin:0">` +
|
|
30
|
-
`<div id="tall" style="width:200px;height:3000px;background:linear-gradient(red,blue)">${url.pathname} hit ${requests}</div>` +
|
|
31
|
-
`<div id="card" style="position:absolute;top:100px;left:20px;width:120px;height:80px;background:#3a7"></div></body>`,
|
|
32
|
-
);
|
|
33
|
-
});
|
|
34
|
-
server.on("connection", (socket) => {
|
|
35
|
-
sockets.add(socket);
|
|
36
|
-
socket.on("close", () => sockets.delete(socket));
|
|
37
|
-
});
|
|
38
|
-
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
39
|
-
base = `http://127.0.0.1:${(server.address() as AddressInfo).port}`;
|
|
40
|
-
|
|
41
|
-
({ browser, page } = await Browser.launch("firefox", { port: 9244, headless: true }));
|
|
42
|
-
await page.navigateTo(`${base}/one`);
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
after(async () => {
|
|
46
|
-
await page.dispose();
|
|
47
|
-
await browser?.close();
|
|
48
|
-
sockets.forEach((socket) => socket.destroy());
|
|
49
|
-
await new Promise((resolve) => server.close(resolve));
|
|
50
|
-
rmSync(dir, { recursive: true, force: true });
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
const OPTIONS = { timeout: 40000 };
|
|
54
|
-
const connector = () => (page as unknown as { biDiConnector: BiDiConnector }).biDiConnector;
|
|
55
|
-
const run = <T>(source: string) => callFunction<T>(connector(), page.contextId, source);
|
|
56
|
-
const size = () => run<[number, number, number]>("() => [innerWidth, innerHeight, devicePixelRatio]");
|
|
57
|
-
const png = (image: Buffer) => ({
|
|
58
|
-
magic: image.subarray(0, 8).toString("hex"),
|
|
59
|
-
width: image.readUInt32BE(16),
|
|
60
|
-
height: image.readUInt32BE(20),
|
|
61
|
-
});
|
|
62
|
-
const PNG_MAGIC = "89504e470d0a1a0a";
|
|
63
|
-
|
|
64
|
-
test("a page opens at the default viewport, 1280x720", OPTIONS, async () => {
|
|
65
|
-
assert.deepEqual((await size()).slice(0, 2), [1280, 720]);
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
test("setViewport changes the size and the pixel ratio, null puts the browser's size back", OPTIONS, async () => {
|
|
69
|
-
await page.setViewport({ width: 400, height: 300 });
|
|
70
|
-
assert.deepEqual(await size(), [400, 300, 1]);
|
|
71
|
-
await page.setViewport({ width: 500, height: 320, devicePixelRatio: 2 });
|
|
72
|
-
assert.deepEqual(await size(), [500, 320, 2]);
|
|
73
|
-
await page.setViewport(null);
|
|
74
|
-
const reset = await size();
|
|
75
|
-
assert.notDeepEqual(reset.slice(0, 2), [500, 320]);
|
|
76
|
-
await page.applyDefaultViewport();
|
|
77
|
-
assert.deepEqual((await size()).slice(0, 2), [1280, 720]);
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
test("screenshot returns a PNG of the viewport, with the viewport's size", OPTIONS, async () => {
|
|
81
|
-
await page.setViewport({ width: 400, height: 300 });
|
|
82
|
-
const image = await page.screenshot();
|
|
83
|
-
assert.deepEqual(png(image), { magic: PNG_MAGIC, width: 400, height: 300 });
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
test("a device pixel ratio of 2 doubles the image", OPTIONS, async () => {
|
|
87
|
-
await page.setViewport({ width: 400, height: 300, devicePixelRatio: 2 });
|
|
88
|
-
assert.deepEqual(png(await page.screenshot()), { magic: PNG_MAGIC, width: 800, height: 600 });
|
|
89
|
-
await page.setViewport(null); // an omitted ratio keeps the current one, so put both back
|
|
90
|
-
await page.applyDefaultViewport();
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
test("fullPage captures the whole document", OPTIONS, async () => {
|
|
94
|
-
await page.setViewport({ width: 400, height: 300 });
|
|
95
|
-
const { width, height } = png(await page.screenshot({ fullPage: true }));
|
|
96
|
-
assert.ok(width > 350 && width <= 400, `width ${width} (a scrollbar takes some of the 400)`);
|
|
97
|
-
assert.ok(height >= 3000, `height ${height}`);
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
test("clip captures a box, in viewport or document coordinates", OPTIONS, async () => {
|
|
101
|
-
await page.setViewport({ width: 400, height: 300 });
|
|
102
|
-
assert.deepEqual(png(await page.screenshot({ clip: { x: 10, y: 20, width: 50, height: 60 } })), { magic: PNG_MAGIC, width: 50, height: 60 });
|
|
103
|
-
const inDocument = png(await page.screenshot({ fullPage: true, clip: { x: 0, y: 2000, width: 40, height: 70 } }));
|
|
104
|
-
assert.deepEqual([inDocument.width, inDocument.height], [40, 70]);
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
test("jpeg is a JPEG, and a lower quality is smaller", OPTIONS, async () => {
|
|
108
|
-
await page.setViewport({ width: 400, height: 300 });
|
|
109
|
-
const high = await page.screenshot({ type: "jpeg", quality: 95 });
|
|
110
|
-
const low = await page.screenshot({ type: "jpeg", quality: 5 });
|
|
111
|
-
assert.equal(high.subarray(0, 3).toString("hex"), "ffd8ff");
|
|
112
|
-
assert.equal(low.subarray(0, 3).toString("hex"), "ffd8ff");
|
|
113
|
-
assert.ok(low.length < high.length, `q5 ${low.length} bytes vs q95 ${high.length} bytes`);
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
test("path writes the screenshot, folders included", OPTIONS, async () => {
|
|
117
|
-
const file = path.join(dir, "shots", "home.png");
|
|
118
|
-
const image = await page.screenshot({ path: file });
|
|
119
|
-
assert.ok(existsSync(file));
|
|
120
|
-
assert.deepEqual([...readFileSync(file)], [...image]);
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
test("a locator's screenshot is the size of its element", OPTIONS, async () => {
|
|
124
|
-
await page.setViewport({ width: 400, height: 300 });
|
|
125
|
-
const image = await page.locator("div[@id='card']").screenshot({ timeout: 5000 });
|
|
126
|
-
assert.deepEqual(png(image), { magic: PNG_MAGIC, width: 120, height: 80 });
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
test("a locator's screenshot scrolls an element below the fold into view", OPTIONS, async () => {
|
|
130
|
-
await setContent(page, `<div style="height:2000px"></div><div id="low" style="width:90px;height:50px;background:#c33"></div>`);
|
|
131
|
-
await page.setViewport({ width: 400, height: 300 });
|
|
132
|
-
const image = await page.locator("div[@id='low']").screenshot({ timeout: 5000 });
|
|
133
|
-
assert.deepEqual(png(image), { magic: PNG_MAGIC, width: 90, height: 50 });
|
|
134
|
-
await page.navigateTo(`${base}/one`);
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
/** The page box of the first page, looked up in the PDF text and in its compressed streams */
|
|
138
|
-
function mediaBox(pdf: Buffer): number[] | undefined {
|
|
139
|
-
const pattern = /\/MediaBox\s*\[\s*(-?[\d.]+)\s+(-?[\d.]+)\s+(-?[\d.]+)\s+(-?[\d.]+)\s*\]/;
|
|
140
|
-
const raw = pdf.toString("latin1");
|
|
141
|
-
const texts = [raw];
|
|
142
|
-
for (const match of raw.matchAll(/stream\r?\n/g)) {
|
|
143
|
-
const start = match.index! + match[0].length;
|
|
144
|
-
const end = raw.indexOf("endstream", start);
|
|
145
|
-
try {
|
|
146
|
-
texts.push(inflateSync(pdf.subarray(start, end)).toString("latin1"));
|
|
147
|
-
} catch {
|
|
148
|
-
// not a Flate stream
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
for (const text of texts) {
|
|
152
|
-
const found = pattern.exec(text);
|
|
153
|
-
if (found) return [Math.round(Number(found[3]) - Number(found[1])), Math.round(Number(found[4]) - Number(found[2]))];
|
|
154
|
-
}
|
|
155
|
-
return undefined;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
test("pdf returns a PDF with the requested paper size", OPTIONS, async () => {
|
|
159
|
-
const a4 = await page.pdf({ format: "A4" });
|
|
160
|
-
assert.equal(a4.subarray(0, 5).toString(), "%PDF-");
|
|
161
|
-
const near = (actual: number[] | undefined, expected: number[]) =>
|
|
162
|
-
assert.ok(actual && actual.every((value, i) => Math.abs(value - expected[i]!) <= 2), `${actual} is not about ${expected}`);
|
|
163
|
-
near(mediaBox(a4), [595, 842]);
|
|
164
|
-
near(mediaBox(await page.pdf({ format: "A4", landscape: true })), [842, 595]);
|
|
165
|
-
near(mediaBox(await page.pdf({ format: "Letter" })), [612, 792]);
|
|
166
|
-
const file = path.join(dir, "pdfs", "page.pdf");
|
|
167
|
-
await page.pdf({ path: file });
|
|
168
|
-
assert.equal(readFileSync(file).subarray(0, 5).toString(), "%PDF-");
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
test("url and title describe the page now", OPTIONS, async () => {
|
|
172
|
-
await page.navigateTo(`${base}/titled`);
|
|
173
|
-
assert.equal(await page.url(), `${base}/titled`);
|
|
174
|
-
assert.equal(await page.title(), "Title /titled");
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
test("goBack and goForward walk the history; at the ends they say false", OPTIONS, async () => {
|
|
178
|
-
const tab = await browser.newPage({ type: "tab" }); // its initial about:blank is not a history entry
|
|
179
|
-
try {
|
|
180
|
-
await tab.navigateTo(`${base}/first`);
|
|
181
|
-
await tab.navigateTo(`${base}/second`);
|
|
182
|
-
assert.equal(await tab.goBack(), true);
|
|
183
|
-
assert.equal(await tab.url(), `${base}/first`);
|
|
184
|
-
assert.equal(await tab.goForward(), true);
|
|
185
|
-
assert.equal(await tab.url(), `${base}/second`);
|
|
186
|
-
assert.equal(await tab.goForward(), false, "nothing ahead");
|
|
187
|
-
assert.equal(await tab.goBack(), true);
|
|
188
|
-
assert.equal(await tab.goBack(), false, "nothing behind the first page");
|
|
189
|
-
assert.equal(await tab.goBack(), false, "however often it is asked");
|
|
190
|
-
assert.equal(await tab.url(), `${base}/first`);
|
|
191
|
-
assert.equal(await tab.goForward(), true, "and the way forward is still there");
|
|
192
|
-
assert.equal(await tab.url(), `${base}/second`);
|
|
193
|
-
} finally {
|
|
194
|
-
await tab.close();
|
|
195
|
-
}
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
test("reload fetches the page again and waits for it", OPTIONS, async () => {
|
|
199
|
-
await page.navigateTo(`${base}/reloaded`);
|
|
200
|
-
const before = requests;
|
|
201
|
-
const result = await page.reload();
|
|
202
|
-
assert.ok(requests > before, "the server was asked again");
|
|
203
|
-
assert.equal(result.url, `${base}/reloaded`);
|
|
204
|
-
assert.equal(typeof result.navigation, "string");
|
|
205
|
-
assert.equal(await run("() => document.readyState"), "complete");
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
test("reload({ ignoreCache }) is reported as unsupported by this Firefox", OPTIONS, async () => {
|
|
209
|
-
await assert.rejects(page.reload({ ignoreCache: true }), UnsupportedOperationError);
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
test("close closes a tab; the page then refuses further use", OPTIONS, async () => {
|
|
213
|
-
const tab = await browser.newPage({ type: "tab" });
|
|
214
|
-
await tab.navigateTo(`${base}/tab`);
|
|
215
|
-
assert.equal(await tab.title(), "Title /tab");
|
|
216
|
-
await tab.close();
|
|
217
|
-
assert.equal(tab.closed, true);
|
|
218
|
-
await assert.rejects(tab.url(), /page closed/);
|
|
219
|
-
await assert.rejects(tab.screenshot(), /page closed/);
|
|
220
|
-
await tab.close(); // idempotent
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
test("close({ runBeforeUnload }) works on a page without handlers", OPTIONS, async () => {
|
|
224
|
-
const tab = await browser.newPage({ type: "tab" });
|
|
225
|
-
await tab.navigateTo(`${base}/tab2`);
|
|
226
|
-
await tab.close({ runBeforeUnload: true });
|
|
227
|
-
assert.equal(tab.closed, true);
|
|
228
|
-
});
|
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { after, before, test } from "node:test";
|
|
3
|
-
import { judgePageLogs } from "../runner/page-logs-report.js";
|
|
4
|
-
import { callFunction } from "../script/call-function.js";
|
|
5
|
-
import type { BiDiConnector } from "../transport/bidi-connection.js";
|
|
6
|
-
import { Browser } from "./browser.js";
|
|
7
|
-
import type { ConsoleMessage, PageError } from "./page-logs.js";
|
|
8
|
-
import type Page from "./page.js";
|
|
9
|
-
|
|
10
|
-
let browser: Browser;
|
|
11
|
-
let page: Page;
|
|
12
|
-
|
|
13
|
-
before(async () => {
|
|
14
|
-
({ browser, page } = await Browser.launch("firefox", { port: 9240, headless: true }));
|
|
15
|
-
await page.navigateTo("about:blank");
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
after(async () => {
|
|
19
|
-
await page.dispose();
|
|
20
|
-
await browser?.close();
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
const OPTIONS = { timeout: 30000 };
|
|
24
|
-
const run = (source: string) =>
|
|
25
|
-
callFunction((page as unknown as { biDiConnector: BiDiConnector }).biDiConnector, page.contextId, source, [], {
|
|
26
|
-
awaitPromise: false,
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
test("console calls arrive with method, text and plain arguments", OPTIONS, async () => {
|
|
30
|
-
page.clearLogs();
|
|
31
|
-
const seen: ConsoleMessage[] = [];
|
|
32
|
-
const onConsole = (message: ConsoleMessage) => seen.push(message);
|
|
33
|
-
page.on("console", onConsole);
|
|
34
|
-
await page.startLogging();
|
|
35
|
-
await run(`() => {
|
|
36
|
-
console.log("hello", 42, { a: [1, 2], b: "x" });
|
|
37
|
-
console.warn("careful");
|
|
38
|
-
console.error("bad but handled");
|
|
39
|
-
console.info("fyi");
|
|
40
|
-
console.debug("details");
|
|
41
|
-
}`);
|
|
42
|
-
await page.syncLogs();
|
|
43
|
-
page.off("console", onConsole);
|
|
44
|
-
|
|
45
|
-
assert.deepEqual(seen.map((message) => message.type()), ["log", "warn", "error", "info", "debug"]);
|
|
46
|
-
assert.match(seen[0]!.text(), /^hello 42 Object/, "the browser's own rendering of the call");
|
|
47
|
-
assert.deepEqual(seen[0]!.args, ["hello", 42, { a: [1, 2], b: "x" }]);
|
|
48
|
-
assert.equal(seen[1]!.level, "warn");
|
|
49
|
-
assert.equal(seen[2]!.level, "error");
|
|
50
|
-
assert.ok(seen[0]!.location(), "a location is reported");
|
|
51
|
-
assert.deepEqual(page.getLogs().map((log) => [log.type, log.method]), [
|
|
52
|
-
["console", "log"], ["console", "warn"], ["console", "error"], ["console", "info"], ["console", "debug"],
|
|
53
|
-
]);
|
|
54
|
-
assert.equal(page.pageErrors().length, 0, "console.error is not an uncaught exception");
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
test("an uncaught exception is a page error with a stack", OPTIONS, async () => {
|
|
58
|
-
page.clearLogs();
|
|
59
|
-
const errors: PageError[] = [];
|
|
60
|
-
const onError = (error: PageError) => errors.push(error);
|
|
61
|
-
page.on("pageerror", onError);
|
|
62
|
-
await run(`() => { setTimeout(function explode() { throw new TypeError("boom from the page"); }, 0); }`);
|
|
63
|
-
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
64
|
-
await page.syncLogs();
|
|
65
|
-
page.off("pageerror", onError);
|
|
66
|
-
|
|
67
|
-
assert.equal(errors.length, 1);
|
|
68
|
-
assert.match(errors[0]!.message, /TypeError: boom from the page/);
|
|
69
|
-
assert.match(errors[0]!.stack ?? "", /explode/);
|
|
70
|
-
assert.equal(page.pageErrors().length, 1);
|
|
71
|
-
assert.deepEqual(page.getLogs().map((log) => [log.type, log.level]), [["javascript", "error"]]);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
test("an unhandled promise rejection is reported as a page error too (Firefox)", OPTIONS, async () => {
|
|
75
|
-
page.clearLogs();
|
|
76
|
-
await run(`() => { Promise.reject(new Error("nobody catches me")); }`);
|
|
77
|
-
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
78
|
-
await page.syncLogs();
|
|
79
|
-
assert.deepEqual(page.pageErrors().map((error) => error.message), ["Error: nobody catches me"]);
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
test("logs of an iframe belong to the page", OPTIONS, async () => {
|
|
83
|
-
page.clearLogs();
|
|
84
|
-
await run(`() => {
|
|
85
|
-
const frame = document.body.appendChild(document.createElement("iframe"));
|
|
86
|
-
frame.src = "about:blank";
|
|
87
|
-
frame.addEventListener("load", () => frame.contentWindow.console.log("from the frame"), { once: true });
|
|
88
|
-
}`);
|
|
89
|
-
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
90
|
-
await page.syncLogs();
|
|
91
|
-
assert.deepEqual(page.getLogs().map((log) => log.text), ["from the frame"]);
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
test("the first log of a new document is not lost", OPTIONS, async () => {
|
|
95
|
-
page.clearLogs();
|
|
96
|
-
await page.navigateTo(`data:text/html,<script>console.log("very early")</script>`);
|
|
97
|
-
await page.syncLogs();
|
|
98
|
-
assert.ok(page.getLogs().some((log) => log.text === "very early"), JSON.stringify(page.getLogs()));
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
test("what the runner would do with these logs", OPTIONS, async () => {
|
|
102
|
-
page.clearLogs();
|
|
103
|
-
await run(`() => { console.log("context"); setTimeout(() => { throw new Error("uncaught in test"); }, 0); }`);
|
|
104
|
-
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
105
|
-
await page.syncLogs();
|
|
106
|
-
|
|
107
|
-
const input = {
|
|
108
|
-
entries: page.getLogs(),
|
|
109
|
-
errors: page.pageErrors(),
|
|
110
|
-
dropped: page.logsDropped,
|
|
111
|
-
testFailed: false,
|
|
112
|
-
allowPageErrors: page.pageErrorsAllowed,
|
|
113
|
-
};
|
|
114
|
-
const verdict = judgePageLogs({ ...input, config: { failOnPageError: true } });
|
|
115
|
-
assert.match(verdict.error!.message, /^1 uncaught page error\(s\): .*uncaught in test/);
|
|
116
|
-
assert.deepEqual(verdict.logs!.map((log) => log.text.replace(/^.*(context|uncaught in test).*$/, "$1")), ["context", "uncaught in test"]);
|
|
117
|
-
assert.equal(judgePageLogs({ ...input, config: { failOnPageError: true, ignoreErrors: ["uncaught in test"] } }).error, undefined);
|
|
118
|
-
assert.equal(judgePageLogs({ ...input, config: {} }).error, undefined);
|
|
119
|
-
});
|