@itmetsam/samurai-framework 0.21.4 → 0.22.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/dist/api.d.ts +8 -1
- package/dist/api.js +1 -0
- package/dist/browser/browser-context.d.ts +18 -6
- package/dist/browser/browser-context.js +25 -7
- package/dist/browser/browser.d.ts +11 -2
- package/dist/browser/browser.js +31 -8
- package/dist/browser/cdp.d.ts +11 -0
- package/dist/browser/cdp.js +28 -0
- package/dist/browser/chrome-launch.d.ts +4 -0
- package/dist/browser/chrome-launch.js +11 -3
- package/dist/browser/context-cookie-store.d.ts +17 -0
- package/dist/browser/context-cookie-store.js +90 -0
- package/dist/browser/index.d.ts +8 -1
- package/dist/browser/index.js +5 -1
- package/dist/browser/keyboard.d.ts +35 -0
- package/dist/browser/keyboard.js +83 -0
- package/dist/browser/launch-firefox.js +1 -1
- package/dist/browser/launch-flags.d.ts +4 -1
- package/dist/browser/launch-flags.js +18 -5
- package/dist/browser/mouse.d.ts +42 -0
- package/dist/browser/mouse.js +66 -0
- package/dist/browser/page-navigation.d.ts +0 -2
- package/dist/browser/page-navigation.js +1 -6
- package/dist/browser/page-settings.d.ts +1 -1
- package/dist/browser/page.d.ts +20 -6
- package/dist/browser/page.js +24 -8
- package/dist/browser/session-capabilities.d.ts +10 -0
- package/dist/browser/session-capabilities.js +11 -0
- package/dist/browser/session.d.ts +2 -1
- package/dist/browser/session.js +3 -3
- package/dist/browser/storage-state.d.ts +4 -0
- package/dist/browser/storage-state.js +17 -0
- package/dist/browser/use-applier.d.ts +38 -0
- package/dist/browser/use-applier.js +121 -0
- package/dist/browser/use-commands.d.ts +36 -0
- package/dist/browser/use-commands.js +120 -0
- package/dist/browser/viewport.d.ts +3 -2
- package/dist/browser/viewport.js +15 -2
- package/dist/cli/args.d.ts +8 -4
- package/dist/cli/args.js +8 -5
- package/dist/cli/console-reporter.js +5 -1
- package/dist/cli/main.js +1 -0
- package/dist/cli/usage.d.ts +1 -1
- package/dist/cli/usage.js +4 -1
- package/dist/config/base-url.d.ts +4 -0
- package/dist/config/base-url.js +6 -0
- package/dist/config/config.js +8 -0
- package/dist/config/fully-parallel.d.ts +3 -0
- package/dist/config/fully-parallel.js +6 -0
- package/dist/config/group-use.d.ts +8 -0
- package/dist/config/group-use.js +41 -0
- package/dist/config/resolved-config.d.ts +2 -0
- package/dist/config/resolved-config.js +3 -0
- package/dist/config/storage-state-file.d.ts +3 -0
- package/dist/config/storage-state-file.js +20 -0
- package/dist/config/use-options.d.ts +18 -0
- package/dist/config/use-options.js +184 -0
- package/dist/config/web-server-options.d.ts +7 -0
- package/dist/config/web-server-options.js +111 -0
- package/dist/locator/check-actions.js +1 -1
- package/dist/locator/drag-actions.d.ts +17 -0
- package/dist/locator/drag-actions.js +80 -0
- package/dist/locator/element-actions.d.ts +10 -4
- package/dist/locator/element-actions.js +23 -16
- package/dist/locator/element-state.d.ts +5 -0
- package/dist/locator/element-state.js +2 -1
- package/dist/locator/event-actions.d.ts +10 -0
- package/dist/locator/event-actions.js +81 -0
- package/dist/locator/input-actions.d.ts +2 -11
- package/dist/locator/input-actions.js +3 -56
- package/dist/locator/key-actions.d.ts +11 -0
- package/dist/locator/key-actions.js +52 -0
- package/dist/locator/key-map.d.ts +16 -0
- package/dist/locator/key-map.js +76 -0
- package/dist/locator/locator.d.ts +35 -5
- package/dist/locator/locator.js +70 -16
- package/dist/locator/page-scripts.d.ts +2 -0
- package/dist/locator/page-scripts.js +7 -0
- package/dist/locator/pointer-actions.d.ts +44 -0
- package/dist/locator/pointer-actions.js +117 -0
- package/dist/locator/pointer-element-actions.d.ts +21 -0
- package/dist/locator/pointer-element-actions.js +42 -0
- package/dist/logger/warn-once.d.ts +4 -0
- package/dist/logger/warn-once.js +13 -0
- package/dist/request/api-request-context.d.ts +65 -0
- package/dist/request/api-request-context.js +198 -0
- package/dist/request/api-response.d.ts +34 -0
- package/dist/request/api-response.js +57 -0
- package/dist/request/cookie-jar.d.ts +48 -0
- package/dist/request/cookie-jar.js +164 -0
- package/dist/request/http-transport.d.ts +31 -0
- package/dist/request/http-transport.js +113 -0
- package/dist/request/redirect.d.ts +10 -0
- package/dist/request/redirect.js +21 -0
- package/dist/request/request-body.d.ts +27 -0
- package/dist/request/request-body.js +71 -0
- package/dist/request/request.d.ts +16 -0
- package/dist/request/request.js +20 -0
- package/dist/runner/all-hooks.d.ts +7 -1
- package/dist/runner/all-hooks.js +17 -1
- package/dist/runner/attachments.d.ts +16 -0
- package/dist/runner/attachments.js +60 -0
- package/dist/runner/fixture-args.d.ts +15 -0
- package/dist/runner/fixture-args.js +95 -0
- package/dist/runner/fixture-defs.d.ts +22 -0
- package/dist/runner/fixture-defs.js +104 -0
- package/dist/runner/fixture-scope.d.ts +33 -0
- package/dist/runner/fixture-scope.js +156 -0
- package/dist/runner/fixtures.d.ts +37 -0
- package/dist/runner/fixtures.js +40 -0
- package/dist/runner/global-hooks.d.ts +13 -0
- package/dist/runner/global-hooks.js +66 -0
- package/dist/runner/hooks.d.ts +6 -1
- package/dist/runner/hooks.js +15 -3
- package/dist/runner/options.d.ts +13 -2
- package/dist/runner/output-tail.d.ts +14 -0
- package/dist/runner/output-tail.js +30 -0
- package/dist/runner/pool-schedule.d.ts +22 -0
- package/dist/runner/pool-schedule.js +55 -0
- package/dist/runner/pool-units.d.ts +34 -0
- package/dist/runner/pool-units.js +85 -0
- package/dist/runner/pooled-run.d.ts +7 -5
- package/dist/runner/pooled-run.js +19 -9
- package/dist/runner/registry.d.ts +3 -39
- package/dist/runner/registry.js +99 -21
- package/dist/runner/reporter.d.ts +17 -1
- package/dist/runner/reporter.js +15 -2
- package/dist/runner/request-fixture.d.ts +14 -0
- package/dist/runner/request-fixture.js +25 -0
- package/dist/runner/run-hooks.d.ts +16 -0
- package/dist/runner/run-hooks.js +52 -0
- package/dist/runner/run-test.d.ts +1 -3
- package/dist/runner/run-test.js +98 -35
- package/dist/runner/run.d.ts +1 -0
- package/dist/runner/run.js +1 -0
- package/dist/runner/select.d.ts +15 -4
- package/dist/runner/select.js +38 -12
- package/dist/runner/serial-run.d.ts +23 -0
- package/dist/runner/serial-run.js +88 -0
- package/dist/runner/serial.d.ts +34 -0
- package/dist/runner/serial.js +63 -0
- package/dist/runner/shard.d.ts +22 -0
- package/dist/runner/shard.js +73 -0
- package/dist/runner/steps.d.ts +13 -0
- package/dist/runner/steps.js +67 -0
- package/dist/runner/suite.d.ts +8 -3
- package/dist/runner/suite.js +12 -3
- package/dist/runner/test-annotations.d.ts +16 -0
- package/dist/runner/test-annotations.js +47 -0
- package/dist/runner/test-function.d.ts +115 -0
- package/dist/runner/test-function.js +1 -0
- package/dist/runner/test-info.d.ts +104 -0
- package/dist/runner/test-info.js +170 -0
- package/dist/runner/test-runner.d.ts +1 -0
- package/dist/runner/test-runner.js +64 -19
- package/dist/runner/test-timeout.d.ts +22 -0
- package/dist/runner/test-timeout.js +45 -0
- package/dist/runner/use-options.d.ts +7 -0
- package/dist/runner/use-options.js +25 -0
- package/dist/runner/wait-for-url.d.ts +20 -0
- package/dist/runner/wait-for-url.js +65 -0
- package/dist/runner/web-server.d.ts +16 -0
- package/dist/runner/web-server.js +165 -0
- package/dist/runner/worker-pool.d.ts +13 -5
- package/dist/runner/worker-pool.js +85 -51
- package/dist/runner/worker-protocol.d.ts +8 -0
- package/dist/runner/worker-session.js +5 -2
- package/dist/runner/worker.d.ts +2 -0
- package/dist/runner/worker.js +7 -1
- package/dist/script/element-handle.d.ts +1 -1
- package/dist/script/element-handle.js +1 -1
- package/dist/types/config.d.ts +86 -2
- package/dist/types/extend.d.ts +30 -0
- package/dist/types/test.d.ts +138 -0
- package/dist/types/use.d.ts +85 -0
- package/package.json +1 -1
- package/dist/browser/emulation.d.ts +0 -61
- package/dist/browser/emulation.js +0 -180
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { HeaderEntry } from "./api-response.js";
|
|
2
|
+
/** One request to send, a single hop: redirects are the caller's business */
|
|
3
|
+
export interface HopRequest {
|
|
4
|
+
url: URL;
|
|
5
|
+
method: string;
|
|
6
|
+
/** Names lower-cased */
|
|
7
|
+
headers: Record<string, string>;
|
|
8
|
+
body?: Buffer | undefined;
|
|
9
|
+
ignoreHTTPSErrors: boolean;
|
|
10
|
+
/** Milliseconds this hop may take; 0 is no limit */
|
|
11
|
+
timeout: number;
|
|
12
|
+
signal: AbortSignal;
|
|
13
|
+
}
|
|
14
|
+
export interface HopResponse {
|
|
15
|
+
status: number;
|
|
16
|
+
statusText: string;
|
|
17
|
+
headers: HeaderEntry[];
|
|
18
|
+
/** The `Set-Cookie` headers, one entry each */
|
|
19
|
+
setCookies: string[];
|
|
20
|
+
/** Decoded: `Content-Encoding` is already undone */
|
|
21
|
+
body: Buffer;
|
|
22
|
+
}
|
|
23
|
+
/** A request that failed before a response was complete */
|
|
24
|
+
export declare class TransportError extends Error {
|
|
25
|
+
readonly code?: string | undefined;
|
|
26
|
+
constructor(message: string, code?: string | undefined);
|
|
27
|
+
}
|
|
28
|
+
/** The url without credentials or query: what an error may say about where the request went */
|
|
29
|
+
export declare function describeURL(url: URL): string;
|
|
30
|
+
/** Sends one request over `http` or `https` and reads the whole response. Errors never include header values. */
|
|
31
|
+
export declare function sendHop(request: HopRequest): Promise<HopResponse>;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import http from "node:http";
|
|
2
|
+
import https from "node:https";
|
|
3
|
+
import zlib from "node:zlib";
|
|
4
|
+
/** A request that failed before a response was complete */
|
|
5
|
+
export class TransportError extends Error {
|
|
6
|
+
constructor(message, code) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.code = code;
|
|
9
|
+
this.name = "TransportError";
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
function decode(body, encoding) {
|
|
13
|
+
if (body.length === 0)
|
|
14
|
+
return body;
|
|
15
|
+
switch (encoding?.trim().toLowerCase()) {
|
|
16
|
+
case "gzip":
|
|
17
|
+
case "x-gzip":
|
|
18
|
+
return zlib.gunzipSync(body);
|
|
19
|
+
case "br":
|
|
20
|
+
return zlib.brotliDecompressSync(body);
|
|
21
|
+
case "deflate":
|
|
22
|
+
try {
|
|
23
|
+
return zlib.inflateSync(body);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
// Some servers send deflate without the zlib header
|
|
27
|
+
return zlib.inflateRawSync(body);
|
|
28
|
+
}
|
|
29
|
+
default:
|
|
30
|
+
return body;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/** The url without credentials or query: what an error may say about where the request went */
|
|
34
|
+
export function describeURL(url) {
|
|
35
|
+
return `${url.origin}${url.pathname}`;
|
|
36
|
+
}
|
|
37
|
+
/** Sends one request over `http` or `https` and reads the whole response. Errors never include header values. */
|
|
38
|
+
export function sendHop(request) {
|
|
39
|
+
const { url, method, headers, body, signal } = request;
|
|
40
|
+
const where = `${method} ${describeURL(url)}`;
|
|
41
|
+
return new Promise((resolve, reject) => {
|
|
42
|
+
if (signal.aborted)
|
|
43
|
+
return reject(new TransportError(`request aborted (${where})`));
|
|
44
|
+
const transport = url.protocol === "https:" ? https : http;
|
|
45
|
+
let settled = false;
|
|
46
|
+
let timer;
|
|
47
|
+
const finish = (error, value) => {
|
|
48
|
+
if (settled)
|
|
49
|
+
return;
|
|
50
|
+
settled = true;
|
|
51
|
+
signal.removeEventListener("abort", onAbort);
|
|
52
|
+
clearTimeout(timer);
|
|
53
|
+
if (error)
|
|
54
|
+
reject(error);
|
|
55
|
+
else
|
|
56
|
+
resolve(value);
|
|
57
|
+
};
|
|
58
|
+
const req = transport.request(url, {
|
|
59
|
+
method,
|
|
60
|
+
headers: {
|
|
61
|
+
...headers,
|
|
62
|
+
...(body && { "content-length": String(body.length) }),
|
|
63
|
+
},
|
|
64
|
+
// No shared pool: nothing stays open after the last request
|
|
65
|
+
agent: false,
|
|
66
|
+
...(url.protocol === "https:" && {
|
|
67
|
+
rejectUnauthorized: !request.ignoreHTTPSErrors,
|
|
68
|
+
}),
|
|
69
|
+
}, (res) => {
|
|
70
|
+
const chunks = [];
|
|
71
|
+
res.on("data", (chunk) => chunks.push(chunk));
|
|
72
|
+
res.on("error", (err) => finish(new TransportError(`${err.message} (${where})`)));
|
|
73
|
+
res.on("end", () => {
|
|
74
|
+
try {
|
|
75
|
+
const raw = res.rawHeaders;
|
|
76
|
+
const entries = [];
|
|
77
|
+
for (let i = 0; i + 1 < raw.length; i += 2)
|
|
78
|
+
entries.push({
|
|
79
|
+
name: raw[i],
|
|
80
|
+
value: raw[i + 1],
|
|
81
|
+
});
|
|
82
|
+
const decoded = method === "HEAD"
|
|
83
|
+
? Buffer.alloc(0)
|
|
84
|
+
: decode(Buffer.concat(chunks), res.headers["content-encoding"]);
|
|
85
|
+
finish(undefined, {
|
|
86
|
+
status: res.statusCode ?? 0,
|
|
87
|
+
statusText: res.statusMessage ?? "",
|
|
88
|
+
headers: entries,
|
|
89
|
+
setCookies: res.headers["set-cookie"] ?? [],
|
|
90
|
+
body: decoded,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
finish(new TransportError(`the response body could not be decoded: ${err.message} (${where})`));
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
const onAbort = () => {
|
|
99
|
+
req.destroy();
|
|
100
|
+
finish(new TransportError(`request aborted (${where})`));
|
|
101
|
+
};
|
|
102
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
103
|
+
timer =
|
|
104
|
+
request.timeout > 0
|
|
105
|
+
? setTimeout(() => {
|
|
106
|
+
req.destroy();
|
|
107
|
+
finish(new TransportError(`timed out after ${request.timeout}ms (${where})`, "ETIMEDOUT"));
|
|
108
|
+
}, request.timeout)
|
|
109
|
+
: undefined;
|
|
110
|
+
req.on("error", (err) => finish(new TransportError(`${err.code ?? err.message} (${where})`, err.code)));
|
|
111
|
+
req.end(body);
|
|
112
|
+
});
|
|
113
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Where a response sends the client next: its `Location` against the URL it came from; `undefined` when it does not redirect */
|
|
2
|
+
export declare function redirectTarget(status: number, location: string | undefined, from: URL): URL | undefined;
|
|
3
|
+
/**
|
|
4
|
+
* The method and body of the request that follows a redirect. 303 (any method but GET and HEAD) and 301/302 after
|
|
5
|
+
* a POST turn into a GET without a body; 307 and 308 keep both.
|
|
6
|
+
*/
|
|
7
|
+
export declare function followUp<B>(status: number, method: string, body: B | undefined): {
|
|
8
|
+
method: string;
|
|
9
|
+
body: B | undefined;
|
|
10
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const REDIRECTS = new Set([301, 302, 303, 307, 308]);
|
|
2
|
+
/** Where a response sends the client next: its `Location` against the URL it came from; `undefined` when it does not redirect */
|
|
3
|
+
export function redirectTarget(status, location, from) {
|
|
4
|
+
if (!REDIRECTS.has(status) || !location)
|
|
5
|
+
return undefined;
|
|
6
|
+
try {
|
|
7
|
+
return new URL(location, from);
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* The method and body of the request that follows a redirect. 303 (any method but GET and HEAD) and 301/302 after
|
|
15
|
+
* a POST turn into a GET without a body; 307 and 308 keep both.
|
|
16
|
+
*/
|
|
17
|
+
export function followUp(status, method, body) {
|
|
18
|
+
const upgrades = (status === 303 && method !== "GET" && method !== "HEAD") ||
|
|
19
|
+
((status === 301 || status === 302) && method === "POST");
|
|
20
|
+
return upgrades ? { method: "GET", body: undefined } : { method, body };
|
|
21
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** A file in a `multipart` body */
|
|
2
|
+
export interface FilePayload {
|
|
3
|
+
name: string;
|
|
4
|
+
/** @default "application/octet-stream" */
|
|
5
|
+
mimeType?: string;
|
|
6
|
+
buffer: Buffer;
|
|
7
|
+
}
|
|
8
|
+
type Scalar = string | number | boolean;
|
|
9
|
+
/** The body options of a request: at most one is set */
|
|
10
|
+
export interface BodyOptions {
|
|
11
|
+
/** Sent as it is (a string, a `Buffer`), or as JSON (anything else) */
|
|
12
|
+
data?: string | Buffer | object | number | boolean | null;
|
|
13
|
+
/** Sent as `application/x-www-form-urlencoded` */
|
|
14
|
+
form?: Record<string, Scalar> | URLSearchParams;
|
|
15
|
+
/** Sent as `multipart/form-data`: fields and files */
|
|
16
|
+
multipart?: Record<string, Scalar | FilePayload>;
|
|
17
|
+
}
|
|
18
|
+
export interface EncodedBody {
|
|
19
|
+
body: Buffer;
|
|
20
|
+
contentType: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The bytes of a request body and the `Content-Type` that goes with them (a header the caller sets wins over it).
|
|
24
|
+
* `undefined` when there is no body. Throws `fetch(): ...` when more than one of the options is set.
|
|
25
|
+
*/
|
|
26
|
+
export declare function encodeBody(options: BodyOptions): EncodedBody | undefined;
|
|
27
|
+
export {};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
function isFile(value) {
|
|
3
|
+
return (typeof value === "object" &&
|
|
4
|
+
value !== null &&
|
|
5
|
+
Buffer.isBuffer(value.buffer));
|
|
6
|
+
}
|
|
7
|
+
function isJson(text) {
|
|
8
|
+
try {
|
|
9
|
+
JSON.parse(text);
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
const quoted = (text) => text.replace(/["\r\n]/g, "_");
|
|
17
|
+
function multipartOf(parts) {
|
|
18
|
+
const boundary = `----samurai${randomBytes(12).toString("hex")}`;
|
|
19
|
+
const chunks = [];
|
|
20
|
+
for (const [name, value] of Object.entries(parts)) {
|
|
21
|
+
const file = isFile(value);
|
|
22
|
+
const head = file
|
|
23
|
+
? `Content-Disposition: form-data; name="${quoted(name)}"; filename="${quoted(value.name)}"\r\nContent-Type: ${value.mimeType ?? "application/octet-stream"}`
|
|
24
|
+
: `Content-Disposition: form-data; name="${quoted(name)}"`;
|
|
25
|
+
chunks.push(Buffer.from(`--${boundary}\r\n${head}\r\n\r\n`), file ? value.buffer : Buffer.from(String(value)), Buffer.from("\r\n"));
|
|
26
|
+
}
|
|
27
|
+
chunks.push(Buffer.from(`--${boundary}--\r\n`));
|
|
28
|
+
return {
|
|
29
|
+
body: Buffer.concat(chunks),
|
|
30
|
+
contentType: `multipart/form-data; boundary=${boundary}`,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The bytes of a request body and the `Content-Type` that goes with them (a header the caller sets wins over it).
|
|
35
|
+
* `undefined` when there is no body. Throws `fetch(): ...` when more than one of the options is set.
|
|
36
|
+
*/
|
|
37
|
+
export function encodeBody(options) {
|
|
38
|
+
const given = ["data", "form", "multipart"].filter((key) => options[key] !== undefined);
|
|
39
|
+
if (given.length > 1)
|
|
40
|
+
throw new Error(`fetch(): ${given.join(" and ")} cannot be used together, pick one`);
|
|
41
|
+
const { data, form, multipart } = options;
|
|
42
|
+
if (form !== undefined) {
|
|
43
|
+
const params = form instanceof URLSearchParams
|
|
44
|
+
? form
|
|
45
|
+
: new URLSearchParams(Object.entries(form).map(([key, value]) => [key, String(value)]));
|
|
46
|
+
return {
|
|
47
|
+
body: Buffer.from(params.toString()),
|
|
48
|
+
contentType: "application/x-www-form-urlencoded",
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
if (multipart !== undefined)
|
|
52
|
+
return multipartOf(multipart);
|
|
53
|
+
if (data === undefined)
|
|
54
|
+
return undefined;
|
|
55
|
+
if (typeof data === "string")
|
|
56
|
+
return {
|
|
57
|
+
body: Buffer.from(data),
|
|
58
|
+
contentType: isJson(data)
|
|
59
|
+
? "application/json"
|
|
60
|
+
: "application/octet-stream",
|
|
61
|
+
};
|
|
62
|
+
if (Buffer.isBuffer(data) || data instanceof Uint8Array)
|
|
63
|
+
return {
|
|
64
|
+
body: Buffer.from(data),
|
|
65
|
+
contentType: "application/octet-stream",
|
|
66
|
+
};
|
|
67
|
+
return {
|
|
68
|
+
body: Buffer.from(JSON.stringify(data)),
|
|
69
|
+
contentType: "application/json",
|
|
70
|
+
};
|
|
71
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { StorageState } from "../types/use.js";
|
|
2
|
+
import { APIRequestContext, type RequestContextOptions } from "./api-request-context.js";
|
|
3
|
+
/** Options of `request.newContext()` */
|
|
4
|
+
export interface NewRequestContextOptions extends RequestContextOptions {
|
|
5
|
+
/** Cookies to start with: a JSON file (relative to the project folder) or the object itself. Local storage is not used. */
|
|
6
|
+
storageState?: string | StorageState;
|
|
7
|
+
}
|
|
8
|
+
/** A request context with its own cookies: the same API as `page.request`, without a browser */
|
|
9
|
+
export declare function createRequestContext(options?: NewRequestContextOptions): APIRequestContext;
|
|
10
|
+
/**
|
|
11
|
+
* Creates `APIRequestContext`s that belong to no page: `request.newContext({ baseURL })`. Inside a test, the
|
|
12
|
+
* `request` fixture is one already.
|
|
13
|
+
*/
|
|
14
|
+
export declare const request: {
|
|
15
|
+
newContext: (options?: NewRequestContextOptions) => Promise<APIRequestContext>;
|
|
16
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { projectDir } from "../config/config.js";
|
|
3
|
+
import { loadStorageState } from "../config/storage-state-file.js";
|
|
4
|
+
import { APIRequestContext, } from "./api-request-context.js";
|
|
5
|
+
import { CookieJar } from "./cookie-jar.js";
|
|
6
|
+
/** A request context with its own cookies: the same API as `page.request`, without a browser */
|
|
7
|
+
export function createRequestContext(options = {}) {
|
|
8
|
+
const { storageState, ...rest } = options;
|
|
9
|
+
const state = storageState === undefined
|
|
10
|
+
? undefined
|
|
11
|
+
: loadStorageState(storageState, path.resolve(projectDir()), "newContext()");
|
|
12
|
+
return new APIRequestContext(rest, new CookieJar(state?.cookies));
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Creates `APIRequestContext`s that belong to no page: `request.newContext({ baseURL })`. Inside a test, the
|
|
16
|
+
* `request` fixture is one already.
|
|
17
|
+
*/
|
|
18
|
+
export const request = {
|
|
19
|
+
newContext: (options) => Promise.resolve(createRequestContext(options)),
|
|
20
|
+
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { SupportedBrowser } from "../types/browser.js";
|
|
2
|
-
import type { AllHookFixtures, RegisteredTestCase } from "../types/test.js";
|
|
2
|
+
import type { AllHookFixtures, RegisteredTestCase, Suite } from "../types/test.js";
|
|
3
3
|
import { type HookError } from "./hooks.js";
|
|
4
4
|
export interface AllHooksOptions {
|
|
5
5
|
/** The limit of each beforeAll / afterAll hook, in ms */
|
|
@@ -21,6 +21,12 @@ export declare class AllHooks {
|
|
|
21
21
|
ensure(test: RegisteredTestCase, browser: SupportedBrowser): Promise<void>;
|
|
22
22
|
/** Runs the `afterAll` hooks of every suite entered, last entered first, and returns the errors; a second call has nothing to do */
|
|
23
23
|
closeAll(): Promise<HookError[]>;
|
|
24
|
+
/**
|
|
25
|
+
* Runs the `afterAll` hooks of `root` and of the suites inside it that were entered, so the next test that arrives
|
|
26
|
+
* runs their `beforeAll` hooks again: a serial block that starts over. Returns the errors.
|
|
27
|
+
*/
|
|
28
|
+
closeWithin(root: Suite): Promise<HookError[]>;
|
|
29
|
+
private runAfter;
|
|
24
30
|
private enter;
|
|
25
31
|
private runBefore;
|
|
26
32
|
}
|
package/dist/runner/all-hooks.js
CHANGED
|
@@ -21,9 +21,25 @@ export class AllHooks {
|
|
|
21
21
|
}
|
|
22
22
|
/** Runs the `afterAll` hooks of every suite entered, last entered first, and returns the errors; a second call has nothing to do */
|
|
23
23
|
async closeAll() {
|
|
24
|
-
const errors = [];
|
|
25
24
|
const entered = this.entered.splice(0).reverse();
|
|
26
25
|
this.scopes.clear();
|
|
26
|
+
return this.runAfter(entered);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Runs the `afterAll` hooks of `root` and of the suites inside it that were entered, so the next test that arrives
|
|
30
|
+
* runs their `beforeAll` hooks again: a serial block that starts over. Returns the errors.
|
|
31
|
+
*/
|
|
32
|
+
async closeWithin(root) {
|
|
33
|
+
const inside = (scope) => suiteChain(scope.suite).includes(root);
|
|
34
|
+
const closing = this.entered.filter(inside).reverse();
|
|
35
|
+
for (const scope of closing) {
|
|
36
|
+
this.entered.splice(this.entered.indexOf(scope), 1);
|
|
37
|
+
this.scopes.get(scope.suite)?.delete(scope.browser);
|
|
38
|
+
}
|
|
39
|
+
return this.runAfter(closing);
|
|
40
|
+
}
|
|
41
|
+
async runAfter(entered) {
|
|
42
|
+
const errors = [];
|
|
27
43
|
for (const { suite, browser, ready } of entered) {
|
|
28
44
|
await ready.catch(() => undefined);
|
|
29
45
|
for (const hook of suite.afterAll ?? []) {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** What `testInfo.attach` takes besides the name */
|
|
2
|
+
export interface AttachInput {
|
|
3
|
+
body?: string | Buffer;
|
|
4
|
+
path?: string;
|
|
5
|
+
contentType?: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Writes one attachment into `dir` and returns what to list: a body is saved under the cleaned name, a path is
|
|
9
|
+
* copied under it keeping its extension. `used` holds the file names taken so far; a repeat gets -2, -3, ...
|
|
10
|
+
* String bodies are masked like the rest of a report.
|
|
11
|
+
*/
|
|
12
|
+
export declare function saveAttachment(dir: string, used: Set<string>, name: string, input: AttachInput): Promise<{
|
|
13
|
+
name: string;
|
|
14
|
+
contentType: string;
|
|
15
|
+
path: string;
|
|
16
|
+
}>;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { copyFile, mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { cleanName } from "../browser/evidence.js";
|
|
4
|
+
import { maskText } from "../config/mask.js";
|
|
5
|
+
/** The file types an attachment is known by; the rest is `application/octet-stream` */
|
|
6
|
+
const TYPES = {
|
|
7
|
+
".txt": "text/plain",
|
|
8
|
+
".log": "text/plain",
|
|
9
|
+
".html": "text/html",
|
|
10
|
+
".json": "application/json",
|
|
11
|
+
".csv": "text/csv",
|
|
12
|
+
".png": "image/png",
|
|
13
|
+
".jpg": "image/jpeg",
|
|
14
|
+
".jpeg": "image/jpeg",
|
|
15
|
+
".webm": "video/webm",
|
|
16
|
+
".pdf": "application/pdf",
|
|
17
|
+
".zip": "application/zip",
|
|
18
|
+
};
|
|
19
|
+
const OCTET = "application/octet-stream";
|
|
20
|
+
/** The extension (with its dot) that stands for `contentType`, else `fallback` */
|
|
21
|
+
function extensionOf(contentType, fallback) {
|
|
22
|
+
const found = Object.entries(TYPES).find(([, type]) => type === contentType);
|
|
23
|
+
return found ? found[0] : fallback;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Writes one attachment into `dir` and returns what to list: a body is saved under the cleaned name, a path is
|
|
27
|
+
* copied under it keeping its extension. `used` holds the file names taken so far; a repeat gets -2, -3, ...
|
|
28
|
+
* String bodies are masked like the rest of a report.
|
|
29
|
+
*/
|
|
30
|
+
export async function saveAttachment(dir, used, name, input) {
|
|
31
|
+
if (name === "")
|
|
32
|
+
throw new Error("attach(): name must not be empty");
|
|
33
|
+
const { body, path: from } = input;
|
|
34
|
+
if (body === undefined && from === undefined)
|
|
35
|
+
throw new Error("attach(): give either body or path");
|
|
36
|
+
if (body !== undefined && from !== undefined)
|
|
37
|
+
throw new Error("attach(): give either body or path, not both");
|
|
38
|
+
const fileExt = from !== undefined ? path.extname(from).toLowerCase() : "";
|
|
39
|
+
const contentType = input.contentType ??
|
|
40
|
+
(from !== undefined
|
|
41
|
+
? (TYPES[fileExt] ?? OCTET)
|
|
42
|
+
: typeof body === "string"
|
|
43
|
+
? "text/plain"
|
|
44
|
+
: OCTET);
|
|
45
|
+
const ext = fileExt !== ""
|
|
46
|
+
? fileExt
|
|
47
|
+
: extensionOf(contentType, typeof body === "string" ? ".txt" : ".bin");
|
|
48
|
+
const base = cleanName(name);
|
|
49
|
+
let unique = base;
|
|
50
|
+
for (let n = 2; used.has(unique); n++)
|
|
51
|
+
unique = `${base}-${n}`;
|
|
52
|
+
used.add(unique);
|
|
53
|
+
const file = path.join(dir, `${unique}${ext}`);
|
|
54
|
+
await mkdir(dir, { recursive: true });
|
|
55
|
+
if (from !== undefined)
|
|
56
|
+
await copyFile(from, file);
|
|
57
|
+
else
|
|
58
|
+
await writeFile(file, typeof body === "string" ? maskText(body) : body);
|
|
59
|
+
return { name, contentType, path: file };
|
|
60
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** What a function asks for in the destructuring of its first parameter */
|
|
2
|
+
export type Requested = {
|
|
3
|
+
kind: "names";
|
|
4
|
+
names: string[];
|
|
5
|
+
rest: boolean;
|
|
6
|
+
}
|
|
7
|
+
/** No destructuring pattern: the function takes the fixtures object whole, or none */
|
|
8
|
+
| {
|
|
9
|
+
kind: "opaque";
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* The fixture names a function destructures from its first parameter (`async ({ page, user }) => ...`), read
|
|
13
|
+
* from its source. A `...rest` entry asks for everything.
|
|
14
|
+
*/
|
|
15
|
+
export declare function requestedBy(fn: (...args: never[]) => unknown): Requested;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
const OPAQUE = { kind: "opaque" };
|
|
2
|
+
/** Where the `}` or `)` that closes the bracket at `from` is, skipping strings; -1 when there is none */
|
|
3
|
+
function closing(text, from) {
|
|
4
|
+
let depth = 0;
|
|
5
|
+
for (let i = from; i < text.length; i++) {
|
|
6
|
+
const c = text[i];
|
|
7
|
+
if (c === '"' || c === "'" || c === "`") {
|
|
8
|
+
i = endOfString(text, i);
|
|
9
|
+
if (i < 0)
|
|
10
|
+
return -1;
|
|
11
|
+
}
|
|
12
|
+
else if (c === "{" || c === "(" || c === "[")
|
|
13
|
+
depth++;
|
|
14
|
+
else if (c === "}" || c === ")" || c === "]") {
|
|
15
|
+
depth--;
|
|
16
|
+
if (depth === 0)
|
|
17
|
+
return i;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return -1;
|
|
21
|
+
}
|
|
22
|
+
function endOfString(text, from) {
|
|
23
|
+
const quote = text[from];
|
|
24
|
+
for (let i = from + 1; i < text.length; i++) {
|
|
25
|
+
if (text[i] === "\\")
|
|
26
|
+
i++;
|
|
27
|
+
else if (text[i] === quote)
|
|
28
|
+
return i;
|
|
29
|
+
}
|
|
30
|
+
return -1;
|
|
31
|
+
}
|
|
32
|
+
/** Splits at the commas that sit outside brackets and strings */
|
|
33
|
+
function topLevelParts(text) {
|
|
34
|
+
const parts = [];
|
|
35
|
+
let depth = 0;
|
|
36
|
+
let start = 0;
|
|
37
|
+
for (let i = 0; i < text.length; i++) {
|
|
38
|
+
const c = text[i];
|
|
39
|
+
if (c === '"' || c === "'" || c === "`") {
|
|
40
|
+
const end = endOfString(text, i);
|
|
41
|
+
if (end < 0)
|
|
42
|
+
break;
|
|
43
|
+
i = end;
|
|
44
|
+
}
|
|
45
|
+
else if (c === "{" || c === "(" || c === "[")
|
|
46
|
+
depth++;
|
|
47
|
+
else if (c === "}" || c === ")" || c === "]")
|
|
48
|
+
depth--;
|
|
49
|
+
else if (c === "," && depth === 0) {
|
|
50
|
+
parts.push(text.slice(start, i));
|
|
51
|
+
start = i + 1;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
parts.push(text.slice(start));
|
|
55
|
+
return parts.map((part) => part.trim()).filter((part) => part !== "");
|
|
56
|
+
}
|
|
57
|
+
/** The property a destructuring entry reads: `a`, `a: b`, `a = 1`, `"a-b": c`; undefined for a computed key */
|
|
58
|
+
function keyOf(entry) {
|
|
59
|
+
const quoted = /^(["'])(.*?)\1/.exec(entry);
|
|
60
|
+
if (quoted)
|
|
61
|
+
return quoted[2];
|
|
62
|
+
return /^[\w$]+/.exec(entry)?.[0];
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The fixture names a function destructures from its first parameter (`async ({ page, user }) => ...`), read
|
|
66
|
+
* from its source. A `...rest` entry asks for everything.
|
|
67
|
+
*/
|
|
68
|
+
export function requestedBy(fn) {
|
|
69
|
+
const text = Function.prototype.toString.call(fn);
|
|
70
|
+
if (/^(async\s+)?[\w$]+\s*=>/.test(text))
|
|
71
|
+
return OPAQUE;
|
|
72
|
+
const open = text.indexOf("(");
|
|
73
|
+
if (open < 0)
|
|
74
|
+
return OPAQUE;
|
|
75
|
+
let at = open + 1;
|
|
76
|
+
while (/\s/.test(text[at] ?? ""))
|
|
77
|
+
at++;
|
|
78
|
+
if (text[at] === ")")
|
|
79
|
+
return { kind: "names", names: [], rest: false };
|
|
80
|
+
if (text[at] !== "{")
|
|
81
|
+
return OPAQUE;
|
|
82
|
+
const end = closing(text, at);
|
|
83
|
+
if (end < 0)
|
|
84
|
+
return OPAQUE;
|
|
85
|
+
const names = [];
|
|
86
|
+
let rest = false;
|
|
87
|
+
for (const entry of topLevelParts(text.slice(at + 1, end))) {
|
|
88
|
+
const key = entry.startsWith("...") ? undefined : keyOf(entry);
|
|
89
|
+
if (key === undefined)
|
|
90
|
+
rest = true;
|
|
91
|
+
else
|
|
92
|
+
names.push(key);
|
|
93
|
+
}
|
|
94
|
+
return { kind: "names", names, rest };
|
|
95
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** The function of a fixture: sets the value up, calls `use(value)`, cleans up after `use` returns */
|
|
2
|
+
type FixtureSetup = (args: Record<string, unknown>, use: (value: unknown) => Promise<void>) => Promise<void>;
|
|
3
|
+
/** One definition of a fixture: the value or the setup function, what it depends on, whether every test gets it */
|
|
4
|
+
export interface FixtureDef {
|
|
5
|
+
name: string;
|
|
6
|
+
auto: boolean;
|
|
7
|
+
deps: string[];
|
|
8
|
+
setup?: FixtureSetup;
|
|
9
|
+
value?: unknown;
|
|
10
|
+
}
|
|
11
|
+
/** Every custom fixture by name; the last definition of a name wins, the ones before it are what it may depend on by its own name */
|
|
12
|
+
export type FixtureLayers = ReadonlyMap<string, readonly FixtureDef[]>;
|
|
13
|
+
/** The fixtures the runner builds for every test: a custom fixture of the same name overrides it */
|
|
14
|
+
export declare const BUILT_IN_FIXTURES: ReadonlySet<string>;
|
|
15
|
+
/** Where a dependency of `name` (at definition `level`) comes from: another definition, or the built-in fixture */
|
|
16
|
+
export declare function dependencyOf(layers: FixtureLayers, name: string, level: number, dep: string): {
|
|
17
|
+
name: string;
|
|
18
|
+
level: number;
|
|
19
|
+
} | "built-in" | undefined;
|
|
20
|
+
/** The fixtures after `test.extend(definitions)` on a test that has `base`; throws `extend(): ...` for a definition that cannot work */
|
|
21
|
+
export declare function extendLayers(base: FixtureLayers | undefined, definitions: unknown): FixtureLayers;
|
|
22
|
+
export {};
|