slicetest 0.6.1 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +177 -24
- package/dist/cli.js +66 -2
- package/dist/config.d.ts +18 -0
- package/dist/config.js +13 -0
- package/dist/connection.d.ts +5 -0
- package/dist/connection.js +31 -0
- package/dist/db.d.ts +12 -1
- package/dist/db.js +33 -6
- package/dist/diagram.d.ts +13 -0
- package/dist/diagram.js +89 -0
- package/dist/form.d.ts +7 -2
- package/dist/form.js +72 -7
- package/dist/global-setup.d.ts +5 -0
- package/dist/global-setup.js +74 -9
- package/dist/graphql.d.ts +34 -0
- package/dist/graphql.js +56 -0
- package/dist/har.d.ts +49 -0
- package/dist/har.js +107 -0
- package/dist/http.d.ts +21 -3
- package/dist/http.js +47 -10
- package/dist/index.d.ts +5 -3
- package/dist/init.js +184 -15
- package/dist/list.d.ts +26 -0
- package/dist/list.js +71 -0
- package/dist/matchers.d.ts +16 -0
- package/dist/matchers.js +81 -1
- package/dist/openapi.d.ts +21 -0
- package/dist/openapi.js +32 -1
- package/dist/provided.d.ts +1 -0
- package/dist/record.d.ts +1 -1
- package/dist/record.js +3 -1
- package/dist/recording.d.ts +1 -1
- package/dist/recording.js +2 -2
- package/dist/runtime.d.ts +12 -5
- package/dist/runtime.js +78 -22
- package/dist/scenario.d.ts +14 -4
- package/dist/scenario.js +36 -17
- package/dist/schema.d.ts +5 -0
- package/dist/schema.js +73 -0
- package/dist/stub.d.ts +63 -0
- package/dist/stub.js +292 -5
- package/dist/timeline.d.ts +9 -0
- package/dist/timeline.js +6 -0
- package/dist/webhook.d.ts +16 -7
- package/dist/webhook.js +47 -7
- package/dist/yaml-runtime.d.ts +3 -2
- package/dist/yaml-runtime.js +308 -51
- package/dist/yaml.d.ts +82 -6
- package/dist/yaml.js +187 -21
- package/package.json +2 -1
- package/schema/scenario.schema.json +640 -14
package/dist/graphql.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GraphQL over HTTP, as clients send it (Apollo, urql, graphql-request, gql, …):
|
|
3
|
+
* a POST with `{ query, variables, operationName }`, or a GET with those as query
|
|
4
|
+
* parameters. Stubs match calls by operation, and the app's own GraphQL endpoint
|
|
5
|
+
* is called with `http.graphql()`.
|
|
6
|
+
*/
|
|
7
|
+
const OPERATION = /(?:^|[\s}])(query|mutation|subscription)\b\s*([_A-Za-z][_0-9A-Za-z]*)?/g;
|
|
8
|
+
/** The operations a document defines, in order. Comments and strings don't count. */
|
|
9
|
+
export function operations(document) {
|
|
10
|
+
const code = document.replace(/"""[\s\S]*?"""|"(?:[^"\\\n]|\\.)*"|#[^\n]*/g, " ");
|
|
11
|
+
const found = [];
|
|
12
|
+
for (const m of code.matchAll(OPERATION))
|
|
13
|
+
found.push({ type: m[1], name: m[2] });
|
|
14
|
+
// `{ viewer { id } }` without a keyword is an anonymous query.
|
|
15
|
+
if (found.length === 0 && /^\s*\{/.test(code))
|
|
16
|
+
found.push({ type: "query" });
|
|
17
|
+
return found;
|
|
18
|
+
}
|
|
19
|
+
/** The GraphQL request in an HTTP call, or undefined when it isn't one. */
|
|
20
|
+
export function graphqlOf(call) {
|
|
21
|
+
let doc, name, variables;
|
|
22
|
+
if (call.method === "GET") {
|
|
23
|
+
doc = call.query.get("query") ?? undefined;
|
|
24
|
+
name = call.query.get("operationName") ?? undefined;
|
|
25
|
+
const v = call.query.get("variables");
|
|
26
|
+
try {
|
|
27
|
+
variables = v ? JSON.parse(v) : undefined;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
variables = undefined;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
else if (call.json && typeof call.json === "object" && !Array.isArray(call.json)) {
|
|
34
|
+
({ query: doc, operationName: name, variables } = call.json);
|
|
35
|
+
}
|
|
36
|
+
if (typeof doc !== "string")
|
|
37
|
+
return undefined;
|
|
38
|
+
const ops = operations(doc);
|
|
39
|
+
const op = typeof name === "string" && name ? (ops.find((o) => o.name === name) ?? { type: "query", name }) : ops[0];
|
|
40
|
+
if (!op)
|
|
41
|
+
return undefined;
|
|
42
|
+
return {
|
|
43
|
+
operation: typeof name === "string" && name ? name : op.name,
|
|
44
|
+
type: op.type,
|
|
45
|
+
query: doc,
|
|
46
|
+
variables: variables && typeof variables === "object" ? variables : {},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/** A GraphQL error list from messages or full error objects. */
|
|
50
|
+
export function graphqlErrors(errors) {
|
|
51
|
+
return errors.map((e) => (typeof e === "string" ? { message: e } : e));
|
|
52
|
+
}
|
|
53
|
+
/** `GraphQL mutation CreateIssue`, for failure messages. */
|
|
54
|
+
export function describeGraphQL(g) {
|
|
55
|
+
return `GraphQL ${g.type} ${g.operation ?? "(anonymous)"}`;
|
|
56
|
+
}
|
package/dist/har.d.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { type Recording } from "./recording.js";
|
|
2
|
+
/**
|
|
3
|
+
* `slicetest import session.har`: turn a HAR file (saved from the browser's
|
|
4
|
+
* network panel, Charles, mitmproxy, Proxyman, Postman, …) into recordings a
|
|
5
|
+
* stub replays, without running the app against the real service first.
|
|
6
|
+
*/
|
|
7
|
+
interface HarEntry {
|
|
8
|
+
request: {
|
|
9
|
+
method: string;
|
|
10
|
+
url: string;
|
|
11
|
+
postData?: {
|
|
12
|
+
mimeType?: string;
|
|
13
|
+
text?: string;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
response: {
|
|
17
|
+
status: number;
|
|
18
|
+
headers?: {
|
|
19
|
+
name: string;
|
|
20
|
+
value: string;
|
|
21
|
+
}[];
|
|
22
|
+
content?: {
|
|
23
|
+
mimeType?: string;
|
|
24
|
+
text?: string;
|
|
25
|
+
encoding?: string;
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export interface HarTarget {
|
|
30
|
+
/** Stub name. */
|
|
31
|
+
name: string;
|
|
32
|
+
/** The real service's base URL; entries under it become recordings, with this prefix removed from their path. */
|
|
33
|
+
upstream: string;
|
|
34
|
+
/** Recordings file, absolute. */
|
|
35
|
+
file: string;
|
|
36
|
+
}
|
|
37
|
+
export declare function harEntries(har: unknown, file?: string): HarEntry[];
|
|
38
|
+
/** The entry as a recording, if it is under `upstream` and replayable; otherwise why not. */
|
|
39
|
+
export declare function toRecording(entry: HarEntry, upstream: string): Recording | "elsewhere" | "skipped";
|
|
40
|
+
export declare function importHar(harFile: string, targets: HarTarget[]): Promise<{
|
|
41
|
+
written: {
|
|
42
|
+
name: string;
|
|
43
|
+
file: string;
|
|
44
|
+
count: number;
|
|
45
|
+
}[];
|
|
46
|
+
skipped: number;
|
|
47
|
+
others: [string, number][];
|
|
48
|
+
}>;
|
|
49
|
+
export {};
|
package/dist/har.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { mergeRecordings } from "./recording.js";
|
|
4
|
+
const KEPT_HEADERS = ["content-type", "location", "retry-after", "link", "etag"];
|
|
5
|
+
const TEXTUAL = /json|text|xml|javascript|x-www-form-urlencoded|graphql/i;
|
|
6
|
+
export function harEntries(har, file = "the HAR file") {
|
|
7
|
+
const entries = har?.log?.entries;
|
|
8
|
+
if (!Array.isArray(entries))
|
|
9
|
+
throw new Error(`slicetest import: ${file} is not a HAR file (no log.entries)`);
|
|
10
|
+
return entries;
|
|
11
|
+
}
|
|
12
|
+
/** The entry as a recording, if it is under `upstream` and replayable; otherwise why not. */
|
|
13
|
+
export function toRecording(entry, upstream) {
|
|
14
|
+
const base = new URL(upstream);
|
|
15
|
+
let url;
|
|
16
|
+
try {
|
|
17
|
+
url = new URL(entry.request.url);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return "skipped";
|
|
21
|
+
}
|
|
22
|
+
const prefix = base.pathname.replace(/\/$/, "");
|
|
23
|
+
if (url.host !== base.host || !(url.pathname === prefix || url.pathname.startsWith(`${prefix}/`)))
|
|
24
|
+
return "elsewhere";
|
|
25
|
+
const method = entry.request.method.toUpperCase();
|
|
26
|
+
// Preflights are the browser's; aborted and blocked requests (status 0) have no answer to replay.
|
|
27
|
+
if (method === "OPTIONS" || !entry.response || !entry.response.status)
|
|
28
|
+
return "skipped";
|
|
29
|
+
const query = Object.fromEntries([...url.searchParams.entries()].sort(([a], [b]) => a.localeCompare(b)));
|
|
30
|
+
const sent = entry.request.postData?.text;
|
|
31
|
+
const json = parse(sent);
|
|
32
|
+
const request = {
|
|
33
|
+
method,
|
|
34
|
+
path: url.pathname.slice(prefix.length) || "/",
|
|
35
|
+
...(Object.keys(query).length ? { query } : {}),
|
|
36
|
+
...(json !== undefined ? { json } : sent ? { body: sent } : {}),
|
|
37
|
+
};
|
|
38
|
+
const content = entry.response.content ?? {};
|
|
39
|
+
let text = content.text ?? "";
|
|
40
|
+
if (content.encoding === "base64" && text) {
|
|
41
|
+
if (!TEXTUAL.test(content.mimeType ?? ""))
|
|
42
|
+
return "skipped";
|
|
43
|
+
text = Buffer.from(text, "base64").toString("utf8");
|
|
44
|
+
}
|
|
45
|
+
const headers = Object.fromEntries((entry.response.headers ?? []).flatMap((h) => (KEPT_HEADERS.includes(h.name.toLowerCase()) ? [[h.name.toLowerCase(), h.value]] : [])));
|
|
46
|
+
const body = parse(text);
|
|
47
|
+
return {
|
|
48
|
+
request,
|
|
49
|
+
response: {
|
|
50
|
+
status: entry.response.status,
|
|
51
|
+
...(Object.keys(headers).length ? { headers } : {}),
|
|
52
|
+
...(body !== undefined ? { json: body } : text ? { body: text } : {}),
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
export async function importHar(harFile, targets) {
|
|
57
|
+
const text = await readFile(harFile, "utf8");
|
|
58
|
+
let har;
|
|
59
|
+
try {
|
|
60
|
+
har = JSON.parse(text);
|
|
61
|
+
}
|
|
62
|
+
catch (e) {
|
|
63
|
+
throw new Error(`slicetest import: ${harFile} is not JSON: ${e.message}`);
|
|
64
|
+
}
|
|
65
|
+
const entries = harEntries(har, harFile);
|
|
66
|
+
const written = [];
|
|
67
|
+
const claimed = new Set();
|
|
68
|
+
let skipped = 0;
|
|
69
|
+
for (const t of targets) {
|
|
70
|
+
const recordings = [];
|
|
71
|
+
for (const e of entries) {
|
|
72
|
+
const r = toRecording(e, t.upstream);
|
|
73
|
+
if (r === "elsewhere")
|
|
74
|
+
continue;
|
|
75
|
+
claimed.add(e);
|
|
76
|
+
if (r === "skipped")
|
|
77
|
+
skipped++;
|
|
78
|
+
else
|
|
79
|
+
recordings.push(r);
|
|
80
|
+
}
|
|
81
|
+
if (recordings.length === 0)
|
|
82
|
+
continue;
|
|
83
|
+
await mergeRecordings(t.file, t.upstream, recordings, `Imported by slicetest from ${path.basename(harFile)} (${t.upstream})`);
|
|
84
|
+
written.push({ name: t.name, file: t.file, count: recordings.length });
|
|
85
|
+
}
|
|
86
|
+
const others = new Map();
|
|
87
|
+
for (const e of entries) {
|
|
88
|
+
if (claimed.has(e))
|
|
89
|
+
continue;
|
|
90
|
+
try {
|
|
91
|
+
const host = new URL(e.request.url).origin;
|
|
92
|
+
others.set(host, (others.get(host) ?? 0) + 1);
|
|
93
|
+
}
|
|
94
|
+
catch { }
|
|
95
|
+
}
|
|
96
|
+
return { written, skipped, others: [...others].sort((a, b) => b[1] - a[1]) };
|
|
97
|
+
}
|
|
98
|
+
function parse(text) {
|
|
99
|
+
if (!text)
|
|
100
|
+
return undefined;
|
|
101
|
+
try {
|
|
102
|
+
return JSON.parse(text);
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
}
|
package/dist/http.d.ts
CHANGED
|
@@ -11,11 +11,15 @@ export interface HttpResponse {
|
|
|
11
11
|
/** Wall-clock time of the request in milliseconds. */
|
|
12
12
|
durationMs: number;
|
|
13
13
|
}
|
|
14
|
+
type QueryValue = string | number | boolean;
|
|
14
15
|
export interface RequestOptions {
|
|
15
16
|
headers?: Record<string, string>;
|
|
16
|
-
|
|
17
|
+
/** A list repeats the parameter: `{ tag: ["a", "b"] }` is `?tag=a&tag=b`. */
|
|
18
|
+
query?: Record<string, QueryValue | QueryValue[] | undefined>;
|
|
17
19
|
/** Follow redirects instead of returning the 3xx response. Default false. */
|
|
18
20
|
follow?: boolean;
|
|
21
|
+
/** Fail the request when the app hasn't answered within this many ms, naming it, instead of the whole test timing out. */
|
|
22
|
+
timeout?: number;
|
|
19
23
|
}
|
|
20
24
|
/** Per-scenario state shared by a client and every client derived from it with `with()`. */
|
|
21
25
|
declare class Session {
|
|
@@ -35,8 +39,12 @@ export declare class HttpClient {
|
|
|
35
39
|
constructor(baseUrl: string, defaults?: RequestOptions, session?: Session);
|
|
36
40
|
/** A client that adds `opts` (e.g. an auth header) to every request, sharing this one's cookies. */
|
|
37
41
|
with(opts: RequestOptions): HttpClient;
|
|
38
|
-
/**
|
|
39
|
-
|
|
42
|
+
/**
|
|
43
|
+
* Wrap fields to be sent as `application/x-www-form-urlencoded`. A list repeats the key (`to=a&to=b`,
|
|
44
|
+
* as an HTML form sends it), and nested objects use bracket keys (`metadata[order]=7`,
|
|
45
|
+
* `items[0][price]=p_1`), as Rails, PHP and Stripe read them. `null` / `undefined` are left out.
|
|
46
|
+
*/
|
|
47
|
+
form(fields: Record<string, unknown>): URLSearchParams;
|
|
40
48
|
get(path: string, opts?: RequestOptions): Promise<HttpResponse>;
|
|
41
49
|
head(path: string, opts?: RequestOptions): Promise<HttpResponse>;
|
|
42
50
|
delete(path: string, opts?: RequestOptions): Promise<HttpResponse>;
|
|
@@ -56,6 +64,16 @@ export declare class HttpClient {
|
|
|
56
64
|
* `{ invalidSignature: true }` and `{ stale: true }` make deliveries the app must refuse.
|
|
57
65
|
*/
|
|
58
66
|
webhook(path: string, payload: unknown, opts: WebhookOptions): Promise<HttpResponse>;
|
|
67
|
+
/**
|
|
68
|
+
* Send a GraphQL operation to the app: `http.graphql("query { poll(id: 1) { title } }")`,
|
|
69
|
+
* `http.graphql(CREATE_POLL, { title: "Tea?" })`. Posts `{ query, variables, operationName }` to
|
|
70
|
+
* `/graphql` (`path` for another endpoint). Pair with `toHaveGraphQLData()`, since GraphQL
|
|
71
|
+
* servers answer errors with status 200.
|
|
72
|
+
*/
|
|
73
|
+
graphql(query: string, variables?: Record<string, unknown>, opts?: RequestOptions & {
|
|
74
|
+
path?: string;
|
|
75
|
+
operationName?: string;
|
|
76
|
+
}): Promise<HttpResponse>;
|
|
59
77
|
/**
|
|
60
78
|
* Run `send` `n` times at once and wait for every response, to provoke races
|
|
61
79
|
* (double bookings, lost updates). Pair with `toHaveStatuses({ 201: 1, 409: n - 1 })`
|
package/dist/http.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { formRequest } from "./form.js";
|
|
1
|
+
import { encodeForm, formRequest } from "./form.js";
|
|
2
2
|
import { lookupHost } from "./intercept.js";
|
|
3
|
+
import { timeline } from "./timeline.js";
|
|
3
4
|
import { signWebhook, webhookBody } from "./webhook.js";
|
|
4
5
|
/** Per-scenario state shared by a client and every client derived from it with `with()`. */
|
|
5
6
|
class Session {
|
|
@@ -33,9 +34,13 @@ export class HttpClient {
|
|
|
33
34
|
with(opts) {
|
|
34
35
|
return new HttpClient(this.baseUrl, merge(this.defaults, opts), this.#session);
|
|
35
36
|
}
|
|
36
|
-
/**
|
|
37
|
+
/**
|
|
38
|
+
* Wrap fields to be sent as `application/x-www-form-urlencoded`. A list repeats the key (`to=a&to=b`,
|
|
39
|
+
* as an HTML form sends it), and nested objects use bracket keys (`metadata[order]=7`,
|
|
40
|
+
* `items[0][price]=p_1`), as Rails, PHP and Stripe read them. `null` / `undefined` are left out.
|
|
41
|
+
*/
|
|
37
42
|
form(fields) {
|
|
38
|
-
return
|
|
43
|
+
return encodeForm(fields);
|
|
39
44
|
}
|
|
40
45
|
get(path, opts) {
|
|
41
46
|
return this.request("GET", path, undefined, opts);
|
|
@@ -83,8 +88,19 @@ export class HttpClient {
|
|
|
83
88
|
* `{ invalidSignature: true }` and `{ stale: true }` make deliveries the app must refuse.
|
|
84
89
|
*/
|
|
85
90
|
webhook(path, payload, opts) {
|
|
86
|
-
const { body, type } = webhookBody(payload);
|
|
87
|
-
|
|
91
|
+
const { body, type } = webhookBody(payload, opts.provider);
|
|
92
|
+
const url = opts.url ?? new URL(path, this.baseUrl).href;
|
|
93
|
+
return this.request("POST", path, body, { headers: { "content-type": type, ...signWebhook(body, { ...opts, url }), ...opts.headers } });
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Send a GraphQL operation to the app: `http.graphql("query { poll(id: 1) { title } }")`,
|
|
97
|
+
* `http.graphql(CREATE_POLL, { title: "Tea?" })`. Posts `{ query, variables, operationName }` to
|
|
98
|
+
* `/graphql` (`path` for another endpoint). Pair with `toHaveGraphQLData()`, since GraphQL
|
|
99
|
+
* servers answer errors with status 200.
|
|
100
|
+
*/
|
|
101
|
+
graphql(query, variables, opts = {}) {
|
|
102
|
+
const { path = "/graphql", operationName, ...request } = opts;
|
|
103
|
+
return this.request("POST", path, { query, ...(variables ? { variables } : {}), ...(operationName ? { operationName } : {}) }, request);
|
|
88
104
|
}
|
|
89
105
|
/**
|
|
90
106
|
* Run `send` `n` times at once and wait for every response, to provoke races
|
|
@@ -111,9 +127,17 @@ export class HttpClient {
|
|
|
111
127
|
if (url.origin !== new URL(this.baseUrl).origin) {
|
|
112
128
|
throw new Error(`slicetest: http only talks to the app under test; "${path}" resolves to ${url.origin}`);
|
|
113
129
|
}
|
|
114
|
-
for (const [k, v] of Object.entries(opts.query ?? {}))
|
|
115
|
-
if (v
|
|
130
|
+
for (const [k, v] of Object.entries(opts.query ?? {})) {
|
|
131
|
+
if (v === undefined)
|
|
132
|
+
continue;
|
|
133
|
+
if (Array.isArray(v)) {
|
|
134
|
+
url.searchParams.delete(k);
|
|
135
|
+
for (const item of v)
|
|
136
|
+
url.searchParams.append(k, String(item));
|
|
137
|
+
}
|
|
138
|
+
else
|
|
116
139
|
url.searchParams.set(k, String(v));
|
|
140
|
+
}
|
|
117
141
|
let res = await this.#send(method, url, body, opts);
|
|
118
142
|
for (let hops = 0; opts.follow && REDIRECTS.has(res.status) && hops < 20; hops++) {
|
|
119
143
|
const location = res.headers.get("location");
|
|
@@ -160,12 +184,14 @@ export class HttpClient {
|
|
|
160
184
|
const started = performance.now();
|
|
161
185
|
let res;
|
|
162
186
|
try {
|
|
163
|
-
res = await fetch(target, { method, headers, body: payload, redirect: "manual" });
|
|
187
|
+
res = await fetch(target, { method, headers, body: payload, redirect: "manual", signal: opts.timeout ? AbortSignal.timeout(opts.timeout) : undefined });
|
|
164
188
|
}
|
|
165
189
|
catch (e) {
|
|
166
190
|
const cause = e.cause?.code;
|
|
167
|
-
|
|
168
|
-
|
|
191
|
+
const failed = { method, url: url.pathname + url.search, status: 0, headers: new Headers(), text: `${e}${cause ? ` (${cause})` : ""}`, json: undefined, durationMs: Math.round(performance.now() - started) };
|
|
192
|
+
timeline.set(failed, { start: started, end: performance.now() });
|
|
193
|
+
this.#record(failed);
|
|
194
|
+
throw new Error(`slicetest: ${method} ${failed.url}: ${requestFailure(e, cause, opts.timeout)}`, { cause: e });
|
|
169
195
|
}
|
|
170
196
|
if (!stub)
|
|
171
197
|
for (const cookie of res.headers.getSetCookie())
|
|
@@ -185,6 +211,7 @@ export class HttpClient {
|
|
|
185
211
|
json,
|
|
186
212
|
durationMs: Math.round(performance.now() - started),
|
|
187
213
|
};
|
|
214
|
+
timeline.set(out, { start: started, end: performance.now() });
|
|
188
215
|
this.#record(out);
|
|
189
216
|
// Listeners (contract checks, coverage) are about the app's responses only.
|
|
190
217
|
if (!stub)
|
|
@@ -278,6 +305,16 @@ function pathMatches(requestPath, cookiePath) {
|
|
|
278
305
|
return false;
|
|
279
306
|
return cookiePath.endsWith("/") || requestPath[cookiePath.length] === "/";
|
|
280
307
|
}
|
|
308
|
+
/** Why a request got no response, in terms of what to look at. */
|
|
309
|
+
function requestFailure(e, code, timeout) {
|
|
310
|
+
if (e.name === "TimeoutError")
|
|
311
|
+
return `no response within ${timeout}ms (http timeout); the app may be waiting on a stub with a delay, a lock or a slow query`;
|
|
312
|
+
if (code === "ECONNREFUSED")
|
|
313
|
+
return "connection refused: the app isn't listening (it may have crashed; its output is below)";
|
|
314
|
+
if (code === "ECONNRESET" || code === "UND_ERR_SOCKET")
|
|
315
|
+
return "the app closed the connection without answering (did it crash or exit?)";
|
|
316
|
+
return `${e.message}${code ? ` (${code})` : ""}`;
|
|
317
|
+
}
|
|
281
318
|
function merge(a, b) {
|
|
282
319
|
return {
|
|
283
320
|
...a,
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
export { scenario } from "./scenario.js";
|
|
2
|
+
export type { ScenarioOptions } from "./scenario.js";
|
|
2
3
|
export { mask } from "./trace.js";
|
|
3
4
|
export type { Trace, MaskOptions } from "./trace.js";
|
|
4
5
|
export type { ScenarioContext } from "./runtime.js";
|
|
5
|
-
export type { Db, Row, Where, RowsOptions, Changes, TableChanges } from "./db.js";
|
|
6
|
-
export type { Stub, RecordedCall, StubResponse, Responder, MatchOptions, RouteBuilder, ChaosOptions, ServerSentEvent } from "./stub.js";
|
|
6
|
+
export type { Db, Row, Where, RowsOptions, Changes, TableChanges, ChangesOptions } from "./db.js";
|
|
7
|
+
export type { Stub, RecordedCall, StubResponse, Responder, MatchOptions, RouteBuilder, GraphQLRouteBuilder, ChaosOptions, ServerSentEvent, UploadedFile } from "./stub.js";
|
|
8
|
+
export type { GraphQLCall } from "./graphql.js";
|
|
7
9
|
export { sse } from "./stub.js";
|
|
8
10
|
export type { HttpClient, HttpResponse, RequestOptions } from "./http.js";
|
|
9
11
|
export type { SubmitOptions } from "./form.js";
|
|
@@ -14,4 +16,4 @@ export type { Issuer, TokenOptions, AuthOptions } from "./auth.js";
|
|
|
14
16
|
export { normalizeSql, type Query, type QueryList } from "./query-log.js";
|
|
15
17
|
export { signWebhook, type WebhookOptions, type WebhookProvider, type HmacScheme } from "./webhook.js";
|
|
16
18
|
export type { SlicetestOptions, ContainerOptions } from "./config.js";
|
|
17
|
-
export type {} from "./matchers.js";
|
|
19
|
+
export type { OrderedCall } from "./matchers.js";
|