@itmetsam/samurai-framework 0.21.5 → 0.22.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/dist/api.d.ts +8 -3
- 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 +4 -4
- package/dist/browser/browser.js +11 -8
- package/dist/browser/chrome-launch.js +9 -1
- 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 +3 -1
- package/dist/browser/index.js +1 -1
- package/dist/browser/keyboard.d.ts +35 -0
- package/dist/browser/keyboard.js +83 -0
- package/dist/browser/mouse.d.ts +42 -0
- package/dist/browser/mouse.js +66 -0
- package/dist/browser/page-content.d.ts +4 -0
- package/dist/browser/page-content.js +12 -0
- package/dist/browser/page-events.d.ts +11 -0
- package/dist/browser/page-events.js +23 -0
- package/dist/browser/page-logging.d.ts +24 -0
- package/dist/browser/page-logging.js +43 -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-viewport.d.ts +21 -0
- package/dist/browser/page-viewport.js +33 -0
- package/dist/browser/page.d.ts +23 -16
- package/dist/browser/page.js +48 -71
- package/dist/browser/screenshot.d.ts +6 -0
- package/dist/browser/screenshot.js +14 -0
- package/dist/browser/storage-state.d.ts +0 -2
- package/dist/browser/storage-state.js +0 -20
- package/dist/browser/use-applier.d.ts +16 -5
- package/dist/browser/use-applier.js +44 -17
- package/dist/browser/use-commands.d.ts +36 -0
- package/dist/browser/use-commands.js +120 -0
- 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 +7 -0
- package/dist/config/fixture-use.d.ts +19 -0
- package/dist/config/fixture-use.js +44 -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 +10 -0
- package/dist/config/group-use.js +51 -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 +3 -1
- package/dist/config/use-options.js +74 -14
- 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/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/fixture-defs.d.ts +2 -1
- package/dist/runner/fixture-defs.js +52 -18
- package/dist/runner/fixture-meta.d.ts +20 -0
- package/dist/runner/fixture-meta.js +109 -0
- package/dist/runner/fixture-options.d.ts +8 -0
- package/dist/runner/fixture-options.js +35 -0
- package/dist/runner/fixture-outputs.d.ts +11 -0
- package/dist/runner/fixture-outputs.js +28 -0
- package/dist/runner/fixture-registry.d.ts +35 -0
- package/dist/runner/fixture-registry.js +73 -0
- package/dist/runner/fixture-scope.d.ts +39 -9
- package/dist/runner/fixture-scope.js +139 -70
- package/dist/runner/fixture-start.d.ts +27 -0
- package/dist/runner/fixture-start.js +76 -0
- package/dist/runner/fixture-use.d.ts +23 -0
- package/dist/runner/fixture-use.js +64 -0
- package/dist/runner/fixture-worker.d.ts +26 -0
- package/dist/runner/fixture-worker.js +46 -0
- package/dist/runner/fixtures.d.ts +4 -1
- package/dist/runner/fixtures.js +3 -2
- package/dist/runner/global-hooks.d.ts +13 -0
- package/dist/runner/global-hooks.js +66 -0
- 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/prepare-run.js +5 -0
- package/dist/runner/registry.js +21 -7
- package/dist/runner/reporter.d.ts +13 -1
- package/dist/runner/reporter.js +13 -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 +3 -0
- package/dist/runner/run-test.js +23 -3
- package/dist/runner/run.d.ts +8 -0
- package/dist/runner/run.js +10 -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/suite.d.ts +3 -3
- package/dist/runner/suite.js +4 -3
- package/dist/runner/test-function.d.ts +15 -3
- package/dist/runner/test-info.js +2 -1
- package/dist/runner/test-runner.d.ts +1 -0
- package/dist/runner/test-runner.js +67 -18
- package/dist/runner/use-options.d.ts +18 -4
- package/dist/runner/use-options.js +33 -6
- 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 +9 -1
- package/dist/runner/worker.d.ts +2 -0
- package/dist/runner/worker.js +7 -1
- package/dist/types/config.d.ts +117 -21
- package/dist/types/extend.d.ts +54 -2
- package/dist/types/test.d.ts +42 -0
- package/dist/types/use.d.ts +59 -7
- package/package.json +1 -1
- package/dist/browser/emulation.d.ts +0 -61
- package/dist/browser/emulation.js +0 -180
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ActionOptions } from "./actionability.js";
|
|
2
|
+
import type { ElementAccess } from "./element-access.js";
|
|
3
|
+
import type { CheckName } from "./element-state.js";
|
|
4
|
+
import { type Modifier } from "./key-map.js";
|
|
5
|
+
import { type Point, type PointerClickOptions } from "./pointer-actions.js";
|
|
6
|
+
export interface PointerOptions extends ActionOptions {
|
|
7
|
+
/** Where on the element to point: pixels from its top left corner (default: the centre) */
|
|
8
|
+
position?: Point;
|
|
9
|
+
/** Keys held down during the action */
|
|
10
|
+
modifiers?: Modifier[];
|
|
11
|
+
}
|
|
12
|
+
export interface ClickOptions extends PointerOptions, PointerClickOptions {
|
|
13
|
+
}
|
|
14
|
+
/** Waits until the element passes `checks` (scrolled into view) and answers where on screen to point */
|
|
15
|
+
export declare function pointAtElement(access: ElementAccess, method: string, checks: readonly CheckName[], options: PointerOptions | undefined): Promise<Point>;
|
|
16
|
+
/** Waits until the element is attached, visible, stable, enabled and not covered, then clicks it with real pointer input */
|
|
17
|
+
export declare function clickElement(access: ElementAccess, options?: ClickOptions, method?: string): Promise<void>;
|
|
18
|
+
/** Waits until the element is attached, visible, stable and not covered, then moves the mouse onto it */
|
|
19
|
+
export declare function hoverElement(access: ElementAccess, options?: PointerOptions): Promise<void>;
|
|
20
|
+
/** Waits like `click`, then touches the element with a finger (a touch pointer, not the mouse) */
|
|
21
|
+
export declare function tapElement(access: ElementAccess, options?: PointerOptions): Promise<void>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { withModifiers } from "./key-actions.js";
|
|
2
|
+
import { modifierCodes } from "./key-map.js";
|
|
3
|
+
import { checkClick, pointerClick, pointerMoveTo, pointerTap, pointInRect, } from "./pointer-actions.js";
|
|
4
|
+
import { CLICK_CHECKS, HOVER_CHECKS } from "./page-scripts.js";
|
|
5
|
+
/** Checks `position` and `modifiers`; `method` names the public method for the error */
|
|
6
|
+
function checkOptions(method, options) {
|
|
7
|
+
const position = options?.position;
|
|
8
|
+
if (position !== undefined &&
|
|
9
|
+
!(Number.isFinite(position.x) && Number.isFinite(position.y))) {
|
|
10
|
+
throw new Error(`${method}(): position must be { x, y } in pixels`);
|
|
11
|
+
}
|
|
12
|
+
return modifierCodes(options?.modifiers ?? [], undefined, method);
|
|
13
|
+
}
|
|
14
|
+
/** Waits until the element passes `checks` (scrolled into view) and answers where on screen to point */
|
|
15
|
+
export async function pointAtElement(access, method, checks, options) {
|
|
16
|
+
const state = await access.waitForActionable(method, checks, {
|
|
17
|
+
scroll: true,
|
|
18
|
+
hitTest: true,
|
|
19
|
+
...(options?.position && { at: options.position }),
|
|
20
|
+
}, options);
|
|
21
|
+
// "attached" is always required, so the box is present
|
|
22
|
+
return pointInRect(state.box, options?.position);
|
|
23
|
+
}
|
|
24
|
+
/** Waits until the element is attached, visible, stable, enabled and not covered, then clicks it with real pointer input */
|
|
25
|
+
export async function clickElement(access, options, method = "click") {
|
|
26
|
+
const modifiers = checkOptions(method, options);
|
|
27
|
+
checkClick(options ?? {}, method);
|
|
28
|
+
const point = await pointAtElement(access, method, CLICK_CHECKS, options);
|
|
29
|
+
await withModifiers(access.connector, access.contextId, modifiers, () => pointerClick(access.connector, access.contextId, point, options, method));
|
|
30
|
+
}
|
|
31
|
+
/** Waits until the element is attached, visible, stable and not covered, then moves the mouse onto it */
|
|
32
|
+
export async function hoverElement(access, options) {
|
|
33
|
+
const modifiers = checkOptions("hover", options);
|
|
34
|
+
const point = await pointAtElement(access, "hover", HOVER_CHECKS, options);
|
|
35
|
+
await withModifiers(access.connector, access.contextId, modifiers, () => pointerMoveTo(access.connector, access.contextId, point));
|
|
36
|
+
}
|
|
37
|
+
/** Waits like `click`, then touches the element with a finger (a touch pointer, not the mouse) */
|
|
38
|
+
export async function tapElement(access, options) {
|
|
39
|
+
const modifiers = checkOptions("tap", options);
|
|
40
|
+
const point = await pointAtElement(access, "tap", CLICK_CHECKS, options);
|
|
41
|
+
await withModifiers(access.connector, access.contextId, modifiers, () => pointerTap(access.connector, access.contextId, point));
|
|
42
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { APIResponse } from "./api-response.js";
|
|
2
|
+
import type { CookieStore } from "./cookie-jar.js";
|
|
3
|
+
import { type BodyOptions } from "./request-body.js";
|
|
4
|
+
import type { StorageState } from "../types/use.js";
|
|
5
|
+
/** Options of one request */
|
|
6
|
+
export interface RequestOptions extends BodyOptions {
|
|
7
|
+
/** Only for `fetch`. @default "GET" */
|
|
8
|
+
method?: string;
|
|
9
|
+
/** Added to the context's `extraHTTPHeaders`; a name set in both takes this value */
|
|
10
|
+
headers?: Record<string, string>;
|
|
11
|
+
/** Query parameters, added to the ones the URL has */
|
|
12
|
+
params?: Record<string, string | number | boolean> | URLSearchParams | string;
|
|
13
|
+
/** Milliseconds for the whole request, redirects included; 0 for no limit. @default 30000 */
|
|
14
|
+
timeout?: number;
|
|
15
|
+
/** Throw for a status outside 200 to 299 instead of returning the response */
|
|
16
|
+
failOnStatusCode?: boolean;
|
|
17
|
+
/** Accept certificates that are self-signed, expired or for another host. @default the context's setting */
|
|
18
|
+
ignoreHTTPSErrors?: boolean;
|
|
19
|
+
/** Redirects to follow; 0 returns the 3xx response itself. @default 20 */
|
|
20
|
+
maxRedirects?: number;
|
|
21
|
+
/** Times to send the request again when the connection is reset before an answer. @default 0 */
|
|
22
|
+
maxRetries?: number;
|
|
23
|
+
}
|
|
24
|
+
/** What a context starts from; `BrowserContext.request` hands its own object and changes it as the run's `use` options arrive */
|
|
25
|
+
export interface RequestContextOptions {
|
|
26
|
+
/** Relative request URLs resolve against it. @default the run's baseURL */
|
|
27
|
+
baseURL?: string | undefined;
|
|
28
|
+
/** Headers added to every request */
|
|
29
|
+
extraHTTPHeaders?: Record<string, string> | undefined;
|
|
30
|
+
/** Accept certificates that are self-signed, expired or for another host */
|
|
31
|
+
ignoreHTTPSErrors?: boolean | undefined;
|
|
32
|
+
}
|
|
33
|
+
/** A request given to `fetch` instead of a URL: anything with a `url` (such as a request the page made) */
|
|
34
|
+
export interface RequestLike {
|
|
35
|
+
url: string;
|
|
36
|
+
method?: string;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Makes HTTP requests from the test, outside the browser: log in, set data up, check an API. Cookies come from
|
|
40
|
+
* its `CookieStore`: the browser context's cookies (`page.request`, `context.request`) or its own jar.
|
|
41
|
+
*/
|
|
42
|
+
export declare class APIRequestContext {
|
|
43
|
+
private readonly options;
|
|
44
|
+
private readonly cookies;
|
|
45
|
+
private readonly pending;
|
|
46
|
+
private disposed;
|
|
47
|
+
constructor(options: RequestContextOptions, cookies: CookieStore);
|
|
48
|
+
get(url: string, options?: RequestOptions): Promise<APIResponse>;
|
|
49
|
+
post(url: string, options?: RequestOptions): Promise<APIResponse>;
|
|
50
|
+
put(url: string, options?: RequestOptions): Promise<APIResponse>;
|
|
51
|
+
patch(url: string, options?: RequestOptions): Promise<APIResponse>;
|
|
52
|
+
delete(url: string, options?: RequestOptions): Promise<APIResponse>;
|
|
53
|
+
head(url: string, options?: RequestOptions): Promise<APIResponse>;
|
|
54
|
+
/** Sends a request with any method and returns its response (redirects followed unless `maxRedirects` is 0) */
|
|
55
|
+
fetch(urlOrRequest: string | URL | RequestLike, options?: RequestOptions): Promise<APIResponse>;
|
|
56
|
+
/** The cookies this context holds, as a `storageState` file lists them (it has no local storage) */
|
|
57
|
+
storageState(): Promise<StorageState>;
|
|
58
|
+
/** Stops the requests in flight; the context sends no more */
|
|
59
|
+
dispose(): Promise<void>;
|
|
60
|
+
private send;
|
|
61
|
+
/** One hop, sent again when the connection is reset and `maxRetries` allows */
|
|
62
|
+
private hop;
|
|
63
|
+
private urlOf;
|
|
64
|
+
private headersOf;
|
|
65
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { RELATIVE_URL } from "../config/base-url.js";
|
|
2
|
+
import { peekRunSettings } from "../config/run-settings.js";
|
|
3
|
+
import { APIResponse } from "./api-response.js";
|
|
4
|
+
import { sendHop, TransportError, describeURL } from "./http-transport.js";
|
|
5
|
+
import { encodeBody } from "./request-body.js";
|
|
6
|
+
import { followUp, redirectTarget } from "./redirect.js";
|
|
7
|
+
/** Milliseconds a request may take, redirects included */
|
|
8
|
+
const DEFAULT_TIMEOUT = 30_000;
|
|
9
|
+
const DEFAULT_MAX_REDIRECTS = 20;
|
|
10
|
+
const HAS_SCHEME = /^[a-z][a-z\d+.-]*:/i;
|
|
11
|
+
/**
|
|
12
|
+
* Makes HTTP requests from the test, outside the browser: log in, set data up, check an API. Cookies come from
|
|
13
|
+
* its `CookieStore`: the browser context's cookies (`page.request`, `context.request`) or its own jar.
|
|
14
|
+
*/
|
|
15
|
+
export class APIRequestContext {
|
|
16
|
+
constructor(options, cookies) {
|
|
17
|
+
this.options = options;
|
|
18
|
+
this.cookies = cookies;
|
|
19
|
+
this.pending = new Set();
|
|
20
|
+
this.disposed = false;
|
|
21
|
+
}
|
|
22
|
+
get(url, options) {
|
|
23
|
+
return this.fetch(url, { ...options, method: "GET" });
|
|
24
|
+
}
|
|
25
|
+
post(url, options) {
|
|
26
|
+
return this.fetch(url, { ...options, method: "POST" });
|
|
27
|
+
}
|
|
28
|
+
put(url, options) {
|
|
29
|
+
return this.fetch(url, { ...options, method: "PUT" });
|
|
30
|
+
}
|
|
31
|
+
patch(url, options) {
|
|
32
|
+
return this.fetch(url, { ...options, method: "PATCH" });
|
|
33
|
+
}
|
|
34
|
+
delete(url, options) {
|
|
35
|
+
return this.fetch(url, { ...options, method: "DELETE" });
|
|
36
|
+
}
|
|
37
|
+
head(url, options) {
|
|
38
|
+
return this.fetch(url, { ...options, method: "HEAD" });
|
|
39
|
+
}
|
|
40
|
+
/** Sends a request with any method and returns its response (redirects followed unless `maxRedirects` is 0) */
|
|
41
|
+
async fetch(urlOrRequest, options = {}) {
|
|
42
|
+
if (this.disposed)
|
|
43
|
+
throw new Error("request context disposed");
|
|
44
|
+
const given = typeof urlOrRequest === "string" || urlOrRequest instanceof URL
|
|
45
|
+
? { url: String(urlOrRequest) }
|
|
46
|
+
: urlOrRequest;
|
|
47
|
+
const method = (options.method ?? given.method ?? "GET").toUpperCase();
|
|
48
|
+
const url = this.urlOf(given.url, options.params);
|
|
49
|
+
const encoded = encodeBody(options);
|
|
50
|
+
const headers = this.headersOf(options.headers);
|
|
51
|
+
if (encoded && !("content-type" in headers))
|
|
52
|
+
headers["content-type"] = encoded.contentType;
|
|
53
|
+
const abort = new AbortController();
|
|
54
|
+
this.pending.add(abort);
|
|
55
|
+
try {
|
|
56
|
+
return await this.send({
|
|
57
|
+
url,
|
|
58
|
+
method,
|
|
59
|
+
headers,
|
|
60
|
+
body: encoded?.body,
|
|
61
|
+
options,
|
|
62
|
+
abort,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
finally {
|
|
66
|
+
this.pending.delete(abort);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/** The cookies this context holds, as a `storageState` file lists them (it has no local storage) */
|
|
70
|
+
async storageState() {
|
|
71
|
+
return { cookies: await this.cookies.list(), origins: [] };
|
|
72
|
+
}
|
|
73
|
+
/** Stops the requests in flight; the context sends no more */
|
|
74
|
+
dispose() {
|
|
75
|
+
this.disposed = true;
|
|
76
|
+
for (const abort of this.pending)
|
|
77
|
+
abort.abort();
|
|
78
|
+
this.pending.clear();
|
|
79
|
+
return Promise.resolve();
|
|
80
|
+
}
|
|
81
|
+
async send(plan) {
|
|
82
|
+
const { options, abort } = plan;
|
|
83
|
+
const timeout = options.timeout ?? DEFAULT_TIMEOUT;
|
|
84
|
+
const deadline = timeout > 0 ? Date.now() + timeout : 0;
|
|
85
|
+
const maxRedirects = options.maxRedirects ?? DEFAULT_MAX_REDIRECTS;
|
|
86
|
+
let { url, method, headers, body } = plan;
|
|
87
|
+
for (let hops = 0;; hops++) {
|
|
88
|
+
const sent = { ...headers };
|
|
89
|
+
const cookie = await this.cookies.cookieHeader(url);
|
|
90
|
+
if (cookie)
|
|
91
|
+
sent.cookie = sent.cookie ? `${sent.cookie}; ${cookie}` : cookie;
|
|
92
|
+
const remaining = deadline > 0 ? Math.max(1, deadline - Date.now()) : 0;
|
|
93
|
+
const hop = await this.hop({ url, method, headers: sent, body }, options, remaining, abort.signal);
|
|
94
|
+
if (hop.setCookies.length > 0)
|
|
95
|
+
await this.cookies.store(url, hop.setCookies);
|
|
96
|
+
const location = hop.headers.find((h) => h.name.toLowerCase() === "location")?.value;
|
|
97
|
+
const next = redirectTarget(hop.status, location, url);
|
|
98
|
+
if (next && maxRedirects > 0) {
|
|
99
|
+
if (hops >= maxRedirects)
|
|
100
|
+
throw new Error(`fetch(): too many redirects, more than ${maxRedirects} (${method} ${describeURL(plan.url)})`);
|
|
101
|
+
const change = followUp(hop.status, method, body);
|
|
102
|
+
if (change.body === undefined && body !== undefined)
|
|
103
|
+
headers = withoutBody(headers);
|
|
104
|
+
if (next.origin !== url.origin)
|
|
105
|
+
delete headers.authorization;
|
|
106
|
+
({ method, body } = change);
|
|
107
|
+
url = next;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
const response = new APIResponse({
|
|
111
|
+
url: url.href,
|
|
112
|
+
status: hop.status,
|
|
113
|
+
statusText: hop.statusText,
|
|
114
|
+
headers: hop.headers,
|
|
115
|
+
body: hop.body,
|
|
116
|
+
});
|
|
117
|
+
if (options.failOnStatusCode && !response.ok())
|
|
118
|
+
throw new Error(`fetch(): ${hop.status} ${hop.statusText} (${method} ${describeURL(url)})`);
|
|
119
|
+
return response;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/** One hop, sent again when the connection is reset and `maxRetries` allows */
|
|
123
|
+
async hop(request, options, timeout, signal) {
|
|
124
|
+
const retries = options.maxRetries ?? 0;
|
|
125
|
+
for (let attempt = 0;; attempt++) {
|
|
126
|
+
try {
|
|
127
|
+
return await sendHop({
|
|
128
|
+
...request,
|
|
129
|
+
ignoreHTTPSErrors: options.ignoreHTTPSErrors ??
|
|
130
|
+
this.options.ignoreHTTPSErrors ??
|
|
131
|
+
false,
|
|
132
|
+
timeout,
|
|
133
|
+
signal,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
catch (err) {
|
|
137
|
+
const again = err instanceof TransportError &&
|
|
138
|
+
err.code === "ECONNRESET" &&
|
|
139
|
+
attempt < retries &&
|
|
140
|
+
!signal.aborted;
|
|
141
|
+
if (!again)
|
|
142
|
+
throw prefixed(err);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
urlOf(given, params) {
|
|
147
|
+
let url;
|
|
148
|
+
try {
|
|
149
|
+
if (HAS_SCHEME.test(given) && !RELATIVE_URL.test(given))
|
|
150
|
+
url = new URL(given);
|
|
151
|
+
else {
|
|
152
|
+
const base = this.options.baseURL ?? peekRunSettings()?.baseURL;
|
|
153
|
+
if (!base)
|
|
154
|
+
throw new Error(`fetch(): "${given}" needs a baseURL; set one in samurai.config.ts, the environment or request.newContext()`);
|
|
155
|
+
url = new URL(given, base);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
catch (err) {
|
|
159
|
+
if (err instanceof Error && err.message.startsWith("fetch():"))
|
|
160
|
+
throw err;
|
|
161
|
+
throw new Error(`fetch(): "${given}" is not a valid URL`);
|
|
162
|
+
}
|
|
163
|
+
if (url.protocol !== "http:" && url.protocol !== "https:")
|
|
164
|
+
throw new Error(`fetch(): "${given}" must be an http or https URL`);
|
|
165
|
+
if (params !== undefined) {
|
|
166
|
+
if (typeof params === "object" && !(params instanceof URLSearchParams)) {
|
|
167
|
+
for (const [key, value] of Object.entries(params))
|
|
168
|
+
url.searchParams.set(key, String(value));
|
|
169
|
+
}
|
|
170
|
+
else
|
|
171
|
+
for (const [key, value] of new URLSearchParams(params))
|
|
172
|
+
url.searchParams.append(key, value);
|
|
173
|
+
}
|
|
174
|
+
return url;
|
|
175
|
+
}
|
|
176
|
+
headersOf(perCall) {
|
|
177
|
+
const headers = {
|
|
178
|
+
accept: "*/*",
|
|
179
|
+
"accept-encoding": "gzip,deflate,br",
|
|
180
|
+
"user-agent": "samurai-framework",
|
|
181
|
+
};
|
|
182
|
+
for (const source of [this.options.extraHTTPHeaders, perCall])
|
|
183
|
+
for (const [name, value] of Object.entries(source ?? {}))
|
|
184
|
+
headers[name.toLowerCase()] = String(value);
|
|
185
|
+
return headers;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
function withoutBody(headers) {
|
|
189
|
+
const rest = { ...headers };
|
|
190
|
+
delete rest["content-type"];
|
|
191
|
+
delete rest["content-length"];
|
|
192
|
+
return rest;
|
|
193
|
+
}
|
|
194
|
+
function prefixed(err) {
|
|
195
|
+
return err instanceof TransportError
|
|
196
|
+
? new Error(`fetch(): ${err.message}`)
|
|
197
|
+
: err;
|
|
198
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export interface HeaderEntry {
|
|
2
|
+
name: string;
|
|
3
|
+
value: string;
|
|
4
|
+
}
|
|
5
|
+
/** What a request context hands to `APIResponse` */
|
|
6
|
+
export interface ResponseInit {
|
|
7
|
+
url: string;
|
|
8
|
+
status: number;
|
|
9
|
+
statusText: string;
|
|
10
|
+
/** Every header as it arrived, in order, repeated names kept */
|
|
11
|
+
headers: HeaderEntry[];
|
|
12
|
+
body: Buffer;
|
|
13
|
+
}
|
|
14
|
+
/** The answer to a request made with an `APIRequestContext`. The body is already read. */
|
|
15
|
+
export declare class APIResponse {
|
|
16
|
+
private readonly init;
|
|
17
|
+
private disposed;
|
|
18
|
+
constructor(init: ResponseInit);
|
|
19
|
+
status(): number;
|
|
20
|
+
statusText(): string;
|
|
21
|
+
/** True for a status from 200 to 299 */
|
|
22
|
+
ok(): boolean;
|
|
23
|
+
/** The URL the response came from: the last one when redirects were followed */
|
|
24
|
+
url(): string;
|
|
25
|
+
/** Header names lower-cased; a name that came more than once is joined with ", " (`set-cookie` with a new line) */
|
|
26
|
+
headers(): Record<string, string>;
|
|
27
|
+
/** Every header as it arrived, repeated names included (each `Set-Cookie` is an entry of its own) */
|
|
28
|
+
headersArray(): HeaderEntry[];
|
|
29
|
+
body(): Promise<Buffer>;
|
|
30
|
+
text(): Promise<string>;
|
|
31
|
+
json<T = unknown>(): Promise<T>;
|
|
32
|
+
/** Frees the body: `body()`, `text()` and `json()` throw after it */
|
|
33
|
+
dispose(): Promise<void>;
|
|
34
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/** The answer to a request made with an `APIRequestContext`. The body is already read. */
|
|
2
|
+
export class APIResponse {
|
|
3
|
+
constructor(init) {
|
|
4
|
+
this.init = init;
|
|
5
|
+
this.disposed = false;
|
|
6
|
+
}
|
|
7
|
+
status() {
|
|
8
|
+
return this.init.status;
|
|
9
|
+
}
|
|
10
|
+
statusText() {
|
|
11
|
+
return this.init.statusText;
|
|
12
|
+
}
|
|
13
|
+
/** True for a status from 200 to 299 */
|
|
14
|
+
ok() {
|
|
15
|
+
return this.init.status >= 200 && this.init.status < 300;
|
|
16
|
+
}
|
|
17
|
+
/** The URL the response came from: the last one when redirects were followed */
|
|
18
|
+
url() {
|
|
19
|
+
return this.init.url;
|
|
20
|
+
}
|
|
21
|
+
/** Header names lower-cased; a name that came more than once is joined with ", " (`set-cookie` with a new line) */
|
|
22
|
+
headers() {
|
|
23
|
+
const joined = {};
|
|
24
|
+
for (const { name, value } of this.init.headers) {
|
|
25
|
+
const key = name.toLowerCase();
|
|
26
|
+
const glue = key === "set-cookie" ? "\n" : ", ";
|
|
27
|
+
joined[key] = key in joined ? `${joined[key]}${glue}${value}` : value;
|
|
28
|
+
}
|
|
29
|
+
return joined;
|
|
30
|
+
}
|
|
31
|
+
/** Every header as it arrived, repeated names included (each `Set-Cookie` is an entry of its own) */
|
|
32
|
+
headersArray() {
|
|
33
|
+
return this.init.headers.map(({ name, value }) => ({ name, value }));
|
|
34
|
+
}
|
|
35
|
+
body() {
|
|
36
|
+
if (this.disposed)
|
|
37
|
+
return Promise.reject(new Error("response disposed"));
|
|
38
|
+
return Promise.resolve(this.init.body);
|
|
39
|
+
}
|
|
40
|
+
async text() {
|
|
41
|
+
return (await this.body()).toString("utf8");
|
|
42
|
+
}
|
|
43
|
+
async json() {
|
|
44
|
+
const text = await this.text();
|
|
45
|
+
try {
|
|
46
|
+
return JSON.parse(text);
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
throw new Error(`json(): the response body of ${this.init.url} is not JSON: ${err.message}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/** Frees the body: `body()`, `text()` and `json()` throw after it */
|
|
53
|
+
dispose() {
|
|
54
|
+
this.disposed = true;
|
|
55
|
+
return Promise.resolve();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { StorageStateCookie } from "../types/use.js";
|
|
2
|
+
/** A cookie as the jar keeps it */
|
|
3
|
+
export interface StoredCookie {
|
|
4
|
+
name: string;
|
|
5
|
+
value: string;
|
|
6
|
+
/** Lower case, no leading dot */
|
|
7
|
+
domain: string;
|
|
8
|
+
/** True when no `Domain` was given: only that exact host gets the cookie */
|
|
9
|
+
hostOnly: boolean;
|
|
10
|
+
path: string;
|
|
11
|
+
secure: boolean;
|
|
12
|
+
httpOnly: boolean;
|
|
13
|
+
sameSite?: "Strict" | "Lax" | "None";
|
|
14
|
+
/** Seconds since the epoch; absent for a session cookie, in the past for a cookie the site deletes */
|
|
15
|
+
expires?: number;
|
|
16
|
+
}
|
|
17
|
+
/** Where a request context reads the cookies it sends and writes the ones it receives: its own jar, or a browser context */
|
|
18
|
+
export interface CookieStore {
|
|
19
|
+
/** The `Cookie` header value for a request to `url`; `undefined` when no cookie applies */
|
|
20
|
+
cookieHeader(url: URL): Promise<string | undefined>;
|
|
21
|
+
/** Stores the cookies of the `Set-Cookie` headers a response from `url` carried (one entry per header) */
|
|
22
|
+
store(url: URL, setCookies: string[]): Promise<void>;
|
|
23
|
+
/** Every cookie, as a storage state lists them */
|
|
24
|
+
list(): Promise<StorageStateCookie[]>;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* One `Set-Cookie` header of a response from `url`. `undefined` for a header that is not a cookie (no name, or a
|
|
28
|
+
* `Domain` the host does not belong to, which a browser would refuse as well).
|
|
29
|
+
*/
|
|
30
|
+
export declare function parseSetCookie(header: string, url: URL, now?: number): StoredCookie | undefined;
|
|
31
|
+
export declare function isExpired(cookie: StoredCookie, now?: number): boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Whether a request to `url` carries `cookie`: host, path, `Secure` (https, or a loopback host the browsers treat
|
|
34
|
+
* as secure) and expiry. `SameSite` is not applied: a request made by the test is not made from another site.
|
|
35
|
+
*/
|
|
36
|
+
export declare function cookieMatches(cookie: StoredCookie, url: URL, now?: number): boolean;
|
|
37
|
+
/** The `Cookie` header value for `cookies`: the longest path first, as a browser sends them */
|
|
38
|
+
export declare function cookieHeaderOf(cookies: StoredCookie[]): string | undefined;
|
|
39
|
+
/** The cookies of an in-memory request context: what its responses set, and what a storage state seeded */
|
|
40
|
+
export declare class CookieJar implements CookieStore {
|
|
41
|
+
private cookies;
|
|
42
|
+
constructor(seed?: readonly StorageStateCookie[]);
|
|
43
|
+
cookieHeader(url: URL): Promise<string | undefined>;
|
|
44
|
+
store(url: URL, setCookies: string[]): Promise<void>;
|
|
45
|
+
list(): Promise<StorageStateCookie[]>;
|
|
46
|
+
/** Replaces the cookie with the same name, domain and path; an expired one only removes it */
|
|
47
|
+
private put;
|
|
48
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
const SAME_SITE = { strict: "Strict", lax: "Lax", none: "None" };
|
|
2
|
+
const INTEGER = /^-?\d+$/;
|
|
3
|
+
function isLoopback(host) {
|
|
4
|
+
return (host === "localhost" || host === "[::1]" || /^127(\.\d+){3}$/.test(host));
|
|
5
|
+
}
|
|
6
|
+
/** The default path of a cookie set without `Path`: the directory of the request path */
|
|
7
|
+
function defaultPath(pathname) {
|
|
8
|
+
const last = pathname.lastIndexOf("/");
|
|
9
|
+
return pathname.startsWith("/") && last > 0 ? pathname.slice(0, last) : "/";
|
|
10
|
+
}
|
|
11
|
+
function domainMatches(host, domain) {
|
|
12
|
+
return host === domain || host.endsWith(`.${domain}`);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* One `Set-Cookie` header of a response from `url`. `undefined` for a header that is not a cookie (no name, or a
|
|
16
|
+
* `Domain` the host does not belong to, which a browser would refuse as well).
|
|
17
|
+
*/
|
|
18
|
+
export function parseSetCookie(header, url, now = Date.now()) {
|
|
19
|
+
const [pair = "", ...attributes] = header.split(";");
|
|
20
|
+
const eq = pair.indexOf("=");
|
|
21
|
+
if (eq < 0)
|
|
22
|
+
return undefined;
|
|
23
|
+
const name = pair.slice(0, eq).trim();
|
|
24
|
+
if (name === "")
|
|
25
|
+
return undefined;
|
|
26
|
+
const cookie = {
|
|
27
|
+
name,
|
|
28
|
+
value: pair.slice(eq + 1).trim(),
|
|
29
|
+
domain: url.hostname.toLowerCase(),
|
|
30
|
+
hostOnly: true,
|
|
31
|
+
path: defaultPath(url.pathname),
|
|
32
|
+
secure: false,
|
|
33
|
+
httpOnly: false,
|
|
34
|
+
};
|
|
35
|
+
let maxAge;
|
|
36
|
+
for (const attribute of attributes) {
|
|
37
|
+
const at = attribute.indexOf("=");
|
|
38
|
+
const key = (at < 0 ? attribute : attribute.slice(0, at))
|
|
39
|
+
.trim()
|
|
40
|
+
.toLowerCase();
|
|
41
|
+
const value = at < 0 ? "" : attribute.slice(at + 1).trim();
|
|
42
|
+
if (key === "domain" && value !== "") {
|
|
43
|
+
const domain = value.replace(/^\./, "").toLowerCase();
|
|
44
|
+
if (!domainMatches(cookie.domain, domain))
|
|
45
|
+
return undefined;
|
|
46
|
+
cookie.domain = domain;
|
|
47
|
+
cookie.hostOnly = false;
|
|
48
|
+
}
|
|
49
|
+
else if (key === "path" && value.startsWith("/"))
|
|
50
|
+
cookie.path = value;
|
|
51
|
+
else if (key === "secure")
|
|
52
|
+
cookie.secure = true;
|
|
53
|
+
else if (key === "httponly")
|
|
54
|
+
cookie.httpOnly = true;
|
|
55
|
+
else if (key === "samesite" && value.toLowerCase() in SAME_SITE)
|
|
56
|
+
cookie.sameSite =
|
|
57
|
+
SAME_SITE[value.toLowerCase()];
|
|
58
|
+
else if (key === "max-age" && INTEGER.test(value))
|
|
59
|
+
maxAge = Number(value);
|
|
60
|
+
else if (key === "expires") {
|
|
61
|
+
const time = Date.parse(value);
|
|
62
|
+
if (!Number.isNaN(time))
|
|
63
|
+
cookie.expires = Math.floor(time / 1000);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// Max-Age wins over Expires
|
|
67
|
+
if (maxAge !== undefined)
|
|
68
|
+
cookie.expires = maxAge <= 0 ? 0 : Math.floor(now / 1000) + maxAge;
|
|
69
|
+
return cookie;
|
|
70
|
+
}
|
|
71
|
+
export function isExpired(cookie, now = Date.now()) {
|
|
72
|
+
return cookie.expires !== undefined && cookie.expires * 1000 <= now;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Whether a request to `url` carries `cookie`: host, path, `Secure` (https, or a loopback host the browsers treat
|
|
76
|
+
* as secure) and expiry. `SameSite` is not applied: a request made by the test is not made from another site.
|
|
77
|
+
*/
|
|
78
|
+
export function cookieMatches(cookie, url, now = Date.now()) {
|
|
79
|
+
if (isExpired(cookie, now))
|
|
80
|
+
return false;
|
|
81
|
+
const host = url.hostname.toLowerCase();
|
|
82
|
+
if (cookie.hostOnly
|
|
83
|
+
? host !== cookie.domain
|
|
84
|
+
: !domainMatches(host, cookie.domain))
|
|
85
|
+
return false;
|
|
86
|
+
const { path } = cookie;
|
|
87
|
+
const here = url.pathname || "/";
|
|
88
|
+
const underPath = here === path ||
|
|
89
|
+
(here.startsWith(path) &&
|
|
90
|
+
(path.endsWith("/") || here[path.length] === "/"));
|
|
91
|
+
if (!underPath)
|
|
92
|
+
return false;
|
|
93
|
+
return !cookie.secure || url.protocol === "https:" || isLoopback(host);
|
|
94
|
+
}
|
|
95
|
+
/** The `Cookie` header value for `cookies`: the longest path first, as a browser sends them */
|
|
96
|
+
export function cookieHeaderOf(cookies) {
|
|
97
|
+
if (cookies.length === 0)
|
|
98
|
+
return undefined;
|
|
99
|
+
return [...cookies]
|
|
100
|
+
.sort((a, b) => b.path.length - a.path.length)
|
|
101
|
+
.map((cookie) => `${cookie.name}=${cookie.value}`)
|
|
102
|
+
.join("; ");
|
|
103
|
+
}
|
|
104
|
+
/** The cookies of an in-memory request context: what its responses set, and what a storage state seeded */
|
|
105
|
+
export class CookieJar {
|
|
106
|
+
constructor(seed = []) {
|
|
107
|
+
this.cookies = [];
|
|
108
|
+
for (const cookie of seed)
|
|
109
|
+
this.put(fromStorageState(cookie));
|
|
110
|
+
}
|
|
111
|
+
cookieHeader(url) {
|
|
112
|
+
const now = Date.now();
|
|
113
|
+
this.cookies = this.cookies.filter((cookie) => !isExpired(cookie, now));
|
|
114
|
+
return Promise.resolve(cookieHeaderOf(this.cookies.filter((c) => cookieMatches(c, url, now))));
|
|
115
|
+
}
|
|
116
|
+
store(url, setCookies) {
|
|
117
|
+
for (const header of setCookies) {
|
|
118
|
+
const cookie = parseSetCookie(header, url);
|
|
119
|
+
if (cookie)
|
|
120
|
+
this.put(cookie);
|
|
121
|
+
}
|
|
122
|
+
return Promise.resolve();
|
|
123
|
+
}
|
|
124
|
+
list() {
|
|
125
|
+
const now = Date.now();
|
|
126
|
+
return Promise.resolve(this.cookies
|
|
127
|
+
.filter((cookie) => !isExpired(cookie, now))
|
|
128
|
+
.map(toStorageState));
|
|
129
|
+
}
|
|
130
|
+
/** Replaces the cookie with the same name, domain and path; an expired one only removes it */
|
|
131
|
+
put(cookie) {
|
|
132
|
+
this.cookies = this.cookies.filter((old) => old.name !== cookie.name ||
|
|
133
|
+
old.domain !== cookie.domain ||
|
|
134
|
+
old.path !== cookie.path);
|
|
135
|
+
if (!isExpired(cookie))
|
|
136
|
+
this.cookies.push(cookie);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function fromStorageState(cookie) {
|
|
140
|
+
return {
|
|
141
|
+
name: cookie.name,
|
|
142
|
+
value: cookie.value,
|
|
143
|
+
domain: cookie.domain.replace(/^\./, "").toLowerCase(),
|
|
144
|
+
hostOnly: !cookie.domain.startsWith("."),
|
|
145
|
+
path: cookie.path ?? "/",
|
|
146
|
+
secure: cookie.secure ?? false,
|
|
147
|
+
httpOnly: cookie.httpOnly ?? false,
|
|
148
|
+
...(cookie.sameSite !== undefined && { sameSite: cookie.sameSite }),
|
|
149
|
+
...(cookie.expires !== undefined &&
|
|
150
|
+
cookie.expires > 0 && { expires: cookie.expires }),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
function toStorageState(cookie) {
|
|
154
|
+
return {
|
|
155
|
+
name: cookie.name,
|
|
156
|
+
value: cookie.value,
|
|
157
|
+
domain: cookie.hostOnly ? cookie.domain : `.${cookie.domain}`,
|
|
158
|
+
path: cookie.path,
|
|
159
|
+
expires: cookie.expires ?? -1,
|
|
160
|
+
httpOnly: cookie.httpOnly,
|
|
161
|
+
secure: cookie.secure,
|
|
162
|
+
sameSite: cookie.sameSite ?? "Lax",
|
|
163
|
+
};
|
|
164
|
+
}
|
|
@@ -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>;
|