@itmetsam/samurai-framework 0.10.0 → 0.12.1
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 +3 -4
- package/dist/assert/assertion-error.d.ts +1 -1
- package/dist/assert/expect.d.ts +5 -50
- package/dist/assert/expect.js +5 -182
- package/dist/assert/locator-assertions.d.ts +22 -0
- package/dist/assert/locator-assertions.js +61 -0
- package/dist/assert/pending-assertions.d.ts +7 -0
- package/dist/assert/pending-assertions.js +40 -0
- package/dist/assert/text-match.d.ts +4 -0
- package/dist/assert/text-match.js +11 -0
- package/dist/assert/value-assertions.d.ts +23 -0
- package/dist/assert/value-assertions.js +82 -0
- package/dist/browser/browser-context.d.ts +0 -1
- package/dist/browser/browser-context.js +13 -5
- package/dist/browser/browser-finder.js +5 -9
- package/dist/browser/browser.d.ts +3 -31
- package/dist/browser/browser.js +36 -219
- package/dist/browser/chrome-launch.d.ts +2 -1
- package/dist/browser/dialog-handling.d.ts +32 -0
- package/dist/browser/dialog-handling.js +85 -0
- package/dist/browser/document-watcher.d.ts +44 -0
- package/dist/browser/document-watcher.js +160 -0
- package/dist/browser/download-folders.d.ts +0 -1
- package/dist/browser/download-folders.js +7 -15
- package/dist/browser/download.js +5 -10
- package/dist/browser/downloads-dir.js +2 -2
- package/dist/browser/emit.d.ts +7 -0
- package/dist/browser/emit.js +16 -0
- package/dist/browser/emulation.d.ts +1 -1
- package/dist/browser/emulation.js +54 -18
- package/dist/browser/file-chooser.js +4 -11
- package/dist/browser/history.d.ts +6 -0
- package/dist/browser/history.js +89 -0
- package/dist/browser/index.d.ts +2 -0
- package/dist/browser/index.js +2 -0
- package/dist/browser/init-script.d.ts +2 -0
- package/dist/browser/init-script.js +16 -0
- package/dist/browser/launch-firefox.d.ts +20 -0
- package/dist/browser/launch-firefox.js +66 -0
- package/dist/browser/launch-flags.d.ts +18 -0
- package/dist/browser/launch-flags.js +100 -0
- package/dist/browser/lazy.d.ts +21 -0
- package/dist/browser/lazy.js +44 -0
- package/dist/browser/page-events.d.ts +44 -0
- package/dist/browser/page-events.js +58 -0
- package/dist/browser/page-logs.js +2 -9
- package/dist/browser/page-navigation.d.ts +58 -0
- package/dist/browser/page-navigation.js +108 -0
- package/dist/browser/page-network.d.ts +69 -0
- package/dist/browser/page-network.js +163 -0
- package/dist/browser/page-settings.d.ts +35 -0
- package/dist/browser/page-settings.js +51 -0
- package/dist/browser/page-transfers.d.ts +36 -0
- package/dist/browser/page-transfers.js +50 -0
- package/dist/browser/page.d.ts +39 -154
- package/dist/browser/page.js +134 -758
- package/dist/browser/pdf.d.ts +1 -1
- package/dist/browser/session.d.ts +16 -0
- package/dist/browser/session.js +32 -0
- package/dist/browser/viewport.d.ts +19 -0
- package/dist/browser/viewport.js +72 -0
- package/dist/browser/warm-up.d.ts +1 -1
- package/dist/browser/warm-up.js +2 -1
- package/dist/browsers/index.d.ts +1 -1
- package/dist/browsers/index.js +1 -1
- package/dist/browsers/pins.d.ts +0 -2
- package/dist/browsers/pins.js +0 -2
- package/dist/browsers/store.d.ts +0 -2
- package/dist/browsers/store.js +1 -2
- package/dist/cli/args.d.ts +13 -6
- package/dist/cli/args.js +13 -10
- package/dist/cli/browsers.d.ts +1 -2
- package/dist/cli/browsers.js +2 -2
- package/dist/cli/console-reporter.js +3 -2
- package/dist/cli/flow-reporter.js +2 -1
- package/dist/cli/flow.d.ts +1 -2
- package/dist/cli/flow.js +25 -22
- package/dist/cli/init.js +2 -7
- package/dist/cli/io.d.ts +10 -0
- package/dist/cli/io.js +1 -0
- package/dist/cli/main.d.ts +3 -10
- package/dist/cli/main.js +10 -143
- package/dist/cli/paint.d.ts +2 -0
- package/dist/cli/paint.js +4 -0
- package/dist/cli/record.d.ts +4 -0
- package/dist/cli/record.js +93 -0
- package/dist/cli/usage.d.ts +1 -0
- package/dist/cli/usage.js +58 -0
- package/dist/cli/version.d.ts +2 -0
- package/dist/cli/version.js +7 -0
- package/dist/config/browsers.d.ts +11 -0
- package/dist/config/browsers.js +32 -0
- package/dist/config/config.js +4 -1
- package/dist/config/mask.d.ts +1 -1
- package/dist/config/mask.js +7 -2
- package/dist/config/run-settings.d.ts +1 -2
- package/dist/config/run-settings.js +1 -6
- package/dist/config/timeouts.d.ts +2 -0
- package/dist/config/timeouts.js +13 -0
- package/dist/config/workers.d.ts +14 -0
- package/dist/config/workers.js +29 -0
- package/dist/flows/run/branch-context.d.ts +14 -0
- package/dist/flows/run/branch-context.js +35 -0
- package/dist/flows/run/handlers/condition.d.ts +1 -1
- package/dist/flows/run/handlers/end.d.ts +1 -1
- package/dist/flows/run/handlers/group.d.ts +1 -1
- package/dist/flows/run/handlers/index.d.ts +2 -2
- package/dist/flows/run/handlers/parallel.d.ts +1 -1
- package/dist/flows/run/handlers/start.d.ts +1 -1
- package/dist/flows/run/handlers/support.d.ts +3 -27
- package/dist/flows/run/handlers/test.d.ts +1 -1
- package/dist/flows/run/handlers/types.d.ts +27 -0
- package/dist/flows/run/handlers/types.js +1 -0
- package/dist/flows/run/index.d.ts +1 -1
- package/dist/flows/run/prepare-flow.d.ts +14 -0
- package/dist/flows/run/prepare-flow.js +44 -0
- package/dist/flows/run/project.d.ts +33 -2
- package/dist/flows/run/project.js +139 -57
- package/dist/flows/run/run.d.ts +3 -4
- package/dist/flows/run/run.js +23 -193
- package/dist/flows/run/walker.d.ts +56 -0
- package/dist/flows/run/walker.js +225 -0
- package/dist/flows/steps/catalog.d.ts +1 -1
- package/dist/flows/steps/handler.d.ts +1 -1
- package/dist/flows/steps/set.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/locator/actionability.d.ts +33 -0
- package/dist/locator/actionability.js +104 -0
- package/dist/locator/chain-resolver.d.ts +20 -0
- package/dist/locator/chain-resolver.js +102 -0
- package/dist/locator/document-swap.d.ts +1 -0
- package/dist/locator/document-swap.js +11 -0
- package/dist/locator/element-state.d.ts +3 -1
- package/dist/locator/element-state.js +11 -3
- package/dist/locator/file-actions.d.ts +35 -0
- package/dist/locator/file-actions.js +55 -0
- package/dist/{script/helpers.js → locator/helper-realm.js} +2 -2
- package/dist/locator/input-actions.d.ts +18 -0
- package/dist/locator/input-actions.js +80 -0
- package/dist/locator/label-locator.d.ts +1 -1
- package/dist/locator/label-locator.js +5 -12
- package/dist/locator/locator.d.ts +14 -37
- package/dist/locator/locator.js +68 -424
- package/dist/locator/page-scripts.d.ts +16 -0
- package/dist/locator/page-scripts.js +54 -0
- package/dist/locator/page-search.d.ts +10 -0
- package/dist/locator/page-search.js +19 -0
- package/dist/locator/selector-errors.d.ts +0 -4
- package/dist/locator/selector-errors.js +0 -7
- package/dist/locator/selector.d.ts +14 -13
- package/dist/locator/selector.js +10 -16
- package/dist/locator/text-locator.d.ts +1 -1
- package/dist/locator/text-locator.js +5 -9
- package/dist/logger/index.d.ts +6 -0
- package/dist/logger/index.js +14 -7
- package/dist/network/bidi-wire.d.ts +10 -0
- package/dist/network/bidi-wire.js +38 -0
- package/dist/network/intercept.d.ts +30 -0
- package/dist/network/intercept.js +130 -0
- package/dist/network/network-tracker.d.ts +10 -47
- package/dist/network/network-tracker.js +34 -68
- package/dist/network/network-types.d.ts +33 -0
- package/dist/network/network-types.js +1 -0
- package/dist/network/network-wait.d.ts +7 -2
- package/dist/network/network-wait.js +14 -4
- package/dist/network/response.d.ts +1 -1
- package/dist/network/response.js +1 -1
- package/dist/network/route.d.ts +92 -0
- package/dist/network/route.js +142 -0
- package/dist/network/router.d.ts +6 -91
- package/dist/network/router.js +25 -258
- package/dist/recorder/expectation.d.ts +5 -0
- package/dist/recorder/expectation.js +20 -0
- package/dist/recorder/files-recorder.d.ts +30 -0
- package/dist/recorder/files-recorder.js +88 -0
- package/dist/recorder/normalise.d.ts +17 -6
- package/dist/recorder/normalise.js +96 -91
- package/dist/recorder/options.d.ts +56 -0
- package/dist/recorder/options.js +1 -0
- package/dist/recorder/page-messages.d.ts +35 -0
- package/dist/recorder/page-messages.js +26 -0
- package/dist/recorder/ranking.js +1 -0
- package/dist/recorder/realms.d.ts +6 -0
- package/dist/recorder/realms.js +13 -0
- package/dist/recorder/recorder.d.ts +10 -74
- package/dist/recorder/recorder.js +39 -199
- package/dist/recorder/session-browser.d.ts +22 -0
- package/dist/recorder/session-browser.js +21 -0
- package/dist/recorder/session-secrets.d.ts +8 -0
- package/dist/recorder/session-secrets.js +45 -0
- package/dist/recorder/session.d.ts +5 -14
- package/dist/recorder/session.js +22 -120
- package/dist/recorder/spec-source.d.ts +39 -0
- package/dist/recorder/spec-source.js +84 -0
- package/dist/recorder/toolbar/action-guard.d.ts +21 -0
- package/dist/recorder/toolbar/action-guard.js +43 -0
- package/dist/recorder/toolbar/actions/assert.d.ts +4 -0
- package/dist/recorder/toolbar/actions/assert.js +52 -0
- package/dist/recorder/toolbar/actions/insert.d.ts +6 -0
- package/dist/recorder/toolbar/actions/insert.js +93 -0
- package/dist/recorder/toolbar/actions/pick.d.ts +8 -0
- package/dist/recorder/toolbar/actions/pick.js +26 -0
- package/dist/recorder/toolbar/actions/reply.d.ts +7 -0
- package/dist/recorder/toolbar/actions/reply.js +10 -0
- package/dist/recorder/toolbar/controller-fixture.d.ts +47 -0
- package/dist/recorder/toolbar/controller-fixture.js +129 -0
- package/dist/recorder/toolbar/controller.d.ts +12 -78
- package/dist/recorder/toolbar/controller.js +35 -247
- package/dist/recorder/toolbar/inspect.d.ts +1 -3
- package/dist/recorder/toolbar/inspect.js +2 -2
- package/dist/recorder/toolbar/page-scope.d.ts +19 -0
- package/dist/recorder/toolbar/page-scope.js +43 -0
- package/dist/recorder/toolbar/protocol.d.ts +2 -1
- package/dist/recorder/toolbar/source/attach.d.ts +2 -0
- package/dist/recorder/toolbar/source/attach.js +41 -0
- package/dist/recorder/toolbar/source/dom.d.ts +2 -0
- package/dist/recorder/toolbar/source/dom.js +47 -0
- package/dist/recorder/toolbar/source/drag.d.ts +2 -0
- package/dist/recorder/toolbar/source/drag.js +32 -0
- package/dist/recorder/toolbar/source/events-page.d.ts +2 -0
- package/dist/recorder/toolbar/source/events-page.js +67 -0
- package/dist/recorder/toolbar/source/events-toolbar.d.ts +2 -0
- package/dist/recorder/toolbar/source/events-toolbar.js +35 -0
- package/dist/recorder/toolbar/source/fields.d.ts +2 -0
- package/dist/recorder/toolbar/source/fields.js +44 -0
- package/dist/recorder/toolbar/source/handlers.d.ts +2 -0
- package/dist/recorder/toolbar/source/handlers.js +134 -0
- package/dist/recorder/toolbar/source/names.d.ts +2 -0
- package/dist/recorder/toolbar/source/names.js +12 -0
- package/dist/recorder/toolbar/source/picking.d.ts +2 -0
- package/dist/recorder/toolbar/source/picking.js +72 -0
- package/dist/recorder/toolbar/source/placement.d.ts +2 -0
- package/dist/recorder/toolbar/source/placement.js +45 -0
- package/dist/recorder/toolbar/source/recorder-side.d.ts +2 -0
- package/dist/recorder/toolbar/source/recorder-side.js +54 -0
- package/dist/recorder/toolbar/source/setup.d.ts +2 -0
- package/dist/recorder/toolbar/source/setup.js +50 -0
- package/dist/recorder/toolbar/source/views-panels.d.ts +2 -0
- package/dist/recorder/toolbar/source/views-panels.js +150 -0
- package/dist/recorder/toolbar/source/views-rail.d.ts +2 -0
- package/dist/recorder/toolbar/source/views-rail.js +40 -0
- package/dist/recorder/toolbar/source/views-render.d.ts +2 -0
- package/dist/recorder/toolbar/source/views-render.js +48 -0
- package/dist/recorder/toolbar/source.js +30 -856
- package/dist/recorder/toolbar/types.d.ts +70 -0
- package/dist/recorder/toolbar/types.js +1 -0
- package/dist/recorder/toolbar-bridge.d.ts +43 -0
- package/dist/recorder/toolbar-bridge.js +106 -0
- package/dist/runner/browsers.d.ts +0 -12
- package/dist/runner/browsers.js +1 -33
- package/dist/runner/options.d.ts +31 -0
- package/dist/runner/options.js +1 -0
- package/dist/runner/pooled-run.d.ts +43 -0
- package/dist/runner/pooled-run.js +75 -0
- package/dist/runner/ports.d.ts +2 -0
- package/dist/runner/ports.js +2 -0
- package/dist/runner/registry.d.ts +1 -1
- package/dist/runner/registry.js +1 -1
- package/dist/runner/reporter.d.ts +31 -10
- package/dist/runner/reporter.js +39 -6
- package/dist/runner/run-test.d.ts +16 -0
- package/dist/runner/run-test.js +178 -0
- package/dist/runner/run.d.ts +5 -3
- package/dist/runner/run.js +28 -29
- package/dist/runner/select.d.ts +24 -0
- package/dist/runner/select.js +77 -0
- package/dist/runner/test-dir.d.ts +13 -0
- package/dist/runner/test-dir.js +27 -0
- package/dist/runner/test-runner.d.ts +25 -56
- package/dist/runner/test-runner.js +81 -270
- package/dist/runner/worker-pool.d.ts +67 -0
- package/dist/runner/worker-pool.js +275 -0
- package/dist/runner/worker-process.d.ts +6 -0
- package/dist/runner/worker-process.js +54 -0
- package/dist/runner/worker-protocol.d.ts +67 -0
- package/dist/runner/worker-protocol.js +1 -0
- package/dist/runner/worker-session.d.ts +7 -0
- package/dist/runner/worker-session.js +60 -0
- package/dist/runner/worker.d.ts +26 -0
- package/dist/runner/worker.js +121 -0
- package/dist/runner/workers-option.d.ts +15 -0
- package/dist/runner/workers-option.js +22 -0
- package/dist/script/call-function.d.ts +1 -9
- package/dist/script/call-function.js +2 -19
- package/dist/script/element-handle.d.ts +1 -2
- package/dist/script/element-handle.js +1 -2
- package/dist/script/serialize.d.ts +3 -12
- package/dist/script/serialize.js +53 -62
- package/dist/steps/parse/call.d.ts +3 -0
- package/dist/steps/parse/call.js +81 -0
- package/dist/steps/parse/expectation.d.ts +5 -0
- package/dist/steps/parse/expectation.js +71 -0
- package/dist/steps/parse/file-entry.d.ts +4 -0
- package/dist/steps/parse/file-entry.js +74 -0
- package/dist/steps/parse/literals.d.ts +4 -0
- package/dist/steps/parse/literals.js +28 -0
- package/dist/steps/parse/step.d.ts +4 -0
- package/dist/steps/parse/step.js +81 -0
- package/dist/steps/parse.js +2 -323
- package/dist/transport/bidi-connection.d.ts +4 -15
- package/dist/transport/bidi-connection.js +9 -63
- package/dist/transport/log-redaction.d.ts +13 -0
- package/dist/transport/log-redaction.js +51 -0
- package/dist/transport/subscriptions.js +18 -13
- package/dist/transport/unsupported.d.ts +7 -0
- package/dist/transport/unsupported.js +13 -0
- package/dist/types/bidi-modules/browser.d.ts +1 -12
- package/dist/types/bidi-modules/browsing-context.d.ts +8 -18
- package/dist/types/bidi-modules/emulation.d.ts +29 -21
- package/dist/types/bidi-modules/input.d.ts +1 -9
- package/dist/types/bidi-modules/network.d.ts +29 -24
- package/dist/types/bidi-modules/script.d.ts +0 -9
- package/dist/types/bidi-modules/session.d.ts +0 -8
- package/dist/types/bidi-modules/storage.d.ts +0 -5
- package/dist/types/bidi.d.ts +7 -37
- package/dist/types/config.d.ts +12 -0
- package/dist/types/test.d.ts +8 -3
- package/dist/wait/wait-until.d.ts +1 -5
- package/dist/wait/wait-until.js +7 -16
- package/package.json +3 -2
- package/dist/config/default.d.ts +0 -2
- package/dist/config/default.js +0 -3
- package/dist/types/bidi-modules/web-extensions.d.ts +0 -9
- package/dist/types/bidi-protocols/command.d.ts +0 -7
- package/dist/types/bidi-protocols/event.d.ts +0 -17
- /package/dist/{script/helpers.d.ts → locator/helper-realm.d.ts} +0 -0
package/dist/network/router.d.ts
CHANGED
|
@@ -1,82 +1,9 @@
|
|
|
1
1
|
import type { ContextTree } from "../browser/context-tree.js";
|
|
2
2
|
import type { BiDiConnector } from "../transport/bidi-connection.js";
|
|
3
|
-
import { type
|
|
3
|
+
import { type RouteHandler } from "./route.js";
|
|
4
4
|
import { type UrlPattern } from "./url-match.js";
|
|
5
|
-
export type RouteHandler
|
|
6
|
-
export
|
|
7
|
-
/** @default 200 */
|
|
8
|
-
status?: number;
|
|
9
|
-
headers?: Record<string, string>;
|
|
10
|
-
/** Sets `content-type` unless `headers` has one; `json` implies "application/json" */
|
|
11
|
-
contentType?: string;
|
|
12
|
-
body?: string | Buffer;
|
|
13
|
-
/** Sent as JSON text; cannot be combined with `body` */
|
|
14
|
-
json?: unknown;
|
|
15
|
-
}
|
|
16
|
-
export interface ContinueOptions {
|
|
17
|
-
/**
|
|
18
|
-
* Sends the request to another URL. Firefox applies this to navigations only: the page's `fetch`, XHR and
|
|
19
|
-
* image requests fail when their URL is changed, so mock those with `fulfill` instead
|
|
20
|
-
*/
|
|
21
|
-
url?: string;
|
|
22
|
-
method?: string;
|
|
23
|
-
/** Replaces all request headers */
|
|
24
|
-
headers?: Record<string, string>;
|
|
25
|
-
postData?: string | Buffer;
|
|
26
|
-
}
|
|
27
|
-
interface RouteEntry {
|
|
28
|
-
pattern: UrlPattern;
|
|
29
|
-
handler: RouteHandler;
|
|
30
|
-
}
|
|
31
|
-
/** Answers `network.*` commands for one blocked request */
|
|
32
|
-
interface RouteDeps {
|
|
33
|
-
send: BiDiConnector["send"];
|
|
34
|
-
/** Passes the request to the next matching route, or lets it continue */
|
|
35
|
-
next: () => Promise<void>;
|
|
36
|
-
/** Called once the route is answered, whichever way */
|
|
37
|
-
settled: (route: Route) => void;
|
|
38
|
-
/**
|
|
39
|
-
* Whether a replaced body needs a `content-length` of its own. Firefox keeps the old one and cuts the new body
|
|
40
|
-
* off; Chrome sets the right one itself and hangs the request when it is given one.
|
|
41
|
-
*/
|
|
42
|
-
sendsContentLength: boolean;
|
|
43
|
-
}
|
|
44
|
-
/**
|
|
45
|
-
* A request the browser holds until it is answered. Answer it exactly once with `fulfill`, `continue`,
|
|
46
|
-
* `abort` or `fallback`; a second call throws.
|
|
47
|
-
*/
|
|
48
|
-
export declare class Route {
|
|
49
|
-
private readonly info;
|
|
50
|
-
/** @internal */
|
|
51
|
-
readonly entry: RouteEntry;
|
|
52
|
-
private readonly deps;
|
|
53
|
-
private answered;
|
|
54
|
-
/** @internal */
|
|
55
|
-
timer: ReturnType<typeof setTimeout> | undefined;
|
|
56
|
-
constructor(info: NetworkRequest,
|
|
57
|
-
/** @internal */
|
|
58
|
-
entry: RouteEntry, deps: RouteDeps);
|
|
59
|
-
request(): NetworkRequest;
|
|
60
|
-
/** @internal */
|
|
61
|
-
get isAnswered(): boolean;
|
|
62
|
-
/** Answers the request without sending it: status 200 and no body unless told otherwise */
|
|
63
|
-
fulfill(options?: FulfillOptions): Promise<void>;
|
|
64
|
-
/**
|
|
65
|
-
* Lets the request go on, optionally changed. `headers` replaces all request headers. A new `postData` brings its
|
|
66
|
-
* own `content-length` (the request's old one would cut the new body off or leave the server waiting).
|
|
67
|
-
*/
|
|
68
|
-
continue(options?: ContinueOptions): Promise<void>;
|
|
69
|
-
/** Makes the request fail (the browser reports a network error to the page) */
|
|
70
|
-
abort(): Promise<void>;
|
|
71
|
-
/** Hands the request to the next route that matches it, or lets it continue when there is none */
|
|
72
|
-
fallback(): Promise<void>;
|
|
73
|
-
/** @internal Lets the request continue unchanged; failures are logged, not thrown */
|
|
74
|
-
release(): Promise<void>;
|
|
75
|
-
/** @internal Fails the request; failures are logged, not thrown */
|
|
76
|
-
failQuietly(): Promise<void>;
|
|
77
|
-
private claim;
|
|
78
|
-
private answer;
|
|
79
|
-
}
|
|
5
|
+
export type { RouteHandler } from "./route.js";
|
|
6
|
+
export { Route } from "./route.js";
|
|
80
7
|
/**
|
|
81
8
|
* Lets a page mock, block and change requests. One `network.addIntercept` serves all routes; matching
|
|
82
9
|
* happens here. Every request the browser holds is answered exactly once, so nothing hangs.
|
|
@@ -88,11 +15,7 @@ export declare class Router {
|
|
|
88
15
|
private routes;
|
|
89
16
|
private pending;
|
|
90
17
|
private failures;
|
|
91
|
-
private
|
|
92
|
-
private interceptIds;
|
|
93
|
-
private currentKey;
|
|
94
|
-
private currentId;
|
|
95
|
-
private subscription;
|
|
18
|
+
private intercept;
|
|
96
19
|
private disposed;
|
|
97
20
|
private routeTimeout;
|
|
98
21
|
constructor(connector: BiDiConnector, tree: ContextTree, contextId: string, options?: {
|
|
@@ -108,18 +31,10 @@ export declare class Router {
|
|
|
108
31
|
/** Lets held requests continue, removes the intercept and stops listening. Idempotent */
|
|
109
32
|
dispose(): Promise<void>;
|
|
110
33
|
private remove;
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
private sync;
|
|
114
|
-
/** The exact URLs to block, or undefined to block everything and match here */
|
|
115
|
-
private urlPatterns;
|
|
116
|
-
private addIntercept;
|
|
117
|
-
private removeIntercept;
|
|
118
|
-
private listen;
|
|
119
|
-
private stop;
|
|
34
|
+
/** Makes the browser-side intercept match the routes */
|
|
35
|
+
private syncIntercept;
|
|
120
36
|
private onBeforeRequestSent;
|
|
121
37
|
/** Runs the `index`-th matching route; past the last one the request continues unchanged */
|
|
122
38
|
private dispatch;
|
|
123
39
|
private record;
|
|
124
40
|
}
|
|
125
|
-
export {};
|
package/dist/network/router.js
CHANGED
|
@@ -1,138 +1,13 @@
|
|
|
1
|
-
import { STATUS_CODES } from "node:http";
|
|
2
1
|
import logger from "../logger/index.js";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
2
|
+
import { toRequest } from "./bidi-wire.js";
|
|
3
|
+
import { Intercept } from "./intercept.js";
|
|
4
|
+
import { PendingRoute, } from "./route.js";
|
|
5
|
+
import { matchUrl, samePattern } from "./url-match.js";
|
|
6
|
+
export { Route } from "./route.js";
|
|
6
7
|
/** Time (ms) a handler may take to answer a request before the router lets it continue */
|
|
7
8
|
const DEFAULT_ROUTE_TIMEOUT = 30000;
|
|
8
9
|
/** How many handler errors the router keeps */
|
|
9
10
|
const MAX_ERRORS = 100;
|
|
10
|
-
function toHeaders(headers) {
|
|
11
|
-
return Object.entries(headers).map(([name, value]) => ({
|
|
12
|
-
name,
|
|
13
|
-
value: { type: "string", value },
|
|
14
|
-
}));
|
|
15
|
-
}
|
|
16
|
-
function toBytes(data) {
|
|
17
|
-
return typeof data === "string"
|
|
18
|
-
? { type: "string", value: data }
|
|
19
|
-
: { type: "base64", value: data.toString("base64") };
|
|
20
|
-
}
|
|
21
|
-
const hasHeader = (headers, name) => Object.keys(headers).some((key) => key.toLowerCase() === name);
|
|
22
|
-
/**
|
|
23
|
-
* A request the browser holds until it is answered. Answer it exactly once with `fulfill`, `continue`,
|
|
24
|
-
* `abort` or `fallback`; a second call throws.
|
|
25
|
-
*/
|
|
26
|
-
export class Route {
|
|
27
|
-
constructor(info,
|
|
28
|
-
/** @internal */
|
|
29
|
-
entry, deps) {
|
|
30
|
-
this.info = info;
|
|
31
|
-
this.entry = entry;
|
|
32
|
-
this.deps = deps;
|
|
33
|
-
this.answered = false;
|
|
34
|
-
}
|
|
35
|
-
request() {
|
|
36
|
-
return this.info;
|
|
37
|
-
}
|
|
38
|
-
/** @internal */
|
|
39
|
-
get isAnswered() {
|
|
40
|
-
return this.answered;
|
|
41
|
-
}
|
|
42
|
-
/** Answers the request without sending it: status 200 and no body unless told otherwise */
|
|
43
|
-
async fulfill(options = {}) {
|
|
44
|
-
if (options.json !== undefined && options.body !== undefined) {
|
|
45
|
-
throw new TypeError("route.fulfill(): use either json or body, not both");
|
|
46
|
-
}
|
|
47
|
-
const text = options.json !== undefined ? JSON.stringify(options.json) : undefined;
|
|
48
|
-
const body = options.body ?? text;
|
|
49
|
-
const headers = { ...options.headers };
|
|
50
|
-
const contentType = options.contentType ??
|
|
51
|
-
(text !== undefined ? "application/json" : undefined);
|
|
52
|
-
if (contentType && !hasHeader(headers, "content-type"))
|
|
53
|
-
headers["content-type"] = contentType;
|
|
54
|
-
if (!hasHeader(headers, "content-length")) {
|
|
55
|
-
headers["content-length"] = String(body === undefined ? 0 : Buffer.byteLength(body));
|
|
56
|
-
}
|
|
57
|
-
const status = options.status ?? 200;
|
|
58
|
-
await this.answer("network.provideResponse", {
|
|
59
|
-
request: this.info.id,
|
|
60
|
-
statusCode: status,
|
|
61
|
-
reasonPhrase: STATUS_CODES[status] ?? "",
|
|
62
|
-
headers: toHeaders(headers),
|
|
63
|
-
...(body !== undefined && { body: toBytes(body) }),
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
/**
|
|
67
|
-
* Lets the request go on, optionally changed. `headers` replaces all request headers. A new `postData` brings its
|
|
68
|
-
* own `content-length` (the request's old one would cut the new body off or leave the server waiting).
|
|
69
|
-
*/
|
|
70
|
-
async continue(options = {}) {
|
|
71
|
-
let headers = options.headers;
|
|
72
|
-
if (options.postData !== undefined) {
|
|
73
|
-
const withoutLength = Object.fromEntries(Object.entries(headers ?? this.info.headers).filter(([name]) => name.toLowerCase() !== "content-length"));
|
|
74
|
-
headers = this.deps.sendsContentLength
|
|
75
|
-
? {
|
|
76
|
-
...withoutLength,
|
|
77
|
-
"content-length": String(Buffer.byteLength(options.postData)),
|
|
78
|
-
}
|
|
79
|
-
: withoutLength;
|
|
80
|
-
}
|
|
81
|
-
await this.answer("network.continueRequest", {
|
|
82
|
-
request: this.info.id,
|
|
83
|
-
...(options.url !== undefined && { url: options.url }),
|
|
84
|
-
...(options.method !== undefined && { method: options.method }),
|
|
85
|
-
...(headers !== undefined && { headers: toHeaders(headers) }),
|
|
86
|
-
...(options.postData !== undefined && {
|
|
87
|
-
body: toBytes(options.postData),
|
|
88
|
-
}),
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
/** Makes the request fail (the browser reports a network error to the page) */
|
|
92
|
-
async abort() {
|
|
93
|
-
await this.answer("network.failRequest", { request: this.info.id });
|
|
94
|
-
}
|
|
95
|
-
/** Hands the request to the next route that matches it, or lets it continue when there is none */
|
|
96
|
-
async fallback() {
|
|
97
|
-
this.claim();
|
|
98
|
-
await this.deps.next();
|
|
99
|
-
}
|
|
100
|
-
/** @internal Lets the request continue unchanged; failures are logged, not thrown */
|
|
101
|
-
async release() {
|
|
102
|
-
if (this.answered)
|
|
103
|
-
return;
|
|
104
|
-
try {
|
|
105
|
-
await this.continue();
|
|
106
|
-
}
|
|
107
|
-
catch (err) {
|
|
108
|
-
logger.debug("Could not let %s continue", this.info.url, { err });
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
/** @internal Fails the request; failures are logged, not thrown */
|
|
112
|
-
async failQuietly() {
|
|
113
|
-
if (this.answered)
|
|
114
|
-
return;
|
|
115
|
-
try {
|
|
116
|
-
await this.abort();
|
|
117
|
-
}
|
|
118
|
-
catch (err) {
|
|
119
|
-
logger.debug("Could not fail %s", this.info.url, { err });
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
claim() {
|
|
123
|
-
if (this.answered)
|
|
124
|
-
throw new Error("route already handled");
|
|
125
|
-
this.answered = true;
|
|
126
|
-
clearTimeout(this.timer);
|
|
127
|
-
this.deps.settled(this);
|
|
128
|
-
}
|
|
129
|
-
async answer(method, params) {
|
|
130
|
-
this.claim();
|
|
131
|
-
await this.deps.send(method, params);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
/** Whether a browser accepts `urlPatterns` in `network.addIntercept`; asked once per connection */
|
|
135
|
-
const patternsUnsupported = new WeakSet();
|
|
136
11
|
/**
|
|
137
12
|
* Lets a page mock, block and change requests. One `network.addIntercept` serves all routes; matching
|
|
138
13
|
* happens here. Every request the browser holds is answered exactly once, so nothing hangs.
|
|
@@ -145,8 +20,6 @@ export class Router {
|
|
|
145
20
|
this.routes = [];
|
|
146
21
|
this.pending = new Set();
|
|
147
22
|
this.failures = [];
|
|
148
|
-
this.queue = Promise.resolve();
|
|
149
|
-
this.interceptIds = new Set();
|
|
150
23
|
this.disposed = false;
|
|
151
24
|
this.onBeforeRequestSent = (params) => {
|
|
152
25
|
if (!params.isBlocked || params.context === null)
|
|
@@ -154,7 +27,7 @@ export class Router {
|
|
|
154
27
|
if (!this.tree.isWithin(params.context, this.contextId))
|
|
155
28
|
return;
|
|
156
29
|
// Held by an intercept of ours, not someone else's
|
|
157
|
-
if (!(params.intercepts
|
|
30
|
+
if (!this.intercept.holds(params.intercepts))
|
|
158
31
|
return;
|
|
159
32
|
const info = toRequest(params.request, params.navigation, params.isBlocked, null);
|
|
160
33
|
const matching = this.routes
|
|
@@ -163,6 +36,7 @@ export class Router {
|
|
|
163
36
|
this.dispatch(info, matching, 0).catch((err) => logger.error("Handling the request to %s failed", info.url, { err }));
|
|
164
37
|
};
|
|
165
38
|
this.routeTimeout = options.routeTimeout ?? DEFAULT_ROUTE_TIMEOUT;
|
|
39
|
+
this.intercept = new Intercept(connector, contextId, this.onBeforeRequestSent);
|
|
166
40
|
}
|
|
167
41
|
/** Errors thrown by route handlers, oldest first */
|
|
168
42
|
get errors() {
|
|
@@ -175,7 +49,7 @@ export class Router {
|
|
|
175
49
|
const entry = { pattern, handler };
|
|
176
50
|
this.routes.push(entry);
|
|
177
51
|
try {
|
|
178
|
-
await this.
|
|
52
|
+
await this.syncIntercept();
|
|
179
53
|
}
|
|
180
54
|
catch (err) {
|
|
181
55
|
this.routes = this.routes.filter((route) => route !== entry);
|
|
@@ -204,146 +78,47 @@ export class Router {
|
|
|
204
78
|
this.routes = this.routes.filter((route) => !removed.includes(route));
|
|
205
79
|
// Requests a removed route still holds would wait for nobody
|
|
206
80
|
await Promise.all([...this.pending]
|
|
207
|
-
.filter((route) => removed.includes(route.entry))
|
|
81
|
+
.filter((route) => route.entry && removed.includes(route.entry))
|
|
208
82
|
.map((route) => route.release()));
|
|
209
|
-
await this.
|
|
83
|
+
await this.syncIntercept();
|
|
210
84
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
this.
|
|
214
|
-
return run;
|
|
215
|
-
}
|
|
216
|
-
/** Makes the browser-side intercept match the routes: none, one for everything, or one for exact URLs */
|
|
217
|
-
async sync() {
|
|
218
|
-
if (this.routes.length === 0)
|
|
219
|
-
return this.stop();
|
|
220
|
-
await this.listen();
|
|
221
|
-
const patterns = this.urlPatterns();
|
|
222
|
-
const key = patterns ? JSON.stringify(patterns) : "all";
|
|
223
|
-
if (key === this.currentKey)
|
|
224
|
-
return;
|
|
225
|
-
const id = await this.addIntercept(patterns);
|
|
226
|
-
const previous = this.currentId;
|
|
227
|
-
this.currentId = id;
|
|
228
|
-
this.currentKey =
|
|
229
|
-
patterns && !patternsUnsupported.has(this.connector) ? key : "all";
|
|
230
|
-
this.interceptIds.add(id);
|
|
231
|
-
// The new intercept is in place before the old one goes: nothing slips through in between
|
|
232
|
-
if (previous !== undefined)
|
|
233
|
-
await this.removeIntercept(previous);
|
|
234
|
-
}
|
|
235
|
-
/** The exact URLs to block, or undefined to block everything and match here */
|
|
236
|
-
urlPatterns() {
|
|
237
|
-
if (patternsUnsupported.has(this.connector))
|
|
238
|
-
return undefined;
|
|
239
|
-
const patterns = [];
|
|
240
|
-
for (const { pattern } of this.routes) {
|
|
241
|
-
// The browser reads a URL string in normal form; anything else could mean something else to it
|
|
242
|
-
if (!isExactString(pattern) || !isNormalUrl(pattern))
|
|
243
|
-
return undefined;
|
|
244
|
-
if (!patterns.some((existing) => existing.type === "string" && existing.pattern === pattern)) {
|
|
245
|
-
patterns.push({ type: "string", pattern });
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
return patterns;
|
|
249
|
-
}
|
|
250
|
-
async addIntercept(patterns) {
|
|
251
|
-
const params = {
|
|
252
|
-
phases: ["beforeRequestSent"],
|
|
253
|
-
contexts: [this.contextId],
|
|
254
|
-
};
|
|
255
|
-
try {
|
|
256
|
-
const { intercept } = await this.connector.send("network.addIntercept", {
|
|
257
|
-
...params,
|
|
258
|
-
...(patterns && { urlPatterns: patterns }),
|
|
259
|
-
});
|
|
260
|
-
return intercept;
|
|
261
|
-
}
|
|
262
|
-
catch (err) {
|
|
263
|
-
if (patterns &&
|
|
264
|
-
err instanceof BiDiError &&
|
|
265
|
-
err.code === "invalid argument") {
|
|
266
|
-
patternsUnsupported.add(this.connector);
|
|
267
|
-
return this.addIntercept(undefined);
|
|
268
|
-
}
|
|
269
|
-
throw err;
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
async removeIntercept(id) {
|
|
273
|
-
try {
|
|
274
|
-
await this.connector.send("network.removeIntercept", { intercept: id });
|
|
275
|
-
}
|
|
276
|
-
catch (err) {
|
|
277
|
-
if (!(err instanceof BiDiError && err.code === "no such intercept"))
|
|
278
|
-
throw err;
|
|
279
|
-
}
|
|
280
|
-
finally {
|
|
281
|
-
this.interceptIds.delete(id);
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
async listen() {
|
|
285
|
-
if (this.subscription)
|
|
286
|
-
return;
|
|
287
|
-
this.connector.onEvent("network.beforeRequestSent", this.onBeforeRequestSent);
|
|
288
|
-
try {
|
|
289
|
-
this.subscription = await this.connector.subscribe([
|
|
290
|
-
"network.beforeRequestSent",
|
|
291
|
-
]);
|
|
292
|
-
}
|
|
293
|
-
catch (err) {
|
|
294
|
-
this.connector.offEvent("network.beforeRequestSent", this.onBeforeRequestSent);
|
|
295
|
-
throw err;
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
async stop() {
|
|
299
|
-
const id = this.currentId;
|
|
300
|
-
this.currentId = undefined;
|
|
301
|
-
this.currentKey = undefined;
|
|
302
|
-
if (id !== undefined)
|
|
303
|
-
await this.removeIntercept(id);
|
|
304
|
-
if (this.subscription) {
|
|
305
|
-
this.connector.offEvent("network.beforeRequestSent", this.onBeforeRequestSent);
|
|
306
|
-
const subscription = this.subscription;
|
|
307
|
-
this.subscription = undefined;
|
|
308
|
-
await subscription.unsubscribe();
|
|
309
|
-
}
|
|
85
|
+
/** Makes the browser-side intercept match the routes */
|
|
86
|
+
syncIntercept() {
|
|
87
|
+
return this.intercept.sync(this.routes.map((route) => route.pattern));
|
|
310
88
|
}
|
|
311
89
|
/** Runs the `index`-th matching route; past the last one the request continues unchanged */
|
|
312
90
|
async dispatch(info, matching, index) {
|
|
313
91
|
const entry = matching[index];
|
|
314
|
-
const
|
|
92
|
+
const control = {
|
|
315
93
|
send: (method, params) => this.connector.send(method, params),
|
|
316
94
|
next: () => this.dispatch(info, matching, index + 1),
|
|
317
|
-
|
|
95
|
+
onSettled: (settled) => {
|
|
318
96
|
this.pending.delete(settled);
|
|
319
97
|
},
|
|
320
98
|
sendsContentLength: this.connector.browserName !== "chrome",
|
|
321
99
|
};
|
|
322
100
|
if (!entry) {
|
|
323
101
|
// Nobody (left) to ask: an unmatched request, or every route fell back
|
|
324
|
-
await new
|
|
102
|
+
await new PendingRoute(info, control).release();
|
|
325
103
|
return;
|
|
326
104
|
}
|
|
327
|
-
const
|
|
328
|
-
this.pending.add(
|
|
329
|
-
|
|
330
|
-
if (route.isAnswered)
|
|
331
|
-
return;
|
|
105
|
+
const pending = new PendingRoute(info, control, entry);
|
|
106
|
+
this.pending.add(pending);
|
|
107
|
+
pending.startTimer(this.routeTimeout, () => {
|
|
332
108
|
logger.error("A route for %s did not answer within %dms; letting the request continue", info.url, this.routeTimeout);
|
|
333
|
-
void
|
|
334
|
-
}
|
|
335
|
-
route.timer.unref?.();
|
|
109
|
+
void pending.release();
|
|
110
|
+
});
|
|
336
111
|
try {
|
|
337
|
-
await entry.handler(route);
|
|
112
|
+
await entry.handler(pending.route);
|
|
338
113
|
}
|
|
339
114
|
catch (err) {
|
|
340
115
|
this.record(info, err);
|
|
341
|
-
await
|
|
116
|
+
await pending.failQuietly();
|
|
342
117
|
return;
|
|
343
118
|
}
|
|
344
|
-
if (!
|
|
119
|
+
if (!pending.isAnswered) {
|
|
345
120
|
logger.warn("The route for %s did not answer the request; letting it continue", info.url);
|
|
346
|
-
await
|
|
121
|
+
await pending.release();
|
|
347
122
|
}
|
|
348
123
|
}
|
|
349
124
|
record(info, err) {
|
|
@@ -355,11 +130,3 @@ export class Router {
|
|
|
355
130
|
logger.error("A route handler threw for %s", info.url, { err });
|
|
356
131
|
}
|
|
357
132
|
}
|
|
358
|
-
function isNormalUrl(value) {
|
|
359
|
-
try {
|
|
360
|
-
return new URL(value).href === value;
|
|
361
|
-
}
|
|
362
|
-
catch {
|
|
363
|
-
return false;
|
|
364
|
-
}
|
|
365
|
-
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Expectation } from "../steps/model.js";
|
|
2
|
+
/** The name a secret gets in `secrets.<NAME>`: the field's name or id, upper snake case */
|
|
3
|
+
export declare function secretNameFor(hint: string | undefined): string;
|
|
4
|
+
/** The assertion an Alt+click or a pick starts with: the value of a field, else the text (cut when long), else visible */
|
|
5
|
+
export declare function expectationFor(text: string, value: string | undefined): Expectation;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const TEXT_LIMIT = 100;
|
|
2
|
+
/** The name a secret gets in `secrets.<NAME>`: the field's name or id, upper snake case */
|
|
3
|
+
export function secretNameFor(hint) {
|
|
4
|
+
const name = (hint ?? "")
|
|
5
|
+
.replace(/([a-z\d])([A-Z])/g, "$1_$2")
|
|
6
|
+
.replace(/[^A-Za-z0-9]+/g, "_")
|
|
7
|
+
.replace(/^_+|_+$/g, "")
|
|
8
|
+
.toUpperCase();
|
|
9
|
+
return name === "" || /^\d/.test(name) ? "PASSWORD" : name;
|
|
10
|
+
}
|
|
11
|
+
/** The assertion an Alt+click or a pick starts with: the value of a field, else the text (cut when long), else visible */
|
|
12
|
+
export function expectationFor(text, value) {
|
|
13
|
+
if (value !== undefined)
|
|
14
|
+
return { matcher: "toHaveValue", expected: value };
|
|
15
|
+
if (text === "")
|
|
16
|
+
return { matcher: "toBeVisible" };
|
|
17
|
+
return text.length <= TEXT_LIMIT
|
|
18
|
+
? { matcher: "toHaveText", expected: text }
|
|
19
|
+
: { matcher: "toContainText", expected: text.slice(0, TEXT_LIMIT).trim() };
|
|
20
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { BiDiConnector } from "../transport/bidi-connection.js";
|
|
2
|
+
import type { FileRecording, MissingFile } from "./options.js";
|
|
3
|
+
/** The warning for a file that isn't saved, as the toolbar and the CLI show it */
|
|
4
|
+
export declare function missingFileText(file: MissingFile, folder: string): string;
|
|
5
|
+
/** What `FilesRecorder` tells the toolbar, when there is one */
|
|
6
|
+
export interface FilesNotices {
|
|
7
|
+
warn(text: string): void;
|
|
8
|
+
/** A file is being saved; `undefined` when done */
|
|
9
|
+
saving(name: string | undefined): void;
|
|
10
|
+
}
|
|
11
|
+
/** Saves the files a person picked or dropped into the project's files folder and keeps count of what was and was not saved */
|
|
12
|
+
export declare class FilesRecorder {
|
|
13
|
+
private readonly connector;
|
|
14
|
+
private readonly context;
|
|
15
|
+
private readonly recording;
|
|
16
|
+
private readonly notices;
|
|
17
|
+
private readonly stored;
|
|
18
|
+
private readonly missing;
|
|
19
|
+
constructor(connector: BiDiConnector, context: string, recording: FileRecording, notices: FilesNotices);
|
|
20
|
+
/** Files stored or reused in the files folder by this recording, project-relative */
|
|
21
|
+
get storedFiles(): string[];
|
|
22
|
+
/** Files whose steps point at a path that wasn't written */
|
|
23
|
+
get missingFiles(): MissingFile[];
|
|
24
|
+
/** Saves one kept file to the files folder; returns the path its step gets */
|
|
25
|
+
store(file: {
|
|
26
|
+
id: number;
|
|
27
|
+
name: string;
|
|
28
|
+
size: number;
|
|
29
|
+
}): Promise<string>;
|
|
30
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { mkdtemp, open, rm } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import logger from "../logger/index.js";
|
|
5
|
+
import { cleanFileName, filesFolder, RECORD_LIMIT, storeFile, } from "../project/index.js";
|
|
6
|
+
import { humanSize } from "../steps/files.js";
|
|
7
|
+
import { readPageFile, releasePageFile } from "./files-reader.js";
|
|
8
|
+
/** The warning for a file that isn't saved, as the toolbar and the CLI show it */
|
|
9
|
+
export function missingFileText(file, folder) {
|
|
10
|
+
const where = folder === "" ? "the project folder" : `${folder}/`;
|
|
11
|
+
return file.reason === "too-big"
|
|
12
|
+
? `${file.name} is ${humanSize(file.size)}. Files over 50 MB aren't saved: add it to ${where} yourself, or use a generated file.`
|
|
13
|
+
: `${file.name} couldn't be read, so it isn't saved: add it to ${where} yourself, or use a generated file.`;
|
|
14
|
+
}
|
|
15
|
+
/** Saves the files a person picked or dropped into the project's files folder and keeps count of what was and was not saved */
|
|
16
|
+
export class FilesRecorder {
|
|
17
|
+
constructor(connector, context, recording, notices) {
|
|
18
|
+
this.connector = connector;
|
|
19
|
+
this.context = context;
|
|
20
|
+
this.recording = recording;
|
|
21
|
+
this.notices = notices;
|
|
22
|
+
this.stored = new Set();
|
|
23
|
+
this.missing = [];
|
|
24
|
+
}
|
|
25
|
+
/** Files stored or reused in the files folder by this recording, project-relative */
|
|
26
|
+
get storedFiles() {
|
|
27
|
+
return [...this.stored];
|
|
28
|
+
}
|
|
29
|
+
/** Files whose steps point at a path that wasn't written */
|
|
30
|
+
get missingFiles() {
|
|
31
|
+
return [...this.missing];
|
|
32
|
+
}
|
|
33
|
+
/** Saves one kept file to the files folder; returns the path its step gets */
|
|
34
|
+
async store(file) {
|
|
35
|
+
const recording = this.recording;
|
|
36
|
+
const folder = filesFolder(recording.projectDir, recording.filesDir);
|
|
37
|
+
const notSaved = (reason) => {
|
|
38
|
+
const clean = cleanFileName(file.name);
|
|
39
|
+
const missing = {
|
|
40
|
+
path: folder.relative ? `${folder.relative}/${clean}` : clean,
|
|
41
|
+
name: file.name,
|
|
42
|
+
size: file.size,
|
|
43
|
+
reason,
|
|
44
|
+
};
|
|
45
|
+
this.missing.push(missing);
|
|
46
|
+
recording.onMissing?.(missing);
|
|
47
|
+
this.notices.warn(missingFileText(missing, folder.relative));
|
|
48
|
+
return missing.path;
|
|
49
|
+
};
|
|
50
|
+
if (file.size > RECORD_LIMIT) {
|
|
51
|
+
await releasePageFile(this.connector, this.context, file.id).catch(() => undefined);
|
|
52
|
+
return notSaved("too-big");
|
|
53
|
+
}
|
|
54
|
+
this.notices.saving(file.name);
|
|
55
|
+
let temp;
|
|
56
|
+
try {
|
|
57
|
+
temp = await mkdtemp(path.join(tmpdir(), "samurai-upload-"));
|
|
58
|
+
const content = path.join(temp, "content");
|
|
59
|
+
const out = await open(content, "w");
|
|
60
|
+
try {
|
|
61
|
+
await readPageFile(this.connector, this.context, file.id, file.size, (bytes) => out.write(bytes));
|
|
62
|
+
}
|
|
63
|
+
finally {
|
|
64
|
+
await out.close();
|
|
65
|
+
}
|
|
66
|
+
const stored = await storeFile({
|
|
67
|
+
projectDir: recording.projectDir,
|
|
68
|
+
...(recording.filesDir !== undefined && {
|
|
69
|
+
filesDir: recording.filesDir,
|
|
70
|
+
}),
|
|
71
|
+
name: file.name,
|
|
72
|
+
content,
|
|
73
|
+
});
|
|
74
|
+
this.stored.add(stored.path);
|
|
75
|
+
return stored.path;
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
logger.warn("Recorder could not save %s", file.name, { err });
|
|
79
|
+
return notSaved("unreadable");
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
this.notices.saving(undefined);
|
|
83
|
+
if (temp)
|
|
84
|
+
await rm(temp, { recursive: true, force: true }).catch(() => undefined);
|
|
85
|
+
await releasePageFile(this.connector, this.context, file.id).catch(() => undefined);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -76,7 +76,7 @@ export type Observation = {
|
|
|
76
76
|
type: "goto";
|
|
77
77
|
url: string;
|
|
78
78
|
};
|
|
79
|
-
|
|
79
|
+
interface NormaliserOptions {
|
|
80
80
|
/** Index the first recorded step gets */
|
|
81
81
|
at: number;
|
|
82
82
|
/** Relative URLs are written against this */
|
|
@@ -84,11 +84,6 @@ export interface NormaliserOptions {
|
|
|
84
84
|
/** A navigation within this many ms of the last action is a consequence of it. @default 2000 */
|
|
85
85
|
navigationWindow?: number;
|
|
86
86
|
}
|
|
87
|
-
/** A picker that opens within this many ms of a click was opened by it */
|
|
88
|
-
export declare const PICKER_WINDOW = 1000;
|
|
89
|
-
/** The name a secret gets in `secrets.<NAME>`: the field's name or id, upper snake case */
|
|
90
|
-
export declare function secretNameFor(hint: string | undefined): string;
|
|
91
|
-
export declare function expectationFor(text: string, value: string | undefined): Expectation;
|
|
92
87
|
/**
|
|
93
88
|
* Turns what the tester did into steps:
|
|
94
89
|
* - typing into a field becomes one `fill`, rewritten as the text grows (a password becomes a secret reference);
|
|
@@ -117,8 +112,24 @@ export declare class Normaliser {
|
|
|
117
112
|
/** Steps the toolbar asks for directly; undefined for page observations */
|
|
118
113
|
private toolbarStep;
|
|
119
114
|
observe(observation: Observation, at: number): RecorderEvent[];
|
|
115
|
+
/**
|
|
116
|
+
* The sandbox reports a picker for any click on a file input, including ones a page script fires: it is only a
|
|
117
|
+
* hint, and only the very last step, recorded just before, may be replaced or removed (a page's own
|
|
118
|
+
* input.click() also counts as the opening click, bounded to the last step within PICKER_WINDOW)
|
|
119
|
+
*/
|
|
120
|
+
private pickerOpened;
|
|
121
|
+
/** The picker closed without a choice: the click that opened it is taken out again */
|
|
122
|
+
private pickerCancelled;
|
|
123
|
+
private filesSet;
|
|
124
|
+
private clicked;
|
|
125
|
+
private pressed;
|
|
126
|
+
/** An Alt+click */
|
|
127
|
+
private asserted;
|
|
128
|
+
/** Typing: one `fill` per field, rewritten as the text grows */
|
|
129
|
+
private typed;
|
|
120
130
|
/** A top-level navigation started */
|
|
121
131
|
navigated(url: string, at: number): RecorderEvent[];
|
|
122
132
|
/** The page the recording starts on, written as a `goto` */
|
|
123
133
|
initialGoto(url: string): RecorderEvent[];
|
|
124
134
|
}
|
|
135
|
+
export {};
|