@jterrazz/test 15.2.0 → 16.0.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 +120 -418
- package/dist/ambiguity.js +7 -6
- package/dist/appium.adapter.js +12 -11
- package/dist/artifacts.js +25 -1698
- package/dist/browser/index.js +1484 -1142
- package/dist/checker.d.ts +1 -1
- package/dist/checker.js +1308 -76
- package/dist/fixtures.js +110 -0
- package/dist/index.d.ts +1482 -1011
- package/dist/index.js +1715 -1173
- package/dist/literate.js +1747 -0
- package/dist/match.js +625 -84
- package/dist/oxlint.cjs +3534 -1469
- package/dist/oxlint.d.cts +60 -48
- package/dist/oxlint.d.ts +60 -48
- package/dist/oxlint.js +3534 -1469
- package/dist/playwright.adapter.js +101 -6
- package/dist/{intercept.js → server.js} +11 -3
- package/dist/{queue.js → stream.js} +63 -4
- package/dist/vitest.d.ts +85 -36
- package/dist/vitest.js +177 -43
- package/package.json +31 -11
- package/dist/spec-document.js +0 -651
package/dist/index.d.ts
CHANGED
|
@@ -1,120 +1,8 @@
|
|
|
1
1
|
import { Scalar } from "yaml";
|
|
2
|
-
import { DeepMockProxy } from "vitest-mock-extended";
|
|
2
|
+
import { DeepMockProxy, MockProxy } from "vitest-mock-extended";
|
|
3
3
|
import { ReactNode as ComponentUi } from "react";
|
|
4
4
|
import "vitest/browser";
|
|
5
|
-
//#region src/
|
|
6
|
-
/**
|
|
7
|
-
* Code-side dynamic-value matchers (`match.*`) and the capture scope shared
|
|
8
|
-
* by every assertion of a single spec execution.
|
|
9
|
-
*
|
|
10
|
-
* Matchers are used inside `toMatchRows` rows and any structural comparison;
|
|
11
|
-
* their file-side twins are the `{{placeholder}}` forms in `_expected/*.http`
|
|
12
|
-
* fixtures (body AND headers), `_expected/*.json` files, and text snapshots
|
|
13
|
-
* (`_expected/*.txt`). One grammar, one vocabulary (CONVENTIONS D4).
|
|
14
|
-
*/
|
|
15
|
-
/** The frozen token vocabulary (CONVENTIONS D4) plus the code-only kinds. */
|
|
16
|
-
type MatcherKind = 'any' | 'base64' | 'date' | 'duration' | 'email' | 'float' | 'hex' | 'includes' | 'int' | 'ip' | 'iso8601' | 'number' | 'path' | 'port' | 'ref' | 'regex' | 'semver' | 'sha' | 'string' | 'time' | 'ulid' | 'url' | 'uuid' | 'workdir';
|
|
17
|
-
/** Every kind usable as a `{{token}}` in fixture files (all but ref/regex/includes). */
|
|
18
|
-
declare const TOKEN_KINDS: readonly ["any", "base64", "date", "duration", "email", "float", "hex", "int", "ip", "iso8601", "number", "path", "port", "semver", "sha", "string", "time", "ulid", "url", "uuid", "workdir"];
|
|
19
|
-
/** A token kind valid inside a fixture `{{...}}` placeholder. */
|
|
20
|
-
type TokenKind = (typeof TOKEN_KINDS)[number];
|
|
21
|
-
/**
|
|
22
|
-
* A dynamic-value matcher. Created via the {@link match} factories — never
|
|
23
|
-
* constructed directly by user code.
|
|
24
|
-
*/
|
|
25
|
-
declare class Matcher {
|
|
26
|
-
/** For kind 'includes': the substring the actual value must contain. */
|
|
27
|
-
readonly includes?: string | undefined;
|
|
28
|
-
readonly kind: MatcherKind;
|
|
29
|
-
/** For kind 'ref': the capture to assert inequality against. */
|
|
30
|
-
readonly notRef?: string | undefined;
|
|
31
|
-
/** For kind 'ref': the capture name. */
|
|
32
|
-
readonly refName?: string | undefined;
|
|
33
|
-
/** For kind 'regex': the pattern the actual value must match. */
|
|
34
|
-
readonly regex?: RegExp | undefined;
|
|
35
|
-
constructor(kind: MatcherKind, options?: {
|
|
36
|
-
includes?: string | undefined;
|
|
37
|
-
notRef?: string | undefined;
|
|
38
|
-
refName?: string | undefined;
|
|
39
|
-
regex?: RegExp | undefined;
|
|
40
|
-
});
|
|
41
|
-
/** Placeholder-style rendering used in failure diffs and serialization. */
|
|
42
|
-
toString(): string;
|
|
43
|
-
toJSON(): string;
|
|
44
|
-
}
|
|
45
|
-
/** A no-argument matcher factory — what every typed entry of `match` is. */
|
|
46
|
-
type TypedMatcher = () => Matcher;
|
|
47
|
-
/**
|
|
48
|
-
* Dynamic-value matchers for structural comparisons — the code-side mirror of
|
|
49
|
-
* the `{{token}}` fixture grammar (CONVENTIONS D4).
|
|
50
|
-
*
|
|
51
|
-
* @example
|
|
52
|
-
* await expect(result.table('users', { database: 'db' })).toMatchRows({
|
|
53
|
-
* columns: ['id', 'name'],
|
|
54
|
-
* rows: [[match.uuid(), 'Alice']],
|
|
55
|
-
* });
|
|
56
|
-
*/
|
|
57
|
-
/**
|
|
58
|
-
* The matcher roster: one no-argument factory per token kind, plus the three
|
|
59
|
-
* code-only forms the `{{token}}` grammar does not carry.
|
|
60
|
-
*/
|
|
61
|
-
type MatchFactories = Record<TokenKind, TypedMatcher> & {
|
|
62
|
-
includes: (substring: string) => Matcher;
|
|
63
|
-
ref: (name: string, options?: {
|
|
64
|
-
not?: string;
|
|
65
|
-
}) => Matcher;
|
|
66
|
-
regex: (regex: RegExp) => Matcher;
|
|
67
|
-
};
|
|
68
|
-
declare const match: MatchFactories;
|
|
69
|
-
/**
|
|
70
|
-
* Named captures recorded by `match.ref()` / `{{type#ref}}` placeholders.
|
|
71
|
-
* One scope lives on each spec result — every assertion chained off the same
|
|
72
|
-
* result shares it, and a new chain starts fresh.
|
|
73
|
-
*/
|
|
74
|
-
declare class CaptureScope {
|
|
75
|
-
private readonly values;
|
|
76
|
-
/**
|
|
77
|
-
* The exact working directory of the current spec, when the framework
|
|
78
|
-
* knows it (cli mode). Drives the `{{workdir}}` token / `match.workdir()`.
|
|
79
|
-
*/
|
|
80
|
-
workdir?: string | undefined;
|
|
81
|
-
constructor(workdir?: string);
|
|
82
|
-
get(name: string): unknown;
|
|
83
|
-
has(name: string): boolean;
|
|
84
|
-
set(name: string, value: unknown): void;
|
|
85
|
-
}
|
|
86
|
-
//#endregion
|
|
87
|
-
//#region src/specification/facets/_common/result/json.d.ts
|
|
88
|
-
/**
|
|
89
|
-
* Read-only accessor for a JSON payload parsed from a text stream (stdout).
|
|
90
|
-
*
|
|
91
|
-
* Lazily parses the JSON on first use; parse errors are deferred until the
|
|
92
|
-
* value is read so that callers can still read the raw stream text without
|
|
93
|
-
* triggering a throw.
|
|
94
|
-
*
|
|
95
|
-
* ANSI escape sequences are stripped by default before `JSON.parse`
|
|
96
|
-
* (CONVENTIONS D6). When a `transform` is configured on the spec runner, it
|
|
97
|
-
* runs on the stripped text — an escape hatch for application noise not
|
|
98
|
-
* covered by the `{{token}}` grammar.
|
|
99
|
-
*
|
|
100
|
-
* Assertions go through `expect()`: `expect(result.json).toMatch('name.json')`
|
|
101
|
-
* (deep-equal against `_expected/<name>`, with `{{placeholder}}` support).
|
|
102
|
-
*/
|
|
103
|
-
declare class JsonAccessor {
|
|
104
|
-
/** @internal Ref-capture scope shared by the current spec execution. */
|
|
105
|
-
readonly captures: CaptureScope;
|
|
106
|
-
/** @internal */
|
|
107
|
-
private readonly rawText;
|
|
108
|
-
/** @internal Test-file directory — fixture resolution root for matchers. */
|
|
109
|
-
readonly testDir: string;
|
|
110
|
-
/** @internal */
|
|
111
|
-
private readonly transform?;
|
|
112
|
-
constructor(rawText: string, testDir: string, transform?: (text: string) => string, captures?: CaptureScope);
|
|
113
|
-
/** The parsed JSON value. Throws if the text is not valid JSON. */
|
|
114
|
-
get value(): unknown;
|
|
115
|
-
}
|
|
116
|
-
//#endregion
|
|
117
|
-
//#region src/specification/contracts/types.d.ts
|
|
5
|
+
//#region src/model/contracts/types.d.ts
|
|
118
6
|
/**
|
|
119
7
|
* The observed outgoing request, reduced to what contract matchers inspect.
|
|
120
8
|
* Built once per request by the engine (MSW on api/jobs, the stub backend on
|
|
@@ -157,7 +45,11 @@ type ContractRequest = {
|
|
|
157
45
|
type ContractResponse = {
|
|
158
46
|
/** HTTP status code (default: 200). */
|
|
159
47
|
status?: number;
|
|
160
|
-
/**
|
|
48
|
+
/**
|
|
49
|
+
* Response body — an object is JSON, a string is text, `null`/`undefined`
|
|
50
|
+
* is empty, and a {@link StreamBody} (built by `http.stream()` /
|
|
51
|
+
* `http.sse()`) arrives in pieces.
|
|
52
|
+
*/
|
|
161
53
|
body: unknown;
|
|
162
54
|
/** Response headers. */
|
|
163
55
|
headers?: Record<string, string> | undefined;
|
|
@@ -174,6 +66,31 @@ type ContractResponse = {
|
|
|
174
66
|
*/
|
|
175
67
|
transport?: 'network-error' | undefined;
|
|
176
68
|
};
|
|
69
|
+
/**
|
|
70
|
+
* The marker that tells a STREAMED body from a value that happens to be an
|
|
71
|
+
* object. A body reaches three different engines (msw's server, msw's worker,
|
|
72
|
+
* the `node:http` stub) and each of them serialises "an object" as JSON, so a
|
|
73
|
+
* stream declared as a plain value arrives as `{}`. The tag is what each
|
|
74
|
+
* engine recognises before it reaches for `JSON.stringify`.
|
|
75
|
+
*/
|
|
76
|
+
declare const STREAM_BODY = "@jterrazz/test:stream";
|
|
77
|
+
/**
|
|
78
|
+
* A body that arrives in pieces — what `http.stream()` and `http.sse()` build.
|
|
79
|
+
*
|
|
80
|
+
* The chunks are written in order, `delayBetweenChunks` milliseconds apart,
|
|
81
|
+
* and the connection closes after the last one. It is the chunks and their
|
|
82
|
+
* ORDER that a streaming subject is specified against: a client that renders
|
|
83
|
+
* tokens as they land, or one that reconnects on a half-read body.
|
|
84
|
+
*/
|
|
85
|
+
type StreamBody = {
|
|
86
|
+
/** The pieces, written in order. */
|
|
87
|
+
chunks: readonly string[];
|
|
88
|
+
/** The `content-type` the stream is served under. */
|
|
89
|
+
contentType: string;
|
|
90
|
+
/** Milliseconds between two chunks. Default 0 — everything at once. */
|
|
91
|
+
delayBetweenChunks: number;
|
|
92
|
+
kind: typeof STREAM_BODY;
|
|
93
|
+
};
|
|
177
94
|
/**
|
|
178
95
|
* A dynamic response: computed from the observed request at the moment the
|
|
179
96
|
* contract is served, rather than fixed ahead of time. Handed the same
|
|
@@ -187,7 +104,7 @@ type ContractResponder = (request: MatchableRequest) => ContractResponse;
|
|
|
187
104
|
*/
|
|
188
105
|
type ContractResponseValue = ContractResponder | ContractResponse;
|
|
189
106
|
//#endregion
|
|
190
|
-
//#region src/
|
|
107
|
+
//#region src/model/contracts/contract.d.ts
|
|
191
108
|
/**
|
|
192
109
|
* A declared external interaction: what to match (`request`) and what to reply
|
|
193
110
|
* (`response`), together in one named artifact. Contracts live in TypeScript
|
|
@@ -272,263 +189,274 @@ declare function defineContract(contract: Contract): Contract;
|
|
|
272
189
|
*/
|
|
273
190
|
declare function defineContracts(...items: ContractInput[]): Contracts;
|
|
274
191
|
//#endregion
|
|
275
|
-
//#region src/
|
|
192
|
+
//#region src/model/contracts/intercept.d.ts
|
|
276
193
|
/**
|
|
277
|
-
*
|
|
278
|
-
*
|
|
279
|
-
* so `within()` never becomes a second selector language.
|
|
194
|
+
* One block's worth of declared network. Disposed at the end of the scope that
|
|
195
|
+
* declared it: the handlers are dropped, and an unmatched request is thrown.
|
|
280
196
|
*/
|
|
281
|
-
type
|
|
197
|
+
type InterceptScope = AsyncDisposable;
|
|
198
|
+
/** What a scope may state about the network it declares. */
|
|
199
|
+
type InterceptOptions = {
|
|
200
|
+
/**
|
|
201
|
+
* The base a RELATIVE request resolves against, for the life of the scope.
|
|
202
|
+
*
|
|
203
|
+
* A module that calls `fetch('/api/posts')` is browser code: under node the
|
|
204
|
+
* URL cannot be parsed at all, so the request fails before any contract can
|
|
205
|
+
* answer it and the test's only way out was to replace `fetch` itself. With
|
|
206
|
+
* an origin stated, the relative call resolves here and the declared
|
|
207
|
+
* contracts — path form or absolute — match it as they would any other.
|
|
208
|
+
*/
|
|
209
|
+
origin?: string;
|
|
210
|
+
};
|
|
282
211
|
/**
|
|
283
|
-
* The
|
|
212
|
+
* The module-scope network double. Resolves once the engine is listening —
|
|
213
|
+
* which is why it is a promise, and why the canonical form awaits it:
|
|
284
214
|
*
|
|
285
|
-
*
|
|
286
|
-
*
|
|
287
|
-
*
|
|
288
|
-
*
|
|
215
|
+
* ```typescript
|
|
216
|
+
* await using _ = await intercept(http.get(url), http.json({ ok: true }));
|
|
217
|
+
* ```
|
|
218
|
+
*
|
|
219
|
+
* Without that `await` the subject could reach the real network before the
|
|
220
|
+
* first handler is in place, so the shape refuses the racy spelling: a promise
|
|
221
|
+
* is not an `AsyncDisposable`, and the compiler says so.
|
|
289
222
|
*/
|
|
290
|
-
type
|
|
223
|
+
type Intercept = ((contracts: ContractInput, options?: InterceptOptions) => Promise<InterceptScope>) & ((request: ContractRequest, response: ContractResponder | ContractResponse, options?: InterceptOptions) => Promise<InterceptScope>);
|
|
224
|
+
//#endregion
|
|
225
|
+
//#region src/model/matching/match.d.ts
|
|
291
226
|
/**
|
|
292
|
-
*
|
|
293
|
-
*
|
|
294
|
-
* concrete locators by the browser integration. CSS/XPath selectors are
|
|
295
|
-
* deliberately not expressible: user-facing elements are the only surface.
|
|
227
|
+
* Code-side dynamic-value matchers (`match.*`) and the capture scope shared
|
|
228
|
+
* by every assertion of a single spec execution.
|
|
296
229
|
*
|
|
297
|
-
*
|
|
298
|
-
* {
|
|
230
|
+
* Matchers are used inside `toMatchRows` rows and any structural comparison;
|
|
231
|
+
* their file-side twins are the `{{placeholder}}` forms in `_expected/*.http`
|
|
232
|
+
* fixtures (body AND headers), `_expected/*.json` files, and text snapshots
|
|
233
|
+
* (`_expected/*.txt`). One grammar, one vocabulary (CONVENTIONS D4).
|
|
299
234
|
*/
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
235
|
+
/** The frozen token vocabulary (CONVENTIONS D4) plus the code-only kinds. */
|
|
236
|
+
type MatcherKind = 'any' | 'base64' | 'date' | 'duration' | 'email' | 'float' | 'hex' | 'includes' | 'int' | 'ip' | 'iso8601' | 'number' | 'path' | 'port' | 'ref' | 'regex' | 'semver' | 'sha' | 'string' | 'time' | 'ulid' | 'url' | 'uuid' | 'workdir';
|
|
237
|
+
/** Every kind usable as a `{{token}}` in fixture files (all but ref/regex/includes). */
|
|
238
|
+
declare const TOKEN_KINDS: readonly ["any", "base64", "date", "duration", "email", "float", "hex", "int", "ip", "iso8601", "number", "path", "port", "semver", "sha", "string", "time", "ulid", "url", "uuid", "workdir"];
|
|
239
|
+
/** A token kind valid inside a fixture `{{...}}` placeholder. */
|
|
240
|
+
type TokenKind = (typeof TOKEN_KINDS)[number];
|
|
241
|
+
/**
|
|
242
|
+
* A dynamic-value matcher. Created via the {@link match} factories — never
|
|
243
|
+
* constructed directly by user code.
|
|
244
|
+
*/
|
|
245
|
+
declare class Matcher {
|
|
246
|
+
/** For kind 'includes': the substring the actual value must contain. */
|
|
247
|
+
readonly includes?: string | undefined;
|
|
248
|
+
readonly kind: MatcherKind;
|
|
249
|
+
/** For kind 'ref': the capture to assert inequality against. */
|
|
250
|
+
readonly notRef?: string | undefined;
|
|
251
|
+
/** For kind 'ref': the capture name. */
|
|
252
|
+
readonly refName?: string | undefined;
|
|
253
|
+
/** For kind 'regex': the pattern the actual value must match. */
|
|
254
|
+
readonly regex?: RegExp | undefined;
|
|
255
|
+
constructor(kind: MatcherKind, options?: {
|
|
256
|
+
includes?: string | undefined;
|
|
257
|
+
notRef?: string | undefined;
|
|
258
|
+
refName?: string | undefined;
|
|
259
|
+
regex?: RegExp | undefined;
|
|
260
|
+
});
|
|
261
|
+
/** Placeholder-style rendering used in failure diffs and serialization. */
|
|
262
|
+
toString(): string;
|
|
263
|
+
toJSON(): string;
|
|
264
|
+
}
|
|
265
|
+
/** A no-argument matcher factory — what every typed entry of `match` is. */
|
|
266
|
+
type TypedMatcher = () => Matcher;
|
|
267
|
+
/**
|
|
268
|
+
* Dynamic-value matchers for structural comparisons — the code-side mirror of
|
|
269
|
+
* the `{{token}}` fixture grammar (CONVENTIONS D4).
|
|
270
|
+
*
|
|
271
|
+
* @example
|
|
272
|
+
* await expect(result.table('users', { database: 'db' })).toMatchRows({
|
|
273
|
+
* columns: ['id', 'name'],
|
|
274
|
+
* rows: [[match.uuid(), 'Alice']],
|
|
275
|
+
* });
|
|
276
|
+
*/
|
|
277
|
+
/**
|
|
278
|
+
* The matcher roster: one no-argument factory per token kind, plus the three
|
|
279
|
+
* code-only forms the `{{token}}` grammar does not carry.
|
|
280
|
+
*/
|
|
281
|
+
type MatchFactories = Record<TokenKind, TypedMatcher> & {
|
|
282
|
+
includes: (substring: string) => Matcher;
|
|
283
|
+
ref: (name: string, options?: {
|
|
284
|
+
not?: string;
|
|
285
|
+
}) => Matcher;
|
|
286
|
+
regex: (regex: RegExp) => Matcher;
|
|
343
287
|
};
|
|
288
|
+
declare const match: MatchFactories;
|
|
344
289
|
/**
|
|
345
|
-
*
|
|
346
|
-
*
|
|
347
|
-
*
|
|
290
|
+
* Named captures recorded by `match.ref()` / `{{type#ref}}` placeholders.
|
|
291
|
+
* One scope lives on each spec result — every assertion chained off the same
|
|
292
|
+
* result shares it, and a new chain starts fresh.
|
|
348
293
|
*/
|
|
349
|
-
|
|
294
|
+
declare class CaptureScope {
|
|
295
|
+
private readonly values;
|
|
350
296
|
/**
|
|
351
|
-
* The
|
|
352
|
-
*
|
|
353
|
-
* author to disambiguate a name they cannot see.
|
|
297
|
+
* The exact working directory of the current spec, when the framework
|
|
298
|
+
* knows it (cli mode). Drives the `{{workdir}}` token / `match.workdir()`.
|
|
354
299
|
*/
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
text: string;
|
|
364
|
-
};
|
|
365
|
-
/** A `<link>` element captured from the rendered document's head. */
|
|
366
|
-
type BrowserLinkElement = {
|
|
367
|
-
href: string;
|
|
368
|
-
hreflang?: string | undefined;
|
|
369
|
-
rel: string;
|
|
370
|
-
type?: string | undefined;
|
|
371
|
-
};
|
|
372
|
-
/** A `<meta>` element captured from the rendered document's head. */
|
|
373
|
-
type BrowserMetaElement = {
|
|
374
|
-
content: string;
|
|
375
|
-
name?: string | undefined;
|
|
376
|
-
property?: string | undefined;
|
|
377
|
-
};
|
|
378
|
-
/** A console message emitted while the page loaded or the scenario ran. */
|
|
379
|
-
type BrowserConsoleMessage = {
|
|
380
|
-
text: string;
|
|
381
|
-
type: string;
|
|
382
|
-
};
|
|
300
|
+
workdir?: string | undefined;
|
|
301
|
+
constructor(workdir?: string);
|
|
302
|
+
get(name: string): unknown;
|
|
303
|
+
has(name: string): boolean;
|
|
304
|
+
set(name: string, value: unknown): void;
|
|
305
|
+
}
|
|
306
|
+
//#endregion
|
|
307
|
+
//#region src/model/result/directory.d.ts
|
|
383
308
|
/**
|
|
384
|
-
*
|
|
385
|
-
*
|
|
386
|
-
*
|
|
387
|
-
*
|
|
309
|
+
* Read-only accessor for a directory produced by a spec action.
|
|
310
|
+
*
|
|
311
|
+
* Assertions go through `expect()` (async — they walk the disk):
|
|
312
|
+
* `await expect(result.directory('out')).toMatch('scaffold/out')`
|
|
313
|
+
* compares the tree against the fixture directory `_expected/<name>/`.
|
|
388
314
|
*/
|
|
389
|
-
|
|
390
|
-
/**
|
|
391
|
-
|
|
392
|
-
/**
|
|
393
|
-
|
|
394
|
-
/**
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
/** Hover the element. */
|
|
405
|
-
hover: (element: ElementRef) => Promise<void>;
|
|
406
|
-
/** Press a key (e.g. `Enter`). */
|
|
407
|
-
press: (key: string) => Promise<void>;
|
|
408
|
-
/** Wait until the element is visible — the only synchronization primitive. */
|
|
409
|
-
see: (element: ElementRef) => Promise<void>;
|
|
410
|
-
/** Select an option in a select field. */
|
|
411
|
-
select: (element: ElementRef, option: string) => Promise<void>;
|
|
412
|
-
};
|
|
413
|
-
/** The behavior of a visit — the When of the spec; assertions stay in the Then. */
|
|
414
|
-
type VisitScenario = (visitor: Visitor) => Promise<void>;
|
|
415
|
-
/** Per-visit options forwarded to the browser context. */
|
|
416
|
-
type BrowserOpenOptions = {
|
|
417
|
-
/**
|
|
418
|
-
* Extra origins the `external: 'block'` policy lets through — the
|
|
419
|
-
* declared stub backend the page legitimately fetches from.
|
|
420
|
-
*/
|
|
421
|
-
allowedOrigins?: string[] | undefined;
|
|
422
|
-
/**
|
|
423
|
-
* Base URL of the site under test — the origin `goto()` resolves against
|
|
424
|
-
* and the boundary of the `external` policy.
|
|
425
|
-
*/
|
|
426
|
-
baseUrl: string;
|
|
427
|
-
/**
|
|
428
|
-
* Cross-origin request policy. `'block'` aborts every request leaving
|
|
429
|
-
* the site under test (analytics, CDNs) — the browser-side analog of
|
|
430
|
-
* strict intercepts. `'allow'` lets them through (deployed-site mode).
|
|
431
|
-
*/
|
|
432
|
-
external: 'allow' | 'block';
|
|
433
|
-
/** Extra HTTP headers sent with every request of the visit (incl. User-Agent overrides). */
|
|
434
|
-
headers?: Record<string, string> | undefined;
|
|
435
|
-
/** The interaction scenario to run after load; the capture reflects the final state. */
|
|
436
|
-
scenario?: undefined | VisitScenario;
|
|
437
|
-
};
|
|
315
|
+
declare class DirectoryAccessor {
|
|
316
|
+
/** @internal Ref-capture scope shared by the current spec execution. */
|
|
317
|
+
readonly captures: CaptureScope;
|
|
318
|
+
/** @internal Absolute path of the directory under assertion. */
|
|
319
|
+
readonly root: string;
|
|
320
|
+
/** @internal Test-file directory — fixture resolution root for matchers. */
|
|
321
|
+
readonly testDir: string;
|
|
322
|
+
constructor(absPath: string, testDir: string, captures?: CaptureScope);
|
|
323
|
+
/** List all files (recursively) under the directory, sorted. */
|
|
324
|
+
files(options?: {
|
|
325
|
+
ignore?: string[];
|
|
326
|
+
}): Promise<string[]>;
|
|
327
|
+
}
|
|
328
|
+
//#endregion
|
|
329
|
+
//#region src/model/result/filesystem.d.ts
|
|
438
330
|
/**
|
|
439
|
-
*
|
|
440
|
-
*
|
|
441
|
-
*
|
|
331
|
+
* Read-only accessor for the whole temporary working directory used by a
|
|
332
|
+
* command spec.
|
|
333
|
+
*
|
|
334
|
+
* Assertions go through `expect()` (async — they walk the disk):
|
|
335
|
+
* `await expect(result.filesystem).toMatch('scaffolded')` compares the
|
|
336
|
+
* tree against the fixture directory `_expected/<name>/`.
|
|
442
337
|
*/
|
|
443
|
-
|
|
444
|
-
/**
|
|
445
|
-
|
|
446
|
-
/**
|
|
447
|
-
|
|
448
|
-
/**
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
/** The ARIA snapshot of the rendered `<body>` — the outline a golden pins. */
|
|
459
|
-
tree: string;
|
|
460
|
-
/** `document.title` after rendering. */
|
|
461
|
-
title: string;
|
|
462
|
-
/** Final URL after redirects and scenario navigation. */
|
|
463
|
-
url: string;
|
|
464
|
-
};
|
|
338
|
+
declare class FilesystemAccessor {
|
|
339
|
+
/** @internal Ref-capture scope shared by the current spec execution. */
|
|
340
|
+
readonly captures: CaptureScope;
|
|
341
|
+
/** The absolute path of the temporary working directory. */
|
|
342
|
+
readonly cwd: string;
|
|
343
|
+
/** @internal Test-file directory — fixture resolution root for matchers. */
|
|
344
|
+
readonly testDir: string;
|
|
345
|
+
constructor(cwd: string, testDir: string, captures?: CaptureScope);
|
|
346
|
+
/** List all files (recursively) under the working directory, sorted. */
|
|
347
|
+
files(options?: {
|
|
348
|
+
ignore?: string[];
|
|
349
|
+
}): Promise<string[]>;
|
|
350
|
+
}
|
|
351
|
+
//#endregion
|
|
352
|
+
//#region src/model/result/json.d.ts
|
|
465
353
|
/**
|
|
466
|
-
*
|
|
467
|
-
*
|
|
468
|
-
*
|
|
354
|
+
* Read-only accessor for a JSON payload parsed from a text stream (stdout).
|
|
355
|
+
*
|
|
356
|
+
* Lazily parses the JSON on first use; parse errors are deferred until the
|
|
357
|
+
* value is read so that callers can still read the raw stream text without
|
|
358
|
+
* triggering a throw.
|
|
359
|
+
*
|
|
360
|
+
* ANSI escape sequences are stripped by default before `JSON.parse`
|
|
361
|
+
* (CONVENTIONS D6). When a `transform` is configured on the spec runner, it
|
|
362
|
+
* runs on the stripped text — an escape hatch for application noise not
|
|
363
|
+
* covered by the `{{token}}` grammar.
|
|
364
|
+
*
|
|
365
|
+
* Assertions go through `expect()`: `expect(result.json).toMatch('name.json')`
|
|
366
|
+
* (deep-equal against `_expected/<name>`, with `{{placeholder}}` support).
|
|
469
367
|
*/
|
|
470
|
-
|
|
471
|
-
/**
|
|
472
|
-
|
|
473
|
-
/**
|
|
474
|
-
|
|
475
|
-
|
|
368
|
+
declare class JsonAccessor<Value = unknown> {
|
|
369
|
+
/** @internal Ref-capture scope shared by the current spec execution. */
|
|
370
|
+
readonly captures: CaptureScope;
|
|
371
|
+
/** @internal */
|
|
372
|
+
private readonly rawText;
|
|
373
|
+
/** @internal Test-file directory — fixture resolution root for matchers. */
|
|
374
|
+
readonly testDir: string;
|
|
375
|
+
/** @internal */
|
|
376
|
+
private readonly transform?;
|
|
377
|
+
constructor(rawText: string, testDir: string, transform?: (text: string) => string, captures?: CaptureScope);
|
|
378
|
+
/**
|
|
379
|
+
* The parsed JSON value. Throws if the text is not valid JSON.
|
|
380
|
+
*
|
|
381
|
+
* `Value` is `unknown` unless the subject that produced this accessor
|
|
382
|
+
* knows better — `.call<T>()` does, and hands its T straight through so a
|
|
383
|
+
* one-field reading needs no cast in the test.
|
|
384
|
+
*/
|
|
385
|
+
get value(): Value;
|
|
386
|
+
}
|
|
476
387
|
//#endregion
|
|
477
|
-
//#region src/
|
|
388
|
+
//#region src/model/result/match-options.d.ts
|
|
478
389
|
/**
|
|
479
|
-
*
|
|
390
|
+
* The per-call option both `toMatch` implementations honour.
|
|
391
|
+
*
|
|
392
|
+
* A golden is read from disk under node and through a server command from
|
|
393
|
+
* inside a page, and BOTH builds must answer `{ frozen: true }` the same way —
|
|
394
|
+
* so the option is declared here, where the node matchers and the page's
|
|
395
|
+
* matchers can each reach it, rather than in the runner's own folder, which a
|
|
396
|
+
* seam may not import (rule I1).
|
|
480
397
|
*/
|
|
481
|
-
type CliOutput = {
|
|
482
|
-
/** Process exit code (0 = success). */
|
|
483
|
-
exitCode: number;
|
|
484
|
-
/** Captured standard output. */
|
|
485
|
-
stdout: string;
|
|
486
|
-
/** Captured standard error. */
|
|
487
|
-
stderr: string;
|
|
488
|
-
};
|
|
489
398
|
/**
|
|
490
|
-
*
|
|
491
|
-
*
|
|
492
|
-
*
|
|
493
|
-
*
|
|
399
|
+
* Per-call options for the fixture-file `toMatch` subjects. `frozen` opts a
|
|
400
|
+
* single fixture OUT of update-mode rewriting: a frozen fixture is NEVER
|
|
401
|
+
* written under `TEST_UPDATE=1` (or vitest `-u`) — in update mode a frozen
|
|
402
|
+
* mismatch still throws its diff, and a frozen missing fixture still throws its
|
|
403
|
+
* "does not exist" error. This is what makes a DELIBERATELY-WRONG fixture (the
|
|
404
|
+
* subject of a negative test that asserts the mismatch/error rendering)
|
|
405
|
+
* survivable across update runs instead of being silently overwritten with the
|
|
406
|
+
* actual output.
|
|
494
407
|
*/
|
|
495
|
-
type
|
|
496
|
-
|
|
497
|
-
waitFor?: string | undefined;
|
|
498
|
-
/** Kill the process after this many milliseconds. Defaults to 10 000. */
|
|
499
|
-
timeout?: number | undefined;
|
|
408
|
+
type MatchFixtureOptions = {
|
|
409
|
+
frozen?: boolean;
|
|
500
410
|
};
|
|
411
|
+
//#endregion
|
|
412
|
+
//#region src/model/ports/server.port.d.ts
|
|
501
413
|
/**
|
|
502
|
-
*
|
|
503
|
-
* Values are merged on top of process.env. A `null` value unsets the variable.
|
|
504
|
-
*/
|
|
505
|
-
type CliEnv = Record<string, null | string>;
|
|
506
|
-
/**
|
|
507
|
-
* Abstract command interface for specification runners.
|
|
508
|
-
* Implement this to plug in your command execution strategy.
|
|
414
|
+
* HTTP response returned by a server port, with parsed JSON body.
|
|
509
415
|
*/
|
|
416
|
+
type ServerResponse = {
|
|
417
|
+
/** HTTP status code. */
|
|
418
|
+
status: number;
|
|
419
|
+
/** Parsed JSON response body, or null if parsing failed. */
|
|
420
|
+
body: unknown;
|
|
421
|
+
/** Response headers as a flat key-value map. */
|
|
422
|
+
headers: Record<string, string>;
|
|
423
|
+
};
|
|
510
424
|
/**
|
|
511
|
-
*
|
|
512
|
-
*
|
|
513
|
-
* immediately-closed stdin (never a TTY) and the adapter's own timeout.
|
|
425
|
+
* Abstract server interface for specification runners.
|
|
426
|
+
* Integration mode uses an in-process Hono app; E2E mode uses real HTTP via fetch.
|
|
514
427
|
*/
|
|
515
|
-
type
|
|
516
|
-
/**
|
|
517
|
-
|
|
518
|
-
/** Milliseconds after which the run is killed (exit code 124). */
|
|
519
|
-
timeout?: number | undefined;
|
|
520
|
-
};
|
|
521
|
-
type CliPort = {
|
|
522
|
-
/** Execute a command with the given arguments in the given working directory. */
|
|
523
|
-
exec: (args: string, cwd: string, env?: CliEnv, input?: CliInput) => Promise<CliOutput>;
|
|
524
|
-
/** Run a long-running process and wait for a pattern or timeout. */
|
|
525
|
-
watch: (args: string, cwd: string, options: ExecOptions, env?: CliEnv) => Promise<CliOutput>;
|
|
428
|
+
type ServerPort = {
|
|
429
|
+
/** Send an HTTP request and return the parsed response. */
|
|
430
|
+
request: (method: string, path: string, body?: unknown, headers?: Record<string, string>) => Promise<ServerResponse>;
|
|
526
431
|
};
|
|
527
432
|
//#endregion
|
|
528
|
-
//#region src/
|
|
433
|
+
//#region src/model/result/response.d.ts
|
|
529
434
|
/**
|
|
530
|
-
*
|
|
531
|
-
*
|
|
435
|
+
* Read-only accessor for an HTTP response.
|
|
436
|
+
*
|
|
437
|
+
* Assertions go through `expect()`:
|
|
438
|
+
* `expect(result.response).toMatch('created.http')` compares status,
|
|
439
|
+
* a subset of headers, and the JSON body against `_expected/<name>` —
|
|
440
|
+
* with `{{placeholder}}` support in all three.
|
|
441
|
+
*/
|
|
442
|
+
declare class ResponseAccessor {
|
|
443
|
+
/** The parsed JSON response body (null when parsing failed). */
|
|
444
|
+
readonly body: unknown;
|
|
445
|
+
/** @internal Ref-capture scope shared by the current spec execution. */
|
|
446
|
+
readonly captures: CaptureScope;
|
|
447
|
+
/** Response headers as a flat, lower-cased key-value map. */
|
|
448
|
+
readonly headers: Record<string, string>;
|
|
449
|
+
/** The HTTP response status code. */
|
|
450
|
+
readonly status: number;
|
|
451
|
+
/** @internal Test-file directory — fixture resolution root for matchers. */
|
|
452
|
+
readonly testDir: string;
|
|
453
|
+
constructor(response: ServerResponse, testDir: string, captures?: CaptureScope);
|
|
454
|
+
}
|
|
455
|
+
//#endregion
|
|
456
|
+
//#region src/model/ports/database.port.d.ts
|
|
457
|
+
/**
|
|
458
|
+
* Abstract database interface for specification runners.
|
|
459
|
+
* Implement this to plug in your database stack (e.g. Postgres, SQLite).
|
|
532
460
|
*/
|
|
533
461
|
type DatabasePort = {
|
|
534
462
|
/** Execute raw SQL (for seeding test data). */
|
|
@@ -539,446 +467,618 @@ type DatabasePort = {
|
|
|
539
467
|
reset: () => Promise<void>;
|
|
540
468
|
};
|
|
541
469
|
//#endregion
|
|
542
|
-
//#region src/
|
|
470
|
+
//#region src/model/result/table.d.ts
|
|
543
471
|
/**
|
|
544
|
-
*
|
|
545
|
-
*
|
|
546
|
-
*
|
|
547
|
-
*
|
|
548
|
-
*
|
|
472
|
+
* Read-only accessor for a database table after a specification run.
|
|
473
|
+
*
|
|
474
|
+
* Assertions go through `expect()` (async — they query the database):
|
|
475
|
+
*
|
|
476
|
+
* await expect(result.table('users', { database: 'db' })).toMatchRows({
|
|
477
|
+
* columns: ['name'],
|
|
478
|
+
* rows: [['Alice']],
|
|
479
|
+
* });
|
|
480
|
+
* await expect(result.table('users', { database: 'db' })).toBeEmpty();
|
|
549
481
|
*/
|
|
550
|
-
|
|
482
|
+
declare class TableAccessor {
|
|
483
|
+
/** @internal Ref-capture scope shared by the current spec execution. */
|
|
484
|
+
readonly captures: CaptureScope;
|
|
485
|
+
/** @internal */
|
|
486
|
+
private readonly db;
|
|
487
|
+
/** The table name this accessor reads. */
|
|
488
|
+
readonly name: string;
|
|
489
|
+
constructor(name: string, db: DatabasePort, captures?: CaptureScope);
|
|
490
|
+
/** @internal Query the table — used by the toMatchRows / toBeEmpty matchers. */
|
|
491
|
+
query(columns: string[]): Promise<unknown[][]>;
|
|
492
|
+
}
|
|
493
|
+
//#endregion
|
|
494
|
+
//#region src/model/result/text.d.ts
|
|
551
495
|
/**
|
|
552
|
-
*
|
|
553
|
-
*
|
|
554
|
-
* `testId()`, `within()`) and translated into iOS predicate strings by the
|
|
555
|
-
* device integration. Structurally the same ref type as the browser's, so the
|
|
556
|
-
* vocabulary stays single; kinds outside {@link MobileElementKind} (the ARIA
|
|
557
|
-
* landmarks) are refused at runtime with a message naming the boundary.
|
|
496
|
+
* Read-only accessor for a captured text handle — THE universal one: stdout,
|
|
497
|
+
* stderr, container logs, and file text all surface as a `TextAccessor`.
|
|
558
498
|
*
|
|
559
|
-
*
|
|
560
|
-
* (`
|
|
561
|
-
* `
|
|
562
|
-
*
|
|
499
|
+
* Backed by a primitive string, exposed via {@link text}, but also provides
|
|
500
|
+
* `toString()` / `valueOf()` for string coercion, so common patterns like
|
|
501
|
+
* `String(result.stdout)` and template-literal interpolation still work.
|
|
502
|
+
*
|
|
503
|
+
* ANSI escape sequences are stripped by default before every comparison
|
|
504
|
+
* (CONVENTIONS D6) — `.text` stays raw. The runner `transform` option remains
|
|
505
|
+
* an escape hatch for application noise not covered by the `{{token}}`
|
|
506
|
+
* grammar.
|
|
507
|
+
*
|
|
508
|
+
* Text operations are **closed** over the type: `.grep(pattern)` returns a
|
|
509
|
+
* `TextAccessor` (not a bare string), preserving the `_expected/`-resolution
|
|
510
|
+
* context and the `transform`, so results are chainable
|
|
511
|
+
* (`result.stdout.grep(a).grep(b)`) and snapshot-able
|
|
512
|
+
* (`expect(result.stdout.grep('users.ts')).toMatch('block.txt')`).
|
|
513
|
+
*
|
|
514
|
+
* Assertions go through `expect()` matchers: `expect(result.stdout).toContain(...)`
|
|
515
|
+
* and `expect(result.stdout).toMatch('name.txt')` (resolved against
|
|
516
|
+
* `_expected/<name>`, flat — with `{{token}}` support, CONVENTIONS D4).
|
|
563
517
|
*/
|
|
564
|
-
|
|
518
|
+
declare class TextAccessor {
|
|
519
|
+
/** @internal Ref-capture scope shared by the current spec execution. */
|
|
520
|
+
readonly captures: CaptureScope;
|
|
521
|
+
/** @internal Stream label used in failure messages ("stdout" / "stderr"). */
|
|
522
|
+
readonly streamName: string;
|
|
523
|
+
/** @internal Test-file directory — fixture resolution root for matchers. */
|
|
524
|
+
readonly testDir: string;
|
|
525
|
+
/** The raw captured text (never transformed, ANSI preserved). */
|
|
526
|
+
readonly text: string;
|
|
527
|
+
/** @internal Normaliser applied before comparisons, never to fixtures. */
|
|
528
|
+
readonly transform?: ((text: string) => string) | undefined;
|
|
529
|
+
constructor(value: string, streamName: string, testDir: string, options?: {
|
|
530
|
+
captures?: CaptureScope | undefined;
|
|
531
|
+
transform?: ((text: string) => string) | undefined;
|
|
532
|
+
});
|
|
533
|
+
/** @internal The text as compared by matchers — ANSI stripped, transform applied. */
|
|
534
|
+
get comparableText(): string;
|
|
535
|
+
/**
|
|
536
|
+
* Keep only the blank-line-separated blocks of the text that contain
|
|
537
|
+
* `pattern` (how linter/compiler output is structured), returned as a new
|
|
538
|
+
* `TextAccessor` — the same subject type, so the result is chainable and
|
|
539
|
+
* snapshot-able. The scalpel for probing large tool outputs; snapshot the
|
|
540
|
+
* whole surface by default, reach for `.grep()` for targeted checks.
|
|
541
|
+
*/
|
|
542
|
+
grep(pattern: string): TextAccessor;
|
|
543
|
+
toString(): string;
|
|
544
|
+
valueOf(): string;
|
|
545
|
+
}
|
|
546
|
+
//#endregion
|
|
547
|
+
//#region src/model/result/result.d.ts
|
|
548
|
+
/** Read-only handle to a single file produced by a spec action. */
|
|
549
|
+
type FileAccessor = {
|
|
550
|
+
/** The UTF-8 text content. Throws if the file does not exist. */
|
|
551
|
+
readonly content: string;
|
|
552
|
+
readonly exists: boolean;
|
|
553
|
+
/** The file text as a {@link TextAccessor}, keeping only blocks matching `pattern`. */
|
|
554
|
+
grep: (pattern: string) => TextAccessor;
|
|
555
|
+
};
|
|
556
|
+
type BaseResultOptions = {
|
|
557
|
+
config: SpecificationConfig;
|
|
558
|
+
testDir: string;
|
|
559
|
+
workDir?: string | undefined;
|
|
560
|
+
};
|
|
565
561
|
/**
|
|
566
|
-
*
|
|
567
|
-
*
|
|
568
|
-
*
|
|
562
|
+
* Base result - common accessors available after any action type.
|
|
563
|
+
* Extended by HttpResult, CliResult, and used directly by job results.
|
|
564
|
+
*
|
|
565
|
+
* Every result carries a fresh {@link CaptureScope}: `match.ref()` captures
|
|
566
|
+
* and `{{type#ref}}` placeholders are scoped to one spec execution.
|
|
569
567
|
*/
|
|
570
|
-
|
|
571
|
-
/**
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
/**
|
|
578
|
-
|
|
579
|
-
|
|
568
|
+
declare class BaseResult {
|
|
569
|
+
/** @internal Ref-capture scope shared by every assertion on this result. */
|
|
570
|
+
readonly captures: CaptureScope;
|
|
571
|
+
protected config: SpecificationConfig;
|
|
572
|
+
protected testDir: string;
|
|
573
|
+
protected workDir?: string | undefined;
|
|
574
|
+
constructor(options: BaseResultOptions);
|
|
575
|
+
/** Access a directory (relative to the working directory) for snapshot assertions. */
|
|
576
|
+
directory(path?: string): DirectoryAccessor;
|
|
577
|
+
/** Access a single file (relative to the working directory) for content assertions. */
|
|
578
|
+
file(path: string): FileAccessor;
|
|
579
|
+
/** Access a database table for row-level assertions via expect() matchers. */
|
|
580
|
+
table(tableName: string, options?: {
|
|
581
|
+
database?: string;
|
|
582
|
+
}): TableAccessor;
|
|
583
|
+
private resolveDatabase;
|
|
584
|
+
}
|
|
585
|
+
//#endregion
|
|
586
|
+
//#region src/facets/api/api.result.d.ts
|
|
587
|
+
/** Result from an HTTP action (.request(), .get(), .post(), .put(), .delete()). */
|
|
588
|
+
declare class HttpResult extends BaseResult {
|
|
589
|
+
private readonly responseData;
|
|
590
|
+
constructor(options: {
|
|
591
|
+
config: SpecificationConfig;
|
|
592
|
+
response: ServerResponse;
|
|
593
|
+
testDir: string;
|
|
594
|
+
});
|
|
595
|
+
/** Access the HTTP response (status, headers, body) for assertions. */
|
|
596
|
+
get response(): ResponseAccessor;
|
|
597
|
+
/** The HTTP response status code. */
|
|
598
|
+
get status(): number;
|
|
599
|
+
}
|
|
600
|
+
//#endregion
|
|
601
|
+
//#region src/model/ports/cli.port.d.ts
|
|
580
602
|
/**
|
|
581
|
-
*
|
|
582
|
-
* Every verb auto-waits by polling until a visible match exists; acting
|
|
583
|
-
* verbs then enforce exactly-one (W3), while `see()` — the single
|
|
584
|
-
* synchronization primitive — is satisfied by any visible match. There is
|
|
585
|
-
* no sleep and no conditional helper.
|
|
603
|
+
* Raw output from a command execution, including exit code and captured output streams.
|
|
586
604
|
*/
|
|
587
|
-
type
|
|
588
|
-
/**
|
|
589
|
-
|
|
590
|
-
/**
|
|
591
|
-
|
|
592
|
-
/**
|
|
593
|
-
|
|
605
|
+
type CliOutput = {
|
|
606
|
+
/** Process exit code (0 = success). */
|
|
607
|
+
exitCode: number;
|
|
608
|
+
/** Captured standard output. */
|
|
609
|
+
stdout: string;
|
|
610
|
+
/** Captured standard error. */
|
|
611
|
+
stderr: string;
|
|
594
612
|
};
|
|
595
|
-
/** The behavior of an open — the When of the spec; assertions stay in the Then. */
|
|
596
|
-
type MobileScenario = (visitor: MobileVisitor) => Promise<void>;
|
|
597
613
|
/**
|
|
598
|
-
*
|
|
599
|
-
*
|
|
600
|
-
*
|
|
601
|
-
*
|
|
614
|
+
* Options for the long-running form of `.exec()` (CONVENTIONS B2). When
|
|
615
|
+
* either option is present the process is spawned and observed: it resolves
|
|
616
|
+
* as soon as `waitFor` appears in stdout/stderr, and is killed when `timeout`
|
|
617
|
+
* elapses (exit code 124).
|
|
602
618
|
*/
|
|
603
|
-
type
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
/** Element type without the `XCUIElementType` prefix — `Button`, `StaticText`. */
|
|
609
|
-
type: string;
|
|
610
|
-
value?: string;
|
|
619
|
+
type ExecOptions = {
|
|
620
|
+
/** Resolve (exit code 0) when stdout/stderr contains this string. */
|
|
621
|
+
waitFor?: string | undefined;
|
|
622
|
+
/** Kill the process after this many milliseconds. Defaults to 10 000. */
|
|
623
|
+
timeout?: number | undefined;
|
|
611
624
|
};
|
|
612
625
|
/**
|
|
613
|
-
*
|
|
614
|
-
*
|
|
615
|
-
* labels/values in document order, consecutive duplicates collapsed.
|
|
626
|
+
* Extra environment variables to set for the child process.
|
|
627
|
+
* Values are merged on top of process.env. A `null` value unsets the variable.
|
|
616
628
|
*/
|
|
617
|
-
type
|
|
618
|
-
texts: string[];
|
|
619
|
-
tree: ScreenNode;
|
|
620
|
-
};
|
|
621
|
-
/** Per-open options forwarded to the device session. */
|
|
622
|
-
type DeviceOpenOptions = {
|
|
623
|
-
/** The app under test — terminated and relaunched for a fresh, deterministic state. */
|
|
624
|
-
bundleId: string;
|
|
625
|
-
/** Deep link applied after the relaunch (`news://events`); absent opens the app plainly. */
|
|
626
|
-
deepLink?: string | undefined;
|
|
627
|
-
/** The interaction scenario to run after launch; the capture reflects the final state. */
|
|
628
|
-
scenario?: MobileScenario | undefined;
|
|
629
|
-
};
|
|
629
|
+
type CliEnv = Record<string, null | string>;
|
|
630
630
|
/**
|
|
631
|
-
*
|
|
632
|
-
*
|
|
633
|
-
* wait it needs to move — the defaults suit a release build, and a project
|
|
634
|
-
* that drives a DEV build pays for its bundler's cold boot.
|
|
631
|
+
* Abstract command interface for specification runners.
|
|
632
|
+
* Implement this to plug in your command execution strategy.
|
|
635
633
|
*/
|
|
636
|
-
type DeviceTimeouts = {
|
|
637
|
-
/**
|
|
638
|
-
* How long every verb polls for a visible match before refusing.
|
|
639
|
-
* Default: `30_000` — enough for a cold app boot on a release bundle.
|
|
640
|
-
*/
|
|
641
|
-
action?: number | undefined;
|
|
642
|
-
/**
|
|
643
|
-
* How long the FIRST session waits for WebDriverAgent to build and
|
|
644
|
-
* launch on the simulator. Default: `240_000`.
|
|
645
|
-
*/
|
|
646
|
-
launch?: number | undefined;
|
|
647
|
-
};
|
|
648
634
|
/**
|
|
649
|
-
*
|
|
650
|
-
*
|
|
651
|
-
*
|
|
652
|
-
* terminates and relaunches the app for a deterministic fresh state.
|
|
635
|
+
* What one invocation is handed beyond its arguments — the two per-run inputs a
|
|
636
|
+
* spec document may state. Both are absent by default: the child gets an
|
|
637
|
+
* immediately-closed stdin (never a TTY) and the adapter's own timeout.
|
|
653
638
|
*/
|
|
654
|
-
type
|
|
655
|
-
/**
|
|
656
|
-
|
|
657
|
-
/**
|
|
658
|
-
|
|
639
|
+
type CliInput = {
|
|
640
|
+
/** Written to the child's stdin, which is then closed. */
|
|
641
|
+
stdin?: string | undefined;
|
|
642
|
+
/** Milliseconds after which the run is killed (exit code 124). */
|
|
643
|
+
timeout?: number | undefined;
|
|
644
|
+
};
|
|
645
|
+
type CliPort = {
|
|
646
|
+
/** Execute a command with the given arguments in the given working directory. */
|
|
647
|
+
exec: (args: string, cwd: string, env?: CliEnv, input?: CliInput) => Promise<CliOutput>;
|
|
648
|
+
/** Run a long-running process and wait for a pattern or timeout. */
|
|
649
|
+
watch: (args: string, cwd: string, options: ExecOptions, env?: CliEnv) => Promise<CliOutput>;
|
|
659
650
|
};
|
|
660
651
|
//#endregion
|
|
661
|
-
//#region src/
|
|
652
|
+
//#region src/facets/cli/literate.d.ts
|
|
662
653
|
/**
|
|
663
|
-
*
|
|
654
|
+
* Running a `<case>.spec.yaml` document: one scenario, one working directory,
|
|
655
|
+
* one set of servers, every run executed and asserted.
|
|
656
|
+
*
|
|
657
|
+
* The engine composes pieces that already exist rather than adding a second of
|
|
658
|
+
* anything — {@link copyPlan} for `fixture:`, the runner's own command adapter
|
|
659
|
+
* for each run, {@link ServeAdapter} for `serve:`, the `{{token}}` engine for
|
|
660
|
+
* every comparison, and {@link mergeTextPreservingPlaceholders} for the
|
|
661
|
+
* rewrite. What is new here is only the ORCHESTRATION the chain cannot express:
|
|
662
|
+
* several commands sharing one working directory, each judged the moment it
|
|
663
|
+
* ends — so what a run states about that directory is what IT left there, and
|
|
664
|
+
* the first run to disagree is the one the failure names.
|
|
664
665
|
*/
|
|
665
|
-
type ServerResponse = {
|
|
666
|
-
/** HTTP status code. */
|
|
667
|
-
status: number;
|
|
668
|
-
/** Parsed JSON response body, or null if parsing failed. */
|
|
669
|
-
body: unknown;
|
|
670
|
-
/** Response headers as a flat key-value map. */
|
|
671
|
-
headers: Record<string, string>;
|
|
672
|
-
};
|
|
673
666
|
/**
|
|
674
|
-
*
|
|
675
|
-
*
|
|
667
|
+
* A server a document may start by name (`serve: mcp`), registered once per app
|
|
668
|
+
* in the `serve` option of `specification.cli()`.
|
|
676
669
|
*/
|
|
677
|
-
type
|
|
678
|
-
/**
|
|
679
|
-
|
|
670
|
+
type LiterateServeRegistration = {
|
|
671
|
+
/** Shell command that starts the server, run from the project root. */
|
|
672
|
+
command: string;
|
|
673
|
+
/** The variable the resolved URL is bound to in every run's child env. */
|
|
674
|
+
env: string;
|
|
675
|
+
/**
|
|
676
|
+
* Matched against the server's output; the FIRST capture group is the port
|
|
677
|
+
* it chose — named (`(?<port>\d+)`) or not, it is group 1 either way.
|
|
678
|
+
*/
|
|
679
|
+
ready: RegExp;
|
|
680
|
+
/** Builds the URL bound to {@link env} from the announced port. */
|
|
681
|
+
url: (port: number) => string;
|
|
682
|
+
};
|
|
683
|
+
/** Per-call options for {@link runSpecDocument} / `cli.run()`. */
|
|
684
|
+
type LiterateRunFlags = {
|
|
685
|
+
/**
|
|
686
|
+
* Opt this document OUT of the update-mode rewrite. A frozen document is
|
|
687
|
+
* NEVER written under `TEST_UPDATE=1`: its mismatch still throws its diff.
|
|
688
|
+
* That is what makes a DELIBERATELY-WRONG `.spec.yaml` — one whose failure
|
|
689
|
+
* rendering is the subject of a negative test — survive an update run
|
|
690
|
+
* instead of being silently corrected into a passing file. The document's
|
|
691
|
+
* mirror of `toMatch(name, { frozen: true })`.
|
|
692
|
+
*/
|
|
693
|
+
frozen?: boolean | undefined;
|
|
680
694
|
};
|
|
681
695
|
//#endregion
|
|
682
|
-
//#region src/
|
|
696
|
+
//#region src/facets/integration/integration.result.d.ts
|
|
697
|
+
/** What `.call()` produced: the value it returned, or the error it threw. */
|
|
698
|
+
type CallOutcome = {
|
|
699
|
+
error: unknown;
|
|
700
|
+
threw: boolean;
|
|
701
|
+
value: unknown;
|
|
702
|
+
};
|
|
703
|
+
type CallResultOptions = BaseResultOptions & {
|
|
704
|
+
outcome: CallOutcome;
|
|
705
|
+
};
|
|
683
706
|
/**
|
|
684
|
-
*
|
|
707
|
+
* What `result.value` is, decided by what the call returned: a string reads as
|
|
708
|
+
* a stream, anything else as JSON. `.call<T>()` already knows T, so the test
|
|
709
|
+
* reads one field (`result.value.value.ok`) without narrowing a union first.
|
|
710
|
+
*/
|
|
711
|
+
type CallValue<Returned> = Returned extends string ? TextAccessor : JsonAccessor<Returned>;
|
|
712
|
+
/**
|
|
713
|
+
* The result of an in-process call — the integration facet's whole surface.
|
|
685
714
|
*
|
|
686
|
-
*
|
|
687
|
-
*
|
|
688
|
-
*
|
|
715
|
+
* Two readings, never both: `value` is what the call RETURNED, `error` is
|
|
716
|
+
* what it THREW. A module that refuses is specified the same way a module
|
|
717
|
+
* that answers is, and neither needs a `try`/`catch` in the test — a shape
|
|
718
|
+
* that costs a refusal spec three times the size of the happy one, and that
|
|
719
|
+
* quietly passes when nothing throws at all.
|
|
689
720
|
*
|
|
690
|
-
*
|
|
691
|
-
*
|
|
721
|
+
* Both are the package's ordinary accessors, so the golden mechanism reaches
|
|
722
|
+
* them, and `Returned` is what `.call<T>()` returned — `result.value.value` is
|
|
723
|
+
* that type, not `unknown`, so a one-field reading needs no golden file: `toMatch('<name>.json'|'<name>.txt')`, the `{{token}}` grammar,
|
|
724
|
+
* `{ frozen }` and `TEST_UPDATE=1` all work here exactly as on an HTTP
|
|
725
|
+
* response or a command's stdout.
|
|
692
726
|
*/
|
|
693
|
-
|
|
727
|
+
declare class CallResult<Returned = unknown> extends BaseResult {
|
|
728
|
+
private readonly outcome;
|
|
729
|
+
constructor(options: CallResultOptions);
|
|
694
730
|
/**
|
|
695
|
-
*
|
|
696
|
-
*
|
|
697
|
-
* set a Redis key prefix.
|
|
731
|
+
* What the call THREW, as text — the message of an `Error`, the value
|
|
732
|
+
* itself when something else was thrown, and empty when it returned.
|
|
698
733
|
*
|
|
699
|
-
*
|
|
734
|
+
* Empty rather than absent on purpose: `await expect(result.error).toBeEmpty()`
|
|
735
|
+
* is how a spec says "and it did not refuse" — awaited, because the
|
|
736
|
+
* matcher answers a promise on every subject it takes, and a dropped one
|
|
737
|
+
* passes the test while the failure surfaces as an unhandled rejection
|
|
738
|
+
* (D2 refuses the bare form).
|
|
700
739
|
*/
|
|
701
|
-
|
|
740
|
+
get error(): TextAccessor;
|
|
702
741
|
/**
|
|
703
|
-
*
|
|
704
|
-
*
|
|
742
|
+
* What the call RETURNED. A string reads as a stream (`<name>.txt`),
|
|
743
|
+
* anything else as JSON (`<name>.json`) — the same two subjects every
|
|
744
|
+
* other facet's results are built from.
|
|
745
|
+
*
|
|
746
|
+
* A call that threw returns the empty value: what it produced is
|
|
747
|
+
* {@link CallResult.error}.
|
|
705
748
|
*/
|
|
706
|
-
|
|
749
|
+
get value(): CallValue<Returned>;
|
|
750
|
+
}
|
|
751
|
+
//#endregion
|
|
752
|
+
//#region src/model/ports/browser.port.d.ts
|
|
753
|
+
/**
|
|
754
|
+
* The landmark roles — the standard set of page regions, and the only
|
|
755
|
+
* containers a scope can name. Closed on purpose: ARIA defines exactly these,
|
|
756
|
+
* so `within()` never becomes a second selector language.
|
|
757
|
+
*/
|
|
758
|
+
type LandmarkKind = 'banner' | 'complementary' | 'contentinfo' | 'form' | 'main' | 'navigation' | 'region' | 'search';
|
|
759
|
+
/**
|
|
760
|
+
* The interactive/textual element kinds — what a visitor actually acts on.
|
|
761
|
+
*
|
|
762
|
+
* `dialog`, `status`, `table`, `row`, `listitem` and `option` are ARIA roles
|
|
763
|
+
* like the landmarks, but they are not CONTAINERS of a page: they are things a
|
|
764
|
+
* visitor reads and acts on, and they are the roles the estate's UI specs were
|
|
765
|
+
* already reaching for through `getByRole` or, worse, through `.first()`.
|
|
766
|
+
*/
|
|
767
|
+
type ElementKind = 'button' | 'dialog' | 'field' | 'heading' | 'link' | 'listitem' | 'option' | 'row' | 'status' | 'table' | 'testId' | 'text';
|
|
768
|
+
/**
|
|
769
|
+
* A user-facing element descriptor — pure data, built by the element
|
|
770
|
+
* vocabulary (`button()`, `link()`, `field()`, …) and translated into
|
|
771
|
+
* concrete locators by the browser integration. CSS/XPath selectors are
|
|
772
|
+
* deliberately not expressible: user-facing elements are the only surface.
|
|
773
|
+
*
|
|
774
|
+
* A descriptor must designate exactly ONE element at action time; see
|
|
775
|
+
* {@link ElementMatch} and CONVENTIONS W3.
|
|
776
|
+
*/
|
|
777
|
+
type ElementRef = {
|
|
707
778
|
/**
|
|
708
|
-
*
|
|
709
|
-
*
|
|
779
|
+
* Match the accessible name as a whole string rather than a substring.
|
|
780
|
+
*
|
|
781
|
+
* ABSENT means "the author said nothing", which the adapters read as the
|
|
782
|
+
* default — exact — while keeping the information that no choice was made,
|
|
783
|
+
* so a name that matches only as a substring can be reported once with the
|
|
784
|
+
* transitional warning.
|
|
710
785
|
*/
|
|
711
|
-
|
|
786
|
+
exact?: boolean;
|
|
787
|
+
/**
|
|
788
|
+
* Assert about the element's FOCUS rather than its presence — built by
|
|
789
|
+
* `focused(element)`. The accessibility tree carries no focus state, so
|
|
790
|
+
* where the keyboard is cannot be goldened; it is asserted by a verb.
|
|
791
|
+
*/
|
|
792
|
+
focused?: boolean;
|
|
793
|
+
/**
|
|
794
|
+
* Narrows to "and it is (not) accepting input" — built by `disabled(x)` /
|
|
795
|
+
* `enabled(x)`. Like `focused`, a STATE the vocabulary can already name the
|
|
796
|
+
* element of; unlike it, one the ARIA tree carries too (`[disabled]`), so
|
|
797
|
+
* this modifier is what names the ONE control a test is about, and it
|
|
798
|
+
* reaches the direction a behavioural substitute cannot — clicking a
|
|
799
|
+
* disabled control is a timeout, never an answer.
|
|
800
|
+
*/
|
|
801
|
+
disabled?: boolean;
|
|
802
|
+
kind: ElementKind | LandmarkKind;
|
|
803
|
+
/** Landmarks may be anonymous (`main()`, `banner()`); everything else is named. */
|
|
804
|
+
name?: string;
|
|
805
|
+
/**
|
|
806
|
+
* Narrows to "and it is (not) the chosen one" — built by `selected(x)`.
|
|
807
|
+
* An option of a COLLAPSED select is in the tree and in the document, and
|
|
808
|
+
* it has no box on the screen, so this facet of it is read from the node
|
|
809
|
+
* (`:checked`) rather than from what a visitor could point at.
|
|
810
|
+
*/
|
|
811
|
+
selected?: boolean;
|
|
812
|
+
/**
|
|
813
|
+
* The value the field must hold — built by `valued(field('Title'), '…')`.
|
|
814
|
+
* A field's value is a PROPERTY of the node: a controlled input's `value`
|
|
815
|
+
* attribute never moves, so nothing but the live value answers for it.
|
|
816
|
+
*/
|
|
817
|
+
value?: string;
|
|
818
|
+
/**
|
|
819
|
+
* Restrict the search to the elements of another descriptor — built by
|
|
820
|
+
* `within(scope, target)`. Chains: a scope may itself carry a scope.
|
|
821
|
+
*/
|
|
822
|
+
scope?: ElementRef;
|
|
712
823
|
};
|
|
713
|
-
//#endregion
|
|
714
|
-
//#region src/specification/ports/service.port.d.ts
|
|
715
824
|
/**
|
|
716
|
-
*
|
|
717
|
-
*
|
|
825
|
+
* One candidate captured when a descriptor matched more than one element —
|
|
826
|
+
* the evidence the ambiguity error enumerates so the author can disambiguate
|
|
827
|
+
* without opening a browser.
|
|
718
828
|
*/
|
|
719
|
-
type
|
|
720
|
-
/** Service type identifier. */
|
|
721
|
-
readonly type: string;
|
|
829
|
+
type ElementMatch = {
|
|
722
830
|
/**
|
|
723
|
-
*
|
|
724
|
-
*
|
|
725
|
-
*
|
|
726
|
-
* kebab-case conversion of the key — so `{ analyticsDb: postgres() }` binds
|
|
727
|
-
* to the compose service `analytics-db` without any option. Set
|
|
728
|
-
* `composeService` explicitly only for names the key cannot derive.
|
|
831
|
+
* The accessible name, when it differs from the text — an `aria-label` is
|
|
832
|
+
* what a role descriptor MATCHED on, and printing the text alone sends the
|
|
833
|
+
* author to disambiguate a name they cannot see.
|
|
729
834
|
*/
|
|
730
|
-
|
|
731
|
-
/**
|
|
732
|
-
|
|
733
|
-
/**
|
|
734
|
-
|
|
735
|
-
/**
|
|
736
|
-
|
|
737
|
-
/**
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
835
|
+
accessibleName?: string | undefined;
|
|
836
|
+
/** Nearest landmark ancestor (`nav`, `footer`, `main`…), when there is one. */
|
|
837
|
+
context?: string | undefined;
|
|
838
|
+
/** The attribute that disambiguates most — `href` for links, `name` for fields. */
|
|
839
|
+
detail?: string | undefined;
|
|
840
|
+
/** Tag name, lower-cased. */
|
|
841
|
+
tag: string;
|
|
842
|
+
/** Text content, whitespace-collapsed and truncated. */
|
|
843
|
+
text: string;
|
|
844
|
+
};
|
|
845
|
+
/** A `<link>` element captured from the rendered document's head. */
|
|
846
|
+
type BrowserLinkElement = {
|
|
847
|
+
href: string;
|
|
848
|
+
hreflang?: string | undefined;
|
|
849
|
+
rel: string;
|
|
850
|
+
type?: string | undefined;
|
|
851
|
+
};
|
|
852
|
+
/** A `<meta>` element captured from the rendered document's head. */
|
|
853
|
+
type BrowserMetaElement = {
|
|
854
|
+
content: string;
|
|
855
|
+
name?: string | undefined;
|
|
856
|
+
property?: string | undefined;
|
|
857
|
+
};
|
|
858
|
+
/** A console message emitted while the page loaded or the scenario ran. */
|
|
859
|
+
type BrowserConsoleMessage = {
|
|
860
|
+
text: string;
|
|
861
|
+
type: string;
|
|
862
|
+
};
|
|
863
|
+
/**
|
|
864
|
+
* The visitor — the interaction vocabulary handed to a visit scenario.
|
|
865
|
+
* Every action auto-waits (playwright actionability); `see()` is the single
|
|
866
|
+
* synchronization primitive: it retries until the element is visible and
|
|
867
|
+
* fails at the timeout. There is no sleep and no conditional helper.
|
|
868
|
+
*/
|
|
869
|
+
type Visitor = {
|
|
870
|
+
/** Check a checkbox or radio. */
|
|
871
|
+
check: (element: ElementRef) => Promise<void>;
|
|
872
|
+
/** Click the element. */
|
|
873
|
+
click: (element: ElementRef) => Promise<void>;
|
|
874
|
+
/** Fill a form field with a value. */
|
|
875
|
+
fill: (element: ElementRef, value: string) => Promise<void>;
|
|
747
876
|
/**
|
|
748
|
-
*
|
|
749
|
-
*
|
|
750
|
-
*
|
|
751
|
-
* `composeDir` is where the compose file lives — where a service reads its
|
|
752
|
-
* `init.sql` from. `root` is the project root the specification resolved
|
|
753
|
-
* (A9), handed down so a service that CACHES something writes it under the
|
|
754
|
-
* project's own `.artifacts/`, never in a machine-global directory two
|
|
755
|
-
* checkouts would share.
|
|
877
|
+
* Wait until the element is GONE — hidden, or never in the document. The
|
|
878
|
+
* absence primitive: `see()` cannot answer "the dialog closed", because an
|
|
879
|
+
* element that never appears and one that disappeared read the same to it.
|
|
756
880
|
*/
|
|
757
|
-
|
|
758
|
-
/**
|
|
759
|
-
|
|
760
|
-
/**
|
|
761
|
-
|
|
881
|
+
gone: (element: ElementRef) => Promise<void>;
|
|
882
|
+
/** Navigate to a path of the site under test. */
|
|
883
|
+
goto: (path: string) => Promise<void>;
|
|
884
|
+
/** Hover the element. */
|
|
885
|
+
hover: (element: ElementRef) => Promise<void>;
|
|
886
|
+
/** Press a key (e.g. `Enter`). */
|
|
887
|
+
press: (key: string) => Promise<void>;
|
|
888
|
+
/** Wait until the element is visible — the only synchronization primitive. */
|
|
889
|
+
see: (element: ElementRef) => Promise<void>;
|
|
890
|
+
/** Select an option in a select field. */
|
|
891
|
+
select: (element: ElementRef, option: string) => Promise<void>;
|
|
892
|
+
};
|
|
893
|
+
/** The behavior of a visit — the When of the spec; assertions stay in the Then. */
|
|
894
|
+
type VisitScenario = (visitor: Visitor) => Promise<void>;
|
|
895
|
+
/** Per-visit options forwarded to the browser context. */
|
|
896
|
+
type BrowserOpenOptions = {
|
|
897
|
+
/**
|
|
898
|
+
* Extra origins the `external: 'block'` policy lets through — the
|
|
899
|
+
* declared stub backend the page legitimately fetches from.
|
|
900
|
+
*/
|
|
901
|
+
allowedOrigins?: string[] | undefined;
|
|
902
|
+
/**
|
|
903
|
+
* Base URL of the site under test — the origin `goto()` resolves against
|
|
904
|
+
* and the boundary of the `external` policy.
|
|
905
|
+
*/
|
|
906
|
+
baseUrl: string;
|
|
907
|
+
/**
|
|
908
|
+
* Pin the page's own `Date` at this instant before the first byte is
|
|
909
|
+
* parsed — the calendar the site's scripts read. `.clock()` on the chain.
|
|
910
|
+
*/
|
|
911
|
+
clock?: string | undefined;
|
|
912
|
+
/**
|
|
913
|
+
* Cross-origin request policy. `'block'` aborts every request leaving
|
|
914
|
+
* the site under test (analytics, CDNs) — the browser-side analog of
|
|
915
|
+
* strict intercepts. `'allow'` lets them through (deployed-site mode).
|
|
916
|
+
*/
|
|
917
|
+
external: 'allow' | 'block';
|
|
918
|
+
/** Extra HTTP headers sent with every request of the visit (incl. User-Agent overrides). */
|
|
919
|
+
headers?: Record<string, string> | undefined;
|
|
920
|
+
/** The interaction scenario to run after load; the capture reflects the final state. */
|
|
921
|
+
scenario?: undefined | VisitScenario;
|
|
762
922
|
};
|
|
763
|
-
//#endregion
|
|
764
|
-
//#region src/specification/facets/_common/result/response.d.ts
|
|
765
923
|
/**
|
|
766
|
-
*
|
|
767
|
-
*
|
|
768
|
-
*
|
|
769
|
-
* `expect(result.response).toMatch('created.http')` compares status,
|
|
770
|
-
* a subset of headers, and the JSON body against `_expected/<name>` —
|
|
771
|
-
* with `{{placeholder}}` support in all three.
|
|
924
|
+
* The rendered page captured by a browser visit — the FINAL state when a
|
|
925
|
+
* scenario ran. Extraction happens in-page (the browser IS the HTML
|
|
926
|
+
* parser); interpretation of the raw elements belongs to the model.
|
|
772
927
|
*/
|
|
773
|
-
|
|
774
|
-
/**
|
|
775
|
-
|
|
776
|
-
/**
|
|
777
|
-
|
|
778
|
-
/**
|
|
779
|
-
|
|
780
|
-
/**
|
|
781
|
-
|
|
782
|
-
/**
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
/**
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
});
|
|
796
|
-
/** Access the HTTP response (status, headers, body) for assertions. */
|
|
797
|
-
get response(): ResponseAccessor;
|
|
798
|
-
/** The HTTP response status code. */
|
|
799
|
-
get status(): number;
|
|
800
|
-
}
|
|
801
|
-
//#endregion
|
|
802
|
-
//#region src/specification/facets/_common/result/filesystem.d.ts
|
|
928
|
+
type BrowserPage = {
|
|
929
|
+
/** Console messages emitted while loading and interacting, in order. */
|
|
930
|
+
consoleMessages: BrowserConsoleMessage[];
|
|
931
|
+
/** Serialized DOM after rendering (`document.documentElement.outerHTML`). */
|
|
932
|
+
html: string;
|
|
933
|
+
/** Raw text content of every `application/ld+json` script, in DOM order. */
|
|
934
|
+
jsonLdBlocks: string[];
|
|
935
|
+
/** `<link>` elements of the head, in DOM order. */
|
|
936
|
+
links: BrowserLinkElement[];
|
|
937
|
+
/** `<meta>` elements of the head, in DOM order. */
|
|
938
|
+
metas: BrowserMetaElement[];
|
|
939
|
+
/** HTTP status of the main document response (0 when unavailable). */
|
|
940
|
+
status: number;
|
|
941
|
+
/** Rendered `document.body.innerText`. */
|
|
942
|
+
text: string;
|
|
943
|
+
/** The ARIA snapshot of the rendered `<body>` — the outline a golden pins. */
|
|
944
|
+
tree: string;
|
|
945
|
+
/** `document.title` after rendering. */
|
|
946
|
+
title: string;
|
|
947
|
+
/** Final URL after redirects and scenario navigation. */
|
|
948
|
+
url: string;
|
|
949
|
+
};
|
|
803
950
|
/**
|
|
804
|
-
*
|
|
805
|
-
*
|
|
806
|
-
*
|
|
807
|
-
* Assertions go through `expect()` (async — they walk the disk):
|
|
808
|
-
* `await expect(result.filesystem).toMatch('scaffolded')` compares the
|
|
809
|
-
* tree against the fixture directory `_expected/<name>/`.
|
|
951
|
+
* Abstract browser interface for the website specification runner.
|
|
952
|
+
* One implementation lives in `integrations/playwright/` — a single shared
|
|
953
|
+
* browser instance per runner; each `open()` gets a fresh, isolated context.
|
|
810
954
|
*/
|
|
811
|
-
|
|
812
|
-
/**
|
|
813
|
-
|
|
814
|
-
/**
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
readonly testDir: string;
|
|
818
|
-
constructor(cwd: string, testDir: string, captures?: CaptureScope);
|
|
819
|
-
/** List all files (recursively) under the working directory, sorted. */
|
|
820
|
-
files(options?: {
|
|
821
|
-
ignore?: string[];
|
|
822
|
-
}): Promise<string[]>;
|
|
823
|
-
}
|
|
955
|
+
type BrowserPort = {
|
|
956
|
+
/** Close the shared browser instance (idempotent). */
|
|
957
|
+
close: () => Promise<void>;
|
|
958
|
+
/** Load `url` in a fresh context, run the scenario, capture the final page. */
|
|
959
|
+
open: (url: string, options: BrowserOpenOptions) => Promise<BrowserPage>;
|
|
960
|
+
};
|
|
824
961
|
//#endregion
|
|
825
|
-
//#region src/
|
|
962
|
+
//#region src/model/ports/device.port.d.ts
|
|
826
963
|
/**
|
|
827
|
-
*
|
|
828
|
-
*
|
|
829
|
-
*
|
|
830
|
-
*
|
|
831
|
-
*
|
|
832
|
-
* `String(result.stdout)` and template-literal interpolation still work.
|
|
833
|
-
*
|
|
834
|
-
* ANSI escape sequences are stripped by default before every comparison
|
|
835
|
-
* (CONVENTIONS D6) — `.text` stays raw. The runner `transform` option remains
|
|
836
|
-
* an escape hatch for application noise not covered by the `{{token}}`
|
|
837
|
-
* grammar.
|
|
838
|
-
*
|
|
839
|
-
* Text operations are **closed** over the type: `.grep(pattern)` returns a
|
|
840
|
-
* `TextAccessor` (not a bare string), preserving the `_expected/`-resolution
|
|
841
|
-
* context and the `transform`, so results are chainable
|
|
842
|
-
* (`result.stdout.grep(a).grep(b)`) and snapshot-able
|
|
843
|
-
* (`expect(result.stdout.grep('users.ts')).toMatch('block.txt')`).
|
|
844
|
-
*
|
|
845
|
-
* Assertions go through `expect()` matchers: `expect(result.stdout).toContain(...)`
|
|
846
|
-
* and `expect(result.stdout).toMatch('name.txt')` (resolved against
|
|
847
|
-
* `_expected/<name>`, flat — with `{{token}}` support, CONVENTIONS D4).
|
|
964
|
+
* The element kinds a mobile screen can designate — the structural subset of
|
|
965
|
+
* {@link ElementRef} kinds that map onto the XCUITest accessibility tree.
|
|
966
|
+
* There is ONE element vocabulary across facets: `button('Bookmark')` works
|
|
967
|
+
* in a visit scenario and a mobile scenario alike. Landmarks have no iOS
|
|
968
|
+
* analog — passing one to a mobile verb refuses at runtime.
|
|
848
969
|
*/
|
|
849
|
-
|
|
850
|
-
/** @internal Ref-capture scope shared by the current spec execution. */
|
|
851
|
-
readonly captures: CaptureScope;
|
|
852
|
-
/** @internal Stream label used in failure messages ("stdout" / "stderr"). */
|
|
853
|
-
readonly streamName: string;
|
|
854
|
-
/** @internal Test-file directory — fixture resolution root for matchers. */
|
|
855
|
-
readonly testDir: string;
|
|
856
|
-
/** The raw captured text (never transformed, ANSI preserved). */
|
|
857
|
-
readonly text: string;
|
|
858
|
-
/** @internal Normaliser applied before comparisons, never to fixtures. */
|
|
859
|
-
readonly transform?: ((text: string) => string) | undefined;
|
|
860
|
-
constructor(value: string, streamName: string, testDir: string, options?: {
|
|
861
|
-
captures?: CaptureScope | undefined;
|
|
862
|
-
transform?: ((text: string) => string) | undefined;
|
|
863
|
-
});
|
|
864
|
-
/** @internal The text as compared by matchers — ANSI stripped, transform applied. */
|
|
865
|
-
get comparableText(): string;
|
|
866
|
-
/**
|
|
867
|
-
* Keep only the blank-line-separated blocks of the text that contain
|
|
868
|
-
* `pattern` (how linter/compiler output is structured), returned as a new
|
|
869
|
-
* `TextAccessor` — the same subject type, so the result is chainable and
|
|
870
|
-
* snapshot-able. The scalpel for probing large tool outputs; snapshot the
|
|
871
|
-
* whole surface by default, reach for `.grep()` for targeted checks.
|
|
872
|
-
*/
|
|
873
|
-
grep(pattern: string): TextAccessor;
|
|
874
|
-
toString(): string;
|
|
875
|
-
valueOf(): string;
|
|
876
|
-
}
|
|
877
|
-
//#endregion
|
|
878
|
-
//#region src/specification/facets/cli/result.d.ts
|
|
970
|
+
type MobileElementKind = 'button' | 'field' | 'testId' | 'text';
|
|
879
971
|
/**
|
|
880
|
-
*
|
|
881
|
-
*
|
|
882
|
-
*
|
|
883
|
-
*
|
|
884
|
-
*
|
|
885
|
-
*
|
|
886
|
-
* pay zero Docker cost even when the runner is Docker-aware.
|
|
972
|
+
* A user-facing element descriptor for mobile scenarios — pure data, built by
|
|
973
|
+
* the shared element vocabulary (`button()`, `field()`, `content()`,
|
|
974
|
+
* `testId()`, `within()`) and translated into iOS predicate strings by the
|
|
975
|
+
* device integration. Structurally the same ref type as the browser's, so the
|
|
976
|
+
* vocabulary stays single; kinds outside {@link MobileElementKind} (the ARIA
|
|
977
|
+
* landmarks) are refused at runtime with a message naming the boundary.
|
|
887
978
|
*
|
|
888
|
-
*
|
|
889
|
-
*
|
|
890
|
-
*
|
|
891
|
-
* a container.
|
|
979
|
+
* A descriptor must designate exactly ONE element when a verb ACTS on it
|
|
980
|
+
* (`tap`, `fill`) — see {@link MobileElementMatch} and CONVENTIONS W3.
|
|
981
|
+
* `see()` acts on nothing, so any visible match satisfies it: XCUITest trees
|
|
982
|
+
* legitimately duplicate a label across container and child.
|
|
892
983
|
*/
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
* Returns an accessor with `.exists === false` when no container
|
|
927
|
-
* carries that name — callers can still assert absence without a try.
|
|
928
|
-
*/
|
|
929
|
-
container(name: string): ContainerAccessor;
|
|
930
|
-
/** All captured container IDs. Triggers the same lazy query as `container()`. */
|
|
931
|
-
get containerIds(): string[];
|
|
932
|
-
[Symbol.asyncDispose](): Promise<void>;
|
|
933
|
-
private ensureDockerAware;
|
|
934
|
-
private loadContainers;
|
|
935
|
-
}
|
|
936
|
-
//#endregion
|
|
937
|
-
//#region src/specification/facets/cli/literate.d.ts
|
|
984
|
+
type MobileElementRef = ElementRef;
|
|
985
|
+
/**
|
|
986
|
+
* One candidate captured when a descriptor matched more than one element —
|
|
987
|
+
* the evidence the ambiguity error enumerates so the author can disambiguate
|
|
988
|
+
* without opening the simulator.
|
|
989
|
+
*/
|
|
990
|
+
type MobileElementMatch = {
|
|
991
|
+
/** The accessibility identifier (`testId()` target), when one is set. */
|
|
992
|
+
identifier?: string;
|
|
993
|
+
/** The accessible label, whitespace-collapsed and truncated. */
|
|
994
|
+
label?: string;
|
|
995
|
+
/** Element type without the `XCUIElementType` prefix — `Button`, `StaticText`. */
|
|
996
|
+
type: string;
|
|
997
|
+
/** The element value (text-field content, adjustable value), when present. */
|
|
998
|
+
value?: string;
|
|
999
|
+
};
|
|
1000
|
+
/**
|
|
1001
|
+
* The visitor — the interaction vocabulary handed to a mobile scenario.
|
|
1002
|
+
* Every verb auto-waits by polling until a visible match exists; acting
|
|
1003
|
+
* verbs then enforce exactly-one (W3), while `see()` — the single
|
|
1004
|
+
* synchronization primitive — is satisfied by any visible match. There is
|
|
1005
|
+
* no sleep and no conditional helper.
|
|
1006
|
+
*/
|
|
1007
|
+
type MobileVisitor = {
|
|
1008
|
+
/** Fill a text field with a value. */
|
|
1009
|
+
fill: (element: MobileElementRef, value: string) => Promise<void>;
|
|
1010
|
+
/** Wait until the element is visible — the only synchronization primitive. */
|
|
1011
|
+
see: (element: MobileElementRef) => Promise<void>;
|
|
1012
|
+
/** Tap the element. */
|
|
1013
|
+
tap: (element: MobileElementRef) => Promise<void>;
|
|
1014
|
+
};
|
|
1015
|
+
/** The behavior of an open — the When of the spec; assertions stay in the Then. */
|
|
1016
|
+
type MobileScenario = (visitor: MobileVisitor) => Promise<void>;
|
|
938
1017
|
/**
|
|
939
|
-
*
|
|
940
|
-
*
|
|
941
|
-
*
|
|
942
|
-
*
|
|
943
|
-
* anything — {@link copyPlan} for `fixture:`, the runner's own command adapter
|
|
944
|
-
* for each run, {@link ServeAdapter} for `serve:`, the `{{token}}` engine for
|
|
945
|
-
* every comparison, and {@link mergeTextPreservingPlaceholders} for the
|
|
946
|
-
* rewrite. What is new here is only the ORCHESTRATION the chain cannot express:
|
|
947
|
-
* several commands sharing one working directory, each judged the moment it
|
|
948
|
-
* ends — so what a run states about that directory is what IT left there, and
|
|
949
|
-
* the first run to disagree is the one the failure names.
|
|
1018
|
+
* One node of the projected accessibility tree — the XCUITest page source
|
|
1019
|
+
* with its noise collapsed: unlabeled, identifier-less, valueless wrapper
|
|
1020
|
+
* nodes are dropped and their children hoisted, so the projection stays
|
|
1021
|
+
* stable and golden-friendly. Type names lose the `XCUIElementType` prefix.
|
|
950
1022
|
*/
|
|
1023
|
+
type ScreenNode = {
|
|
1024
|
+
children?: ScreenNode[];
|
|
1025
|
+
/** The accessibility identifier, only when it differs from the label. */
|
|
1026
|
+
identifier?: string;
|
|
1027
|
+
label?: string;
|
|
1028
|
+
/** Element type without the `XCUIElementType` prefix — `Button`, `StaticText`. */
|
|
1029
|
+
type: string;
|
|
1030
|
+
value?: string;
|
|
1031
|
+
};
|
|
951
1032
|
/**
|
|
952
|
-
*
|
|
953
|
-
*
|
|
1033
|
+
* The screen captured by a device open — the FINAL state when a scenario
|
|
1034
|
+
* ran. The tree is the projected page source; `texts` are the visible
|
|
1035
|
+
* labels/values in document order, consecutive duplicates collapsed.
|
|
954
1036
|
*/
|
|
955
|
-
type
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
1037
|
+
type DeviceScreen = {
|
|
1038
|
+
texts: string[];
|
|
1039
|
+
tree: ScreenNode;
|
|
1040
|
+
};
|
|
1041
|
+
/** Per-open options forwarded to the device session. */
|
|
1042
|
+
type DeviceOpenOptions = {
|
|
1043
|
+
/** The app under test — terminated and relaunched for a fresh, deterministic state. */
|
|
1044
|
+
bundleId: string;
|
|
1045
|
+
/** Deep link applied after the relaunch (`news://events`); absent opens the app plainly. */
|
|
1046
|
+
deepLink?: string | undefined;
|
|
1047
|
+
/** The interaction scenario to run after launch; the capture reflects the final state. */
|
|
1048
|
+
scenario?: MobileScenario | undefined;
|
|
1049
|
+
};
|
|
1050
|
+
/**
|
|
1051
|
+
* How long a device session waits, in milliseconds. Every field is optional
|
|
1052
|
+
* and falls back to the framework's own default, so a runner states only the
|
|
1053
|
+
* wait it needs to move — the defaults suit a release build, and a project
|
|
1054
|
+
* that drives a DEV build pays for its bundler's cold boot.
|
|
1055
|
+
*/
|
|
1056
|
+
type DeviceTimeouts = {
|
|
960
1057
|
/**
|
|
961
|
-
*
|
|
962
|
-
*
|
|
1058
|
+
* How long every verb polls for a visible match before refusing.
|
|
1059
|
+
* Default: `30_000` — enough for a cold app boot on a release bundle.
|
|
963
1060
|
*/
|
|
964
|
-
|
|
965
|
-
/** Builds the URL bound to {@link env} from the announced port. */
|
|
966
|
-
url: (port: number) => string;
|
|
967
|
-
};
|
|
968
|
-
/** Per-call options for {@link runSpecDocument} / `cli.run()`. */
|
|
969
|
-
type LiterateRunFlags = {
|
|
1061
|
+
action?: number | undefined;
|
|
970
1062
|
/**
|
|
971
|
-
*
|
|
972
|
-
*
|
|
973
|
-
* That is what makes a DELIBERATELY-WRONG `.spec.yaml` — one whose failure
|
|
974
|
-
* rendering is the subject of a negative test — survive an update run
|
|
975
|
-
* instead of being silently corrected into a passing file. The document's
|
|
976
|
-
* mirror of `toMatch(name, { frozen: true })`.
|
|
1063
|
+
* How long the FIRST session waits for WebDriverAgent to build and
|
|
1064
|
+
* launch on the simulator. Default: `240_000`.
|
|
977
1065
|
*/
|
|
978
|
-
|
|
1066
|
+
launch?: number | undefined;
|
|
1067
|
+
};
|
|
1068
|
+
/**
|
|
1069
|
+
* Abstract device interface for the mobile specification runner.
|
|
1070
|
+
* One implementation lives in `integrations/appium/` — a single driver
|
|
1071
|
+
* session per runner, created on the first `open()` and reused; each open
|
|
1072
|
+
* terminates and relaunches the app for a deterministic fresh state.
|
|
1073
|
+
*/
|
|
1074
|
+
type DevicePort = {
|
|
1075
|
+
/** End the driver session (idempotent). */
|
|
1076
|
+
close: () => Promise<void>;
|
|
1077
|
+
/** Relaunch the app, apply the deep link, run the scenario, capture the final screen. */
|
|
1078
|
+
open: (options: DeviceOpenOptions) => Promise<DeviceScreen>;
|
|
979
1079
|
};
|
|
980
1080
|
//#endregion
|
|
981
|
-
//#region src/
|
|
1081
|
+
//#region src/facets/mobile/mobile.result.d.ts
|
|
982
1082
|
/** Result from a `.open()` action — the screen as the device saw it, final state. */
|
|
983
1083
|
declare class ScreenResult extends BaseResult {
|
|
984
1084
|
private readonly capturedScreen;
|
|
@@ -999,7 +1099,7 @@ declare class ScreenResult extends BaseResult {
|
|
|
999
1099
|
get screen(): JsonAccessor;
|
|
1000
1100
|
}
|
|
1001
1101
|
//#endregion
|
|
1002
|
-
//#region src/
|
|
1102
|
+
//#region src/facets/website/website.result.d.ts
|
|
1003
1103
|
/** A raw HTTP exchange captured by `.fetch()` — redirects are NOT followed. */
|
|
1004
1104
|
type FetchExchange = {
|
|
1005
1105
|
body: string;
|
|
@@ -1083,7 +1183,88 @@ declare class PageResult extends BaseResult {
|
|
|
1083
1183
|
get url(): string;
|
|
1084
1184
|
}
|
|
1085
1185
|
//#endregion
|
|
1086
|
-
//#region src/
|
|
1186
|
+
//#region src/model/ports/isolation.port.d.ts
|
|
1187
|
+
/**
|
|
1188
|
+
* Strategy for isolating service state across parallel test workers.
|
|
1189
|
+
*
|
|
1190
|
+
* Each service handle provides an isolation strategy. The framework
|
|
1191
|
+
* calls `acquire()` once per vitest worker, `reset()` before each
|
|
1192
|
+
* `spec.run()`, and `release()` when the worker shuts down.
|
|
1193
|
+
*
|
|
1194
|
+
* Implement this interface to support new service types (e.g. MongoDB,
|
|
1195
|
+
* Elasticsearch, S3).
|
|
1196
|
+
*/
|
|
1197
|
+
type IsolationStrategy = {
|
|
1198
|
+
/**
|
|
1199
|
+
* Create an isolated namespace for this worker.
|
|
1200
|
+
* Called once when the worker starts — e.g. clone a template database,
|
|
1201
|
+
* set a Redis key prefix.
|
|
1202
|
+
*
|
|
1203
|
+
* @param workerId - Unique identifier for this vitest worker.
|
|
1204
|
+
*/
|
|
1205
|
+
acquire: (workerId: string) => Promise<void>;
|
|
1206
|
+
/**
|
|
1207
|
+
* Fast reset within the namespace between `spec.run()` calls.
|
|
1208
|
+
* E.g. truncate tables (without dropping the database).
|
|
1209
|
+
*/
|
|
1210
|
+
reset: () => Promise<void>;
|
|
1211
|
+
/**
|
|
1212
|
+
* Tear down the isolated namespace.
|
|
1213
|
+
* Called once when the worker shuts down — e.g. drop the cloned database.
|
|
1214
|
+
*/
|
|
1215
|
+
release: () => Promise<void>;
|
|
1216
|
+
};
|
|
1217
|
+
//#endregion
|
|
1218
|
+
//#region src/model/ports/service.port.d.ts
|
|
1219
|
+
/**
|
|
1220
|
+
* A service handle — returned by factory functions like postgres(), redis().
|
|
1221
|
+
* Mutable: connectionString is populated after the orchestrator starts containers.
|
|
1222
|
+
*/
|
|
1223
|
+
type ServiceHandle = {
|
|
1224
|
+
/** Service type identifier. */
|
|
1225
|
+
readonly type: string;
|
|
1226
|
+
/**
|
|
1227
|
+
* The name this handle is known by. Left `null` until the orchestrator
|
|
1228
|
+
* assigns it at start time: it is the kebab-case form of the RECORD KEY,
|
|
1229
|
+
* so `{ analyticsDb: postgres() }` reports as `analytics-db` and reads its
|
|
1230
|
+
* init script from `docker/analytics-db/`. The key is the only name a
|
|
1231
|
+
* specification writes; a handle carries no second one of its own.
|
|
1232
|
+
*/
|
|
1233
|
+
serviceName: null | string;
|
|
1234
|
+
/** Default container port for this service type. */
|
|
1235
|
+
readonly defaultPort: number;
|
|
1236
|
+
/** Default Docker image for this service type. */
|
|
1237
|
+
readonly defaultImage: string;
|
|
1238
|
+
/** Environment variables to pass to the container. */
|
|
1239
|
+
readonly environment: Record<string, string>;
|
|
1240
|
+
/** Connection string — populated after start. */
|
|
1241
|
+
connectionString: string;
|
|
1242
|
+
/** Whether this service has been started. */
|
|
1243
|
+
started: boolean;
|
|
1244
|
+
/** Build the connection string from host and port. */
|
|
1245
|
+
buildConnectionString: (host: string, port: number) => string;
|
|
1246
|
+
/** Create a DatabasePort adapter (if this is a database). Returns null otherwise. */
|
|
1247
|
+
createDatabaseAdapter: () => DatabasePort | null;
|
|
1248
|
+
/** Verify the service is ready and accepting connections. Throws with context if not. */
|
|
1249
|
+
healthcheck: () => Promise<void>;
|
|
1250
|
+
/**
|
|
1251
|
+
* Run initialization scripts (e.g., init.sql). Throws with SQL error
|
|
1252
|
+
* context if it fails.
|
|
1253
|
+
*
|
|
1254
|
+
* `dockerDir` is `<root>/docker` — where a service reads its `init.sql`
|
|
1255
|
+
* from, under its own name. `root` is the project root the specification resolved
|
|
1256
|
+
* (A9), handed down so a service that CACHES something writes it under the
|
|
1257
|
+
* project's own `.artifacts/`, never in a machine-global directory two
|
|
1258
|
+
* checkouts would share.
|
|
1259
|
+
*/
|
|
1260
|
+
initialize: (dockerDir: string, root: string) => Promise<void>;
|
|
1261
|
+
/** Reset state between tests (truncate tables, flush cache, etc.) */
|
|
1262
|
+
reset: () => Promise<void>;
|
|
1263
|
+
/** Get the isolation strategy for parallel test execution. */
|
|
1264
|
+
isolation: () => IsolationStrategy;
|
|
1265
|
+
};
|
|
1266
|
+
//#endregion
|
|
1267
|
+
//#region src/model/chain/stub-backend.d.ts
|
|
1087
1268
|
/** Options for the stub backend server. */
|
|
1088
1269
|
type StubBackendOptions = {
|
|
1089
1270
|
/**
|
|
@@ -1125,7 +1306,7 @@ declare class StubBackend {
|
|
|
1125
1306
|
private record;
|
|
1126
1307
|
}
|
|
1127
1308
|
//#endregion
|
|
1128
|
-
//#region src/
|
|
1309
|
+
//#region src/model/chain/builder.d.ts
|
|
1129
1310
|
/** A named job that can be triggered via jobs.trigger(). */
|
|
1130
1311
|
type JobHandle = {
|
|
1131
1312
|
name: string;
|
|
@@ -1194,16 +1375,17 @@ type SpecificationConfig = {
|
|
|
1194
1375
|
* facet factories auto-populate this when `dockerConfig` is present.
|
|
1195
1376
|
*/
|
|
1196
1377
|
dockerTestRunId?: string | undefined;
|
|
1378
|
+
/**
|
|
1379
|
+
* Environment applied to every run of the declared binary
|
|
1380
|
+
* (`specification.cli({ defaults })`) — under a chain's own `.env()` and
|
|
1381
|
+
* under a document's `env:`, both of which win.
|
|
1382
|
+
*/
|
|
1383
|
+
defaultEnv?: CliEnv | undefined;
|
|
1197
1384
|
/**
|
|
1198
1385
|
* Named environment SETS a spec document may name by bare word
|
|
1199
1386
|
* (`env: frozen`). Declared once per app in `specification.cli()`.
|
|
1200
1387
|
*/
|
|
1201
1388
|
envSets?: Record<string, CliEnv> | undefined;
|
|
1202
|
-
/**
|
|
1203
|
-
* When set, `.intercept()` is unavailable on this runner and throws this
|
|
1204
|
-
* reason immediately (compose mode — MSW is in-process, CONVENTIONS I3).
|
|
1205
|
-
*/
|
|
1206
|
-
interceptDisabledReason?: string | undefined;
|
|
1207
1389
|
jobs?: JobHandle[] | undefined;
|
|
1208
1390
|
/** The project root — the working directory a document's `serve:` command runs from. */
|
|
1209
1391
|
root?: string | undefined;
|
|
@@ -1240,6 +1422,8 @@ type InterceptMethod<T> = ((contracts: ContractInput) => T) & ((request: Contrac
|
|
|
1240
1422
|
* of the declared services record that are databases.
|
|
1241
1423
|
*/
|
|
1242
1424
|
type ApiSpecification<DatabaseKey extends string = string> = {
|
|
1425
|
+
/** Pin the app's `Date` at `iso` for this chain — the calendar the app reads. */
|
|
1426
|
+
clock: (iso: string) => ApiSpecification<DatabaseKey>;
|
|
1243
1427
|
/** Set HTTP headers for the request. Multiple calls merge. */
|
|
1244
1428
|
headers: (headers: Record<string, string>) => ApiSpecification<DatabaseKey>;
|
|
1245
1429
|
/** Declare outgoing calls — a contract, a list, a composite, or an inline request + response pair. */
|
|
@@ -1261,9 +1445,21 @@ type ApiSpecification<DatabaseKey extends string = string> = {
|
|
|
1261
1445
|
};
|
|
1262
1446
|
/**
|
|
1263
1447
|
* The `jobs` facet — job chain entry handed out by `specification.jobs()`.
|
|
1264
|
-
* Jobs run in-process by definition (CONVENTIONS
|
|
1448
|
+
* Jobs run in-process by definition (CONVENTIONS A8).
|
|
1449
|
+
*/
|
|
1450
|
+
/**
|
|
1451
|
+
* What a triggered job HANDS BACK.
|
|
1452
|
+
*
|
|
1453
|
+
* A job writes to a database, a cache, a queue — never to a working directory
|
|
1454
|
+
* the spec then walks, because a job has none: it runs in this process, where
|
|
1455
|
+
* the only filesystem is the repository's. So the two accessors that read one
|
|
1456
|
+
* (`file()`, `directory()`) are not on this result. A pipeline that really
|
|
1457
|
+
* produces files is a binary, and `specification.cli()` is its facet.
|
|
1265
1458
|
*/
|
|
1459
|
+
type JobsResult = Omit<BaseResult, 'directory' | 'file'>;
|
|
1266
1460
|
type JobsSpecification<DatabaseKey extends string = string> = {
|
|
1461
|
+
/** Pin the job's `Date` at `iso` for this chain — the calendar the job reads. */
|
|
1462
|
+
clock: (iso: string) => JobsSpecification<DatabaseKey>;
|
|
1267
1463
|
/** Declare outgoing calls — a contract, a list, a composite, or an inline request + response pair. */
|
|
1268
1464
|
intercept: InterceptMethod<JobsSpecification<DatabaseKey>>;
|
|
1269
1465
|
/** Queue a SQL seed file from `_seeds/` to run before the action. */
|
|
@@ -1271,7 +1467,32 @@ type JobsSpecification<DatabaseKey extends string = string> = {
|
|
|
1271
1467
|
database?: DatabaseKey;
|
|
1272
1468
|
}) => JobsSpecification<DatabaseKey>;
|
|
1273
1469
|
/** Execute the named job registered via the `jobs` option and resolve with the result. */
|
|
1274
|
-
trigger: (name: string) => Promise<
|
|
1470
|
+
trigger: (name: string) => Promise<JobsResult>;
|
|
1471
|
+
};
|
|
1472
|
+
/**
|
|
1473
|
+
* The `integration` facet — the in-process chain handed out by
|
|
1474
|
+
* `specification.integration()`.
|
|
1475
|
+
*
|
|
1476
|
+
* The subject is a MODULE, not an entry: no HTTP, no binary, no page. What
|
|
1477
|
+
* makes it a spec rather than a module test is what it stands on — real
|
|
1478
|
+
* services, declared contracts, a golden — so it takes the same setups as
|
|
1479
|
+
* every other facet and ends on the one action a module has: being called.
|
|
1480
|
+
*/
|
|
1481
|
+
type IntegrationSpecification<Services extends Record<string, unknown> = Record<string, unknown>, DatabaseKey extends string = string> = {
|
|
1482
|
+
/** Pin the module's `Date` at `iso` for this chain. */
|
|
1483
|
+
clock: (iso: string) => IntegrationSpecification<Services, DatabaseKey>;
|
|
1484
|
+
/** Declare outgoing calls — a contract, a list, a composite, or an inline request + response pair. */
|
|
1485
|
+
intercept: InterceptMethod<IntegrationSpecification<Services, DatabaseKey>>;
|
|
1486
|
+
/** Queue a SQL seed file from `_seeds/` to run before the call. */
|
|
1487
|
+
seed: (file: string, options?: {
|
|
1488
|
+
database?: DatabaseKey;
|
|
1489
|
+
}) => IntegrationSpecification<Services, DatabaseKey>;
|
|
1490
|
+
/**
|
|
1491
|
+
* Call the module and resolve with what it produced. The started services
|
|
1492
|
+
* record is handed in, so the subject is constructed with the real
|
|
1493
|
+
* connection strings rather than with a double.
|
|
1494
|
+
*/
|
|
1495
|
+
call: <T>(subject: (services: Services) => Promise<T> | T) => Promise<CallResult<T>>;
|
|
1275
1496
|
};
|
|
1276
1497
|
/**
|
|
1277
1498
|
* The `cli` facet — command chain entry handed out by `specification.cli()`.
|
|
@@ -1317,6 +1538,11 @@ type CliSpecification<DatabaseKey extends string = string> = {
|
|
|
1317
1538
|
* performs one raw HTTP exchange and never follows redirects.
|
|
1318
1539
|
*/
|
|
1319
1540
|
type WebsiteSpecification = {
|
|
1541
|
+
/**
|
|
1542
|
+
* Pin the PAGE's `Date` at `iso` for this chain — what the site's own
|
|
1543
|
+
* scripts read. A `.fetch()` opens no page and refuses the setup.
|
|
1544
|
+
*/
|
|
1545
|
+
clock: (iso: string) => WebsiteSpecification;
|
|
1320
1546
|
/** Set HTTP headers for the exchange (incl. User-Agent overrides). Multiple calls merge. */
|
|
1321
1547
|
headers: (headers: Record<string, string>) => WebsiteSpecification;
|
|
1322
1548
|
/**
|
|
@@ -1341,104 +1567,78 @@ type WebsiteSpecification = {
|
|
|
1341
1567
|
*/
|
|
1342
1568
|
type MobileSpecification = {
|
|
1343
1569
|
/**
|
|
1344
|
-
* Declare the chain's backend contracts — served by the declared stub
|
|
1345
|
-
* backend (requires the runner's `backend` option). Multiple calls append.
|
|
1346
|
-
*/
|
|
1347
|
-
intercept: InterceptMethod<MobileSpecification>;
|
|
1348
|
-
/**
|
|
1349
|
-
* Relaunch the app and resolve with the captured screen. With a deep
|
|
1350
|
-
* link, the app opens on it; with a scenario, the visitor interacts
|
|
1351
|
-
* first (the When) and the capture reflects the FINAL screen state.
|
|
1352
|
-
*/
|
|
1353
|
-
open: (deepLink?: string, scenario?: MobileScenario) => Promise<ScreenResult>;
|
|
1354
|
-
};
|
|
1355
|
-
//#endregion
|
|
1356
|
-
//#region src/specification/facets/_common/result/directory.d.ts
|
|
1357
|
-
/**
|
|
1358
|
-
* Read-only accessor for a directory produced by a spec action.
|
|
1359
|
-
*
|
|
1360
|
-
* Assertions go through `expect()` (async — they walk the disk):
|
|
1361
|
-
* `await expect(result.directory('out')).toMatch('scaffold/out')`
|
|
1362
|
-
* compares the tree against the fixture directory `_expected/<name>/`.
|
|
1363
|
-
*/
|
|
1364
|
-
declare class DirectoryAccessor {
|
|
1365
|
-
/** @internal Ref-capture scope shared by the current spec execution. */
|
|
1366
|
-
readonly captures: CaptureScope;
|
|
1367
|
-
/** @internal Absolute path of the directory under assertion. */
|
|
1368
|
-
readonly root: string;
|
|
1369
|
-
/** @internal Test-file directory — fixture resolution root for matchers. */
|
|
1370
|
-
readonly testDir: string;
|
|
1371
|
-
constructor(absPath: string, testDir: string, captures?: CaptureScope);
|
|
1372
|
-
/** List all files (recursively) under the directory, sorted. */
|
|
1373
|
-
files(options?: {
|
|
1374
|
-
ignore?: string[];
|
|
1375
|
-
}): Promise<string[]>;
|
|
1376
|
-
}
|
|
1377
|
-
//#endregion
|
|
1378
|
-
//#region src/specification/facets/_common/result/table.d.ts
|
|
1379
|
-
/**
|
|
1380
|
-
* Read-only accessor for a database table after a specification run.
|
|
1381
|
-
*
|
|
1382
|
-
* Assertions go through `expect()` (async — they query the database):
|
|
1383
|
-
*
|
|
1384
|
-
* await expect(result.table('users', { database: 'db' })).toMatchRows({
|
|
1385
|
-
* columns: ['name'],
|
|
1386
|
-
* rows: [['Alice']],
|
|
1387
|
-
* });
|
|
1388
|
-
* await expect(result.table('users', { database: 'db' })).toBeEmpty();
|
|
1389
|
-
*/
|
|
1390
|
-
declare class TableAccessor {
|
|
1391
|
-
/** @internal Ref-capture scope shared by the current spec execution. */
|
|
1392
|
-
readonly captures: CaptureScope;
|
|
1393
|
-
/** @internal */
|
|
1394
|
-
private readonly db;
|
|
1395
|
-
/** The table name this accessor reads. */
|
|
1396
|
-
readonly name: string;
|
|
1397
|
-
constructor(name: string, db: DatabasePort, captures?: CaptureScope);
|
|
1398
|
-
/** @internal Query the table — used by the toMatchRows / toBeEmpty matchers. */
|
|
1399
|
-
query(columns: string[]): Promise<unknown[][]>;
|
|
1400
|
-
}
|
|
1401
|
-
//#endregion
|
|
1402
|
-
//#region src/specification/facets/_common/result/result.d.ts
|
|
1403
|
-
/** Read-only handle to a single file produced by a spec action. */
|
|
1404
|
-
type FileAccessor = {
|
|
1405
|
-
/** The UTF-8 text content. Throws if the file does not exist. */
|
|
1406
|
-
readonly content: string;
|
|
1407
|
-
readonly exists: boolean;
|
|
1408
|
-
/** The file text as a {@link TextAccessor}, keeping only blocks matching `pattern`. */
|
|
1409
|
-
grep: (pattern: string) => TextAccessor;
|
|
1410
|
-
};
|
|
1411
|
-
type BaseResultOptions = {
|
|
1412
|
-
config: SpecificationConfig;
|
|
1413
|
-
testDir: string;
|
|
1414
|
-
workDir?: string | undefined;
|
|
1570
|
+
* Declare the chain's backend contracts — served by the declared stub
|
|
1571
|
+
* backend (requires the runner's `backend` option). Multiple calls append.
|
|
1572
|
+
*/
|
|
1573
|
+
intercept: InterceptMethod<MobileSpecification>;
|
|
1574
|
+
/**
|
|
1575
|
+
* Relaunch the app and resolve with the captured screen. With a deep
|
|
1576
|
+
* link, the app opens on it; with a scenario, the visitor interacts
|
|
1577
|
+
* first (the When) and the capture reflects the FINAL screen state.
|
|
1578
|
+
*/
|
|
1579
|
+
open: (deepLink?: string, scenario?: MobileScenario) => Promise<ScreenResult>;
|
|
1415
1580
|
};
|
|
1581
|
+
//#endregion
|
|
1582
|
+
//#region src/facets/cli/cli.result.d.ts
|
|
1416
1583
|
/**
|
|
1417
|
-
*
|
|
1418
|
-
* Extended by HttpResult, CliResult, and used directly by job results.
|
|
1584
|
+
* Result from a command action (`.exec()`).
|
|
1419
1585
|
*
|
|
1420
|
-
*
|
|
1421
|
-
*
|
|
1586
|
+
* When the runner was configured with a `docker` option, the result also
|
|
1587
|
+
* exposes container accessors and participates in `Symbol.asyncDispose`
|
|
1588
|
+
* cleanup. The Docker shell-outs are **lazy**: a test that never calls
|
|
1589
|
+
* `.container(...)` never queries the Docker daemon, so command-only tests
|
|
1590
|
+
* pay zero Docker cost even when the runner is Docker-aware.
|
|
1591
|
+
*
|
|
1592
|
+
* Dispose always runs one final label-filtered `docker ps` to catch
|
|
1593
|
+
* containers that were spawned during `.exec` but never asserted on —
|
|
1594
|
+
* tests still get leak-free cleanup even if they forget to reach into
|
|
1595
|
+
* a container.
|
|
1422
1596
|
*/
|
|
1423
|
-
declare class BaseResult {
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
constructor(options:
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
})
|
|
1438
|
-
|
|
1597
|
+
declare class CliResult extends BaseResult {
|
|
1598
|
+
private readonly commandOutput;
|
|
1599
|
+
private containersCache;
|
|
1600
|
+
private readonly dockerConfig?;
|
|
1601
|
+
private readonly testRunId?;
|
|
1602
|
+
private readonly transform?;
|
|
1603
|
+
constructor(options: {
|
|
1604
|
+
commandOutput: CliOutput;
|
|
1605
|
+
config: SpecificationConfig;
|
|
1606
|
+
dockerConfig?: DockerSpecConfig | undefined;
|
|
1607
|
+
testDir: string;
|
|
1608
|
+
testRunId?: string | undefined;
|
|
1609
|
+
transform?: ((text: string) => string) | undefined;
|
|
1610
|
+
workDir: string;
|
|
1611
|
+
});
|
|
1612
|
+
/** The process exit code. */
|
|
1613
|
+
get exitCode(): number;
|
|
1614
|
+
/** Accessor for the captured standard output with file-based assertions. */
|
|
1615
|
+
get stdout(): TextAccessor;
|
|
1616
|
+
/** Accessor for the captured standard error with file-based assertions. */
|
|
1617
|
+
get stderr(): TextAccessor;
|
|
1618
|
+
/** Accessor for parsing stdout as JSON and asserting against JSON fixtures. */
|
|
1619
|
+
get json(): JsonAccessor;
|
|
1620
|
+
/** Accessor for the temporary working directory the command ran in. */
|
|
1621
|
+
get filesystem(): FilesystemAccessor;
|
|
1622
|
+
/**
|
|
1623
|
+
* Look up a container the command spawned during this run by the value of
|
|
1624
|
+
* its name-label (as declared in the `docker.nameLabel` of the
|
|
1625
|
+
* `specification.cli()` options).
|
|
1626
|
+
* First access triggers a one-shot `docker ps` + `docker inspect`
|
|
1627
|
+
* query and caches the result for the rest of the result's lifetime.
|
|
1628
|
+
* Tests that don't call this never touch Docker.
|
|
1629
|
+
*
|
|
1630
|
+
* Returns an accessor with `.exists === false` when no container
|
|
1631
|
+
* carries that name — callers can still assert absence without a try.
|
|
1632
|
+
*/
|
|
1633
|
+
container(name: string): ContainerAccessor;
|
|
1634
|
+
/** All captured container IDs. Triggers the same lazy query as `container()`. */
|
|
1635
|
+
get containerIds(): string[];
|
|
1636
|
+
[Symbol.asyncDispose](): Promise<void>;
|
|
1637
|
+
private ensureDockerAware;
|
|
1638
|
+
private loadContainers;
|
|
1439
1639
|
}
|
|
1440
1640
|
//#endregion
|
|
1441
|
-
//#region src/
|
|
1641
|
+
//#region src/seams/docker/container-accessor.d.ts
|
|
1442
1642
|
/**
|
|
1443
1643
|
* Assertion accessor for a single Docker container captured by the docker()
|
|
1444
1644
|
* spec mode. Mirrors the shape of {@link CliResult} so tests use the same
|
|
@@ -1491,31 +1691,7 @@ declare class ContainerAccessor {
|
|
|
1491
1691
|
private containerExec;
|
|
1492
1692
|
}
|
|
1493
1693
|
//#endregion
|
|
1494
|
-
//#region src/
|
|
1495
|
-
/**
|
|
1496
|
-
* The per-call option both `toMatch` implementations honour.
|
|
1497
|
-
*
|
|
1498
|
-
* A golden is read from disk under node and through a server command from
|
|
1499
|
-
* inside a page, and BOTH builds must answer `{ frozen: true }` the same way —
|
|
1500
|
-
* so the option is declared here, where the node matchers and the page's
|
|
1501
|
-
* matchers can each reach it, rather than in the runner's own folder, which a
|
|
1502
|
-
* seam may not import (rule I1).
|
|
1503
|
-
*/
|
|
1504
|
-
/**
|
|
1505
|
-
* Per-call options for the fixture-file `toMatch` subjects. `frozen` opts a
|
|
1506
|
-
* single fixture OUT of update-mode rewriting: a frozen fixture is NEVER
|
|
1507
|
-
* written under `TEST_UPDATE=1` (or vitest `-u`) — in update mode a frozen
|
|
1508
|
-
* mismatch still throws its diff, and a frozen missing fixture still throws its
|
|
1509
|
-
* "does not exist" error. This is what makes a DELIBERATELY-WRONG fixture (the
|
|
1510
|
-
* subject of a negative test that asserts the mismatch/error rendering)
|
|
1511
|
-
* survivable across update runs instead of being silently overwritten with the
|
|
1512
|
-
* actual output.
|
|
1513
|
-
*/
|
|
1514
|
-
type MatchFixtureOptions = {
|
|
1515
|
-
frozen?: boolean;
|
|
1516
|
-
};
|
|
1517
|
-
//#endregion
|
|
1518
|
-
//#region src/integrations/vitest-browser/ui.d.ts
|
|
1694
|
+
//#region src/seams/vitest-browser/ui.d.ts
|
|
1519
1695
|
/**
|
|
1520
1696
|
* A vanilla DOM subject: a function handed the container it must fill. Its
|
|
1521
1697
|
* optional return is the teardown, the way a DOM library states one.
|
|
@@ -1539,7 +1715,7 @@ declare module 'vitest/browser' {
|
|
|
1539
1715
|
}
|
|
1540
1716
|
}
|
|
1541
1717
|
//#endregion
|
|
1542
|
-
//#region src/
|
|
1718
|
+
//#region src/seams/vitest-browser/vitest-browser.adapter.d.ts
|
|
1543
1719
|
/** What a mounted subject offers the chain, React or vanilla DOM alike. */
|
|
1544
1720
|
type MountedSurface = {
|
|
1545
1721
|
/** The element the subject was mounted into. */
|
|
@@ -1563,7 +1739,7 @@ declare function componentVerbs(surface: MountedSurface): {
|
|
|
1563
1739
|
unmount: () => Promise<void>;
|
|
1564
1740
|
};
|
|
1565
1741
|
//#endregion
|
|
1566
|
-
//#region src/
|
|
1742
|
+
//#region src/facets/component/rendered-text.d.ts
|
|
1567
1743
|
/**
|
|
1568
1744
|
* A stream captured INSIDE the page.
|
|
1569
1745
|
*
|
|
@@ -1578,7 +1754,7 @@ declare class RenderedText extends TextAccessor {
|
|
|
1578
1754
|
readonly capturedInPage: true;
|
|
1579
1755
|
}
|
|
1580
1756
|
//#endregion
|
|
1581
|
-
//#region src/
|
|
1757
|
+
//#region src/facets/component/component.result.d.ts
|
|
1582
1758
|
/**
|
|
1583
1759
|
* What a render resolves to — the component as the page saw it, after the
|
|
1584
1760
|
* scenario ran.
|
|
@@ -1608,7 +1784,7 @@ declare class RenderResult {
|
|
|
1608
1784
|
});
|
|
1609
1785
|
}
|
|
1610
1786
|
//#endregion
|
|
1611
|
-
//#region src/
|
|
1787
|
+
//#region src/facets/component/component.types.d.ts
|
|
1612
1788
|
/**
|
|
1613
1789
|
* The component facet's shape, stated where BOTH runtimes can read it.
|
|
1614
1790
|
*
|
|
@@ -1662,26 +1838,29 @@ type ComponentChain = {
|
|
|
1662
1838
|
wrap: (wrapper: (ui: ComponentUi) => ComponentUi) => ComponentChain;
|
|
1663
1839
|
};
|
|
1664
1840
|
//#endregion
|
|
1665
|
-
//#region src/
|
|
1841
|
+
//#region src/model/elements/elements.d.ts
|
|
1666
1842
|
/**
|
|
1667
1843
|
* The element vocabulary — user-facing descriptors for visit scenarios.
|
|
1668
1844
|
* Reads like English (`click(link('Articles'))`), translates to
|
|
1669
1845
|
* accessibility-first locators in the browser integration. CSS/XPath is
|
|
1670
1846
|
* deliberately not expressible; `testId()` is the single escape hatch.
|
|
1671
1847
|
*
|
|
1672
|
-
* A descriptor must designate exactly ONE element (CONVENTIONS W3).
|
|
1673
|
-
*
|
|
1674
|
-
*
|
|
1675
|
-
*
|
|
1676
|
-
* would say "the Articles link *in the nav*";
|
|
1677
|
-
* 2. `{ exact: true }` — match the accessible name whole rather than as a
|
|
1678
|
-
* substring, when two names genuinely overlap.
|
|
1848
|
+
* A descriptor must designate exactly ONE element (CONVENTIONS W3). A name
|
|
1849
|
+
* matches the accessible name WHOLE, and `within(scope, target)` is how an
|
|
1850
|
+
* overlap is narrowed — the way a person would say "the Articles link *in the
|
|
1851
|
+
* nav*".
|
|
1679
1852
|
*/
|
|
1680
1853
|
/** Options accepted by every named descriptor. */
|
|
1681
1854
|
type ElementOptions = {
|
|
1682
1855
|
/**
|
|
1683
|
-
* Match the accessible name as a whole string.
|
|
1684
|
-
*
|
|
1856
|
+
* Match the accessible name as a whole string. `true` by default since
|
|
1857
|
+
* 16.0.
|
|
1858
|
+
*
|
|
1859
|
+
* A substring match is what a person does NOT mean: `link('Articles')`
|
|
1860
|
+
* designated "Read Articles" as readily as "Articles", so a test could pass
|
|
1861
|
+
* for years against the element beside the one it named. `{ exact: false }`
|
|
1862
|
+
* is the opt-out, for a name that genuinely carries a variable part the
|
|
1863
|
+
* test does not control.
|
|
1685
1864
|
*/
|
|
1686
1865
|
exact?: boolean;
|
|
1687
1866
|
};
|
|
@@ -1796,7 +1975,7 @@ declare const selected: (element: ElementRef) => ElementRef;
|
|
|
1796
1975
|
*/
|
|
1797
1976
|
declare const valued: (element: ElementRef, value: string) => ElementRef;
|
|
1798
1977
|
//#endregion
|
|
1799
|
-
//#region src/
|
|
1978
|
+
//#region src/model/contracts/filters.d.ts
|
|
1800
1979
|
/**
|
|
1801
1980
|
* A text filter on a provider request builder (`openai.chat({ user })`,
|
|
1802
1981
|
* `anthropic.messages({ system })`, …).
|
|
@@ -1814,7 +1993,7 @@ declare const valued: (element: ElementRef, value: string) => ElementRef;
|
|
|
1814
1993
|
*/
|
|
1815
1994
|
type TextFilter = Matcher | RegExp | string;
|
|
1816
1995
|
//#endregion
|
|
1817
|
-
//#region src/
|
|
1996
|
+
//#region src/seams/anthropic/anthropic.d.ts
|
|
1818
1997
|
type AnthropicMessagesFilter = {
|
|
1819
1998
|
model?: TextFilter;
|
|
1820
1999
|
system?: TextFilter;
|
|
@@ -1846,7 +2025,7 @@ declare const anthropic: {
|
|
|
1846
2025
|
timeout(): ContractResponse;
|
|
1847
2026
|
};
|
|
1848
2027
|
//#endregion
|
|
1849
|
-
//#region src/
|
|
2028
|
+
//#region src/model/contracts/http.d.ts
|
|
1850
2029
|
/**
|
|
1851
2030
|
* Request filters for the generic HTTP provider. Every field is a subset
|
|
1852
2031
|
* constraint — a request matches when all provided fields match.
|
|
@@ -1863,6 +2042,28 @@ type HttpContractFilter = {
|
|
|
1863
2042
|
/** Query-param subset. string = exact value, RegExp = `test()`. */
|
|
1864
2043
|
query?: Record<string, RegExp | string>;
|
|
1865
2044
|
};
|
|
2045
|
+
/** Init options for a streamed reply. */
|
|
2046
|
+
type HttpStreamInit = {
|
|
2047
|
+
/** The `content-type` the stream is served under. Default `application/octet-stream`. */
|
|
2048
|
+
contentType?: string;
|
|
2049
|
+
/** Milliseconds between two chunks. Default 0 — every chunk at once. */
|
|
2050
|
+
delay?: number;
|
|
2051
|
+
/** Response headers, merged over the builder's own. */
|
|
2052
|
+
headers?: Record<string, string>;
|
|
2053
|
+
/** HTTP status code. Default 200. */
|
|
2054
|
+
status?: number;
|
|
2055
|
+
};
|
|
2056
|
+
/** One server-sent event, as `http.sse()` frames it. */
|
|
2057
|
+
type SseEvent = {
|
|
2058
|
+
/** The `data:` payload. An object is serialised as JSON; a string is sent as-is. */
|
|
2059
|
+
data: unknown;
|
|
2060
|
+
/** The `event:` name. Omitted for an unnamed (default `message`) event. */
|
|
2061
|
+
event?: string;
|
|
2062
|
+
/** The `id:` the client echoes as `Last-Event-ID` when it reconnects. */
|
|
2063
|
+
id?: string;
|
|
2064
|
+
/** The `retry:` hint, in milliseconds. */
|
|
2065
|
+
retry?: number;
|
|
2066
|
+
};
|
|
1866
2067
|
/** Init options shared by the response builders. */
|
|
1867
2068
|
type HttpResponseInit = {
|
|
1868
2069
|
/** Delay in ms before responding (for timeout testing). */
|
|
@@ -1902,11 +2103,32 @@ declare const http: {
|
|
|
1902
2103
|
* server that is not running fails it. `fetch` rejects; no status is served.
|
|
1903
2104
|
*/
|
|
1904
2105
|
unreachable(): ContractResponse;
|
|
2106
|
+
/**
|
|
2107
|
+
* Response: a body that arrives in PIECES, written in order.
|
|
2108
|
+
*
|
|
2109
|
+
* A subject that reads a response as it lands — a token-by-token render, a
|
|
2110
|
+
* progress bar, a reconnect on a half-read body — is specified by the
|
|
2111
|
+
* chunks and their order, which a single serialised body cannot state.
|
|
2112
|
+
* Honoured by all three engines: msw's server, msw's worker, and the
|
|
2113
|
+
* `node:http` stub the website and mobile facets serve from.
|
|
2114
|
+
*
|
|
2115
|
+
* @example
|
|
2116
|
+
* http.stream(['Hel', 'lo, ', 'world'], { contentType: 'text/plain', delay: 10 })
|
|
2117
|
+
*/
|
|
2118
|
+
stream(chunks: readonly string[], init?: HttpStreamInit): ContractResponse;
|
|
2119
|
+
/**
|
|
2120
|
+
* Response: a server-sent event stream — one chunk per event, framed the
|
|
2121
|
+
* way an `EventSource` reads them, served as `text/event-stream`.
|
|
2122
|
+
*
|
|
2123
|
+
* @example
|
|
2124
|
+
* http.sse([{ data: { token: 'Hel' } }, { data: { token: 'lo' } }, { event: 'done', data: '' }])
|
|
2125
|
+
*/
|
|
2126
|
+
sse(events: readonly SseEvent[], init?: Omit<HttpStreamInit, "contentType">): ContractResponse;
|
|
1905
2127
|
/** Response: a text body, served as `text/plain` (200 by default). */
|
|
1906
2128
|
text(body: string, init?: HttpResponseInit): ContractResponse;
|
|
1907
2129
|
};
|
|
1908
2130
|
//#endregion
|
|
1909
|
-
//#region src/
|
|
2131
|
+
//#region src/seams/openai/openai.d.ts
|
|
1910
2132
|
type OpenAIChatFilter = {
|
|
1911
2133
|
model?: TextFilter;
|
|
1912
2134
|
system?: TextFilter;
|
|
@@ -1964,32 +2186,149 @@ declare const openai: {
|
|
|
1964
2186
|
timeout(): ContractResponse;
|
|
1965
2187
|
};
|
|
1966
2188
|
//#endregion
|
|
1967
|
-
//#region src/
|
|
1968
|
-
/**
|
|
1969
|
-
type
|
|
2189
|
+
//#region src/model/doubles/mock-of.d.ts
|
|
2190
|
+
/** How much of the port the double answers for. */
|
|
2191
|
+
type MockOfOptions = {
|
|
2192
|
+
/**
|
|
2193
|
+
* Deep (the default): reading a member that was never stubbed hands back
|
|
2194
|
+
* another double, however far down the port nests. `false` gives the flat
|
|
2195
|
+
* proxy — every member of `T` and nothing beneath them, which is what a
|
|
2196
|
+
* port with a nested namespace wants when the test only ever touches the
|
|
2197
|
+
* first level.
|
|
2198
|
+
*/
|
|
2199
|
+
deep?: boolean;
|
|
2200
|
+
};
|
|
2201
|
+
/**
|
|
2202
|
+
* A typed double for an injected port — the fourth rung of the doubles ladder
|
|
2203
|
+
* (docs/18-conventions.md), and the only one a module test may build itself.
|
|
2204
|
+
*
|
|
2205
|
+
* The type argument is the port: `T` is constrained to `object` and has no
|
|
2206
|
+
* default, so a double is always asked for something. A call with no argument
|
|
2207
|
+
* answers for `object`, which satisfies no port and fails where it is used.
|
|
2208
|
+
*
|
|
2209
|
+
* @example
|
|
2210
|
+
* const gateway = mockOf<PaymentGateway>(); // deep
|
|
2211
|
+
* const clock = mockOf<ClockPort>({ deep: false }); // flat
|
|
2212
|
+
*/
|
|
2213
|
+
declare function mockOf<T extends object>(options: {
|
|
2214
|
+
deep: false;
|
|
2215
|
+
}): MockProxy<T>;
|
|
2216
|
+
declare function mockOf<T extends object>(options?: MockOfOptions): DeepMockProxy<T>;
|
|
2217
|
+
/** The factory's own shape — kept as a name so a consumer can annotate with it. */
|
|
2218
|
+
type MockPort = typeof mockOf;
|
|
2219
|
+
//#endregion
|
|
2220
|
+
//#region src/model/clock/clock.d.ts
|
|
2221
|
+
/**
|
|
2222
|
+
* The one time primitive.
|
|
2223
|
+
*
|
|
2224
|
+
* Time is the determinism the framework owns (CONVENTIONS D16): a test that
|
|
2225
|
+
* samples `Date.now()` and asserts on what comes back is a test that fails on
|
|
2226
|
+
* the next leap second, and a test that reaches for `vi.useFakeTimers()` owns a
|
|
2227
|
+
* teardown it will one day forget. `clock` is both answers in one name — the
|
|
2228
|
+
* instant is stated, and the scope restores itself.
|
|
2229
|
+
*
|
|
2230
|
+
* ```typescript
|
|
2231
|
+
* using _ = clock.at('2026-03-04T09:30:00Z'); // Date is pinned until the scope ends
|
|
2232
|
+
* ```
|
|
2233
|
+
*
|
|
2234
|
+
* `at()` takes the CALENDAR and leaves the scheduler alone: a UI framework's
|
|
2235
|
+
* own loop, a `setTimeout` inside the subject and every promise still resolve
|
|
2236
|
+
* on their own. `run()` takes the scheduler too, for a subject whose behaviour
|
|
2237
|
+
* IS the passage of time — a debounce, a retry backoff, a poll.
|
|
2238
|
+
*/
|
|
2239
|
+
/** A pinned clock — released when the `using` scope that declared it ends. */
|
|
2240
|
+
type PinnedClock = Disposable;
|
|
2241
|
+
declare const clock: {
|
|
2242
|
+
/**
|
|
2243
|
+
* Move the pinned clock forward by `ms` — and, under {@link clock.run},
|
|
2244
|
+
* fire every callback that came due, awaiting what each of them started.
|
|
2245
|
+
*
|
|
2246
|
+
* Only a pinned clock moves: called with no clock taken, it says so rather
|
|
2247
|
+
* than silently doing nothing to the real one.
|
|
2248
|
+
*/
|
|
2249
|
+
advance(ms: number): Promise<void>;
|
|
2250
|
+
/**
|
|
2251
|
+
* Pin `Date` at `iso` for the current scope. `Date` ONLY: the scheduler
|
|
2252
|
+
* stays real, so a subject that awaits, renders or polls keeps running.
|
|
2253
|
+
*
|
|
2254
|
+
* @example
|
|
2255
|
+
* using _ = clock.at('2026-03-04T09:30:00Z');
|
|
2256
|
+
* expect(stampedAt()).toBe('2026-03-04T09:30:00.000Z');
|
|
2257
|
+
*/
|
|
2258
|
+
at(iso: string): PinnedClock;
|
|
2259
|
+
/**
|
|
2260
|
+
* Take the scheduler as well as the calendar: `setTimeout`, `setInterval`
|
|
2261
|
+
* and their friends queue instead of firing, and {@link clock.advance} is
|
|
2262
|
+
* what makes them due. For a subject whose behaviour IS elapsed time — a
|
|
2263
|
+
* debounce, a retry backoff, a poll — where waiting for real would be the
|
|
2264
|
+
* arbitrary sleep J2 forbids.
|
|
2265
|
+
*
|
|
2266
|
+
* @example
|
|
2267
|
+
* using _ = clock.run('2026-03-04T09:30:00Z');
|
|
2268
|
+
* const pending = retrying(failsTwice);
|
|
2269
|
+
* await clock.advance(5_000);
|
|
2270
|
+
* await expect(pending).resolves.toBe('ok');
|
|
2271
|
+
*/
|
|
2272
|
+
run(iso?: string): PinnedClock;
|
|
2273
|
+
};
|
|
2274
|
+
//#endregion
|
|
2275
|
+
//#region src/model/assertions/required.d.ts
|
|
1970
2276
|
/**
|
|
1971
|
-
*
|
|
1972
|
-
*
|
|
2277
|
+
* The value, or a failure that says what was missing and why it mattered.
|
|
2278
|
+
*
|
|
2279
|
+
* A test reaching into a captured structure — a row, a header, the third
|
|
2280
|
+
* element of a list — meets `T | undefined` and has to do something about it.
|
|
2281
|
+
* The two things it usually does are both wrong: `!` erases the question, and
|
|
2282
|
+
* an `if (!x) throw new Error('missing')` re-states the same three lines in
|
|
2283
|
+
* every repository (the estate had two hand-written copies of exactly this).
|
|
2284
|
+
* `required` is that gesture with the reason attached, so the failure reads as
|
|
2285
|
+
* a sentence rather than as a `TypeError` two frames further down.
|
|
2286
|
+
*
|
|
2287
|
+
* @example
|
|
2288
|
+
* const id = required(result.response.body.id, 'the creation reply carries the new id');
|
|
1973
2289
|
*/
|
|
1974
|
-
declare
|
|
2290
|
+
declare function required<T>(value: null | T | undefined, why: string): T;
|
|
1975
2291
|
//#endregion
|
|
1976
|
-
//#region src/
|
|
1977
|
-
/**
|
|
1978
|
-
type
|
|
1979
|
-
/**
|
|
1980
|
-
|
|
1981
|
-
/**
|
|
1982
|
-
|
|
2292
|
+
//#region src/model/assertions/wait-until.d.ts
|
|
2293
|
+
/** How long to wait, and how often to look. */
|
|
2294
|
+
type WaitUntilOptions = {
|
|
2295
|
+
/** Milliseconds between two readings. Default 50. */
|
|
2296
|
+
interval?: number;
|
|
2297
|
+
/** How long the condition has to become true. Default 5 000 ms. */
|
|
2298
|
+
timeout?: number;
|
|
2299
|
+
/** What the condition is, named in the failure. Default: the predicate's source. */
|
|
2300
|
+
why?: string;
|
|
1983
2301
|
};
|
|
1984
2302
|
/**
|
|
1985
|
-
*
|
|
1986
|
-
*
|
|
2303
|
+
* Wait until a condition holds — the sanctioned answer to "the thing I am
|
|
2304
|
+
* specifying is not ready yet".
|
|
2305
|
+
*
|
|
2306
|
+
* A spec that sleeps for a guessed duration is a spec that is slow on a fast
|
|
2307
|
+
* machine and flaky on a slow one, which is why J2 refuses `setTimeout` under
|
|
2308
|
+
* `specs/`. Every facet that drives something already waits on a CONDITION —
|
|
2309
|
+
* a served page, a banner on stdout, an element the visitor can see — and this
|
|
2310
|
+
* is that primitive where no facet owns the waiting: a background write to
|
|
2311
|
+
* land, a queue to drain, a file to appear.
|
|
2312
|
+
*
|
|
2313
|
+
* The failure names the condition and how long it was given, so a timeout is a
|
|
2314
|
+
* sentence rather than "expected true, got false".
|
|
2315
|
+
*
|
|
2316
|
+
* The budget is measured on `performance.now()`, not on `Date` — `clock.at()`
|
|
2317
|
+
* freezes the calendar, and a deadline read off a frozen calendar never
|
|
2318
|
+
* arrives: the poll would run until vitest killed the test with a sentence
|
|
2319
|
+
* about the file rather than about the condition.
|
|
2320
|
+
*
|
|
2321
|
+
* Not for a subject under `clock.run()`: there the scheduler is the test's, and
|
|
2322
|
+
* `clock.advance(ms)` is what makes time pass.
|
|
2323
|
+
*
|
|
2324
|
+
* @example
|
|
2325
|
+
* await waitUntil(() => outbox.length === 1, { why: 'the job wrote its one message' });
|
|
1987
2326
|
*/
|
|
1988
|
-
declare
|
|
2327
|
+
declare function waitUntil(predicate: () => boolean | Promise<boolean>, options?: WaitUntilOptions): Promise<void>;
|
|
1989
2328
|
//#endregion
|
|
1990
2329
|
//#region src/surface.d.ts
|
|
1991
2330
|
declare module 'vitest' {
|
|
1992
|
-
interface Assertion<T =
|
|
2331
|
+
interface Assertion<R extends Promise<void> | void = void, T = unknown> {
|
|
1993
2332
|
/**
|
|
1994
2333
|
* Assert the subject is empty — zero rows for a table (async), an
|
|
1995
2334
|
* empty stream for a text accessor (console, errors, stdout).
|
|
@@ -2013,9 +2352,9 @@ declare module 'vitest' {
|
|
|
2013
2352
|
* Use it for a deliberately-wrong fixture whose diff/error rendering is
|
|
2014
2353
|
* the behaviour under test.
|
|
2015
2354
|
*/
|
|
2016
|
-
toMatch: T extends DirectoryAccessor | FilesystemAccessor ? (name: RegExp | string, options?: MatchFixtureOptions) => Promise<void> : T extends {
|
|
2355
|
+
toMatch: T extends DirectoryAccessor | FilesystemAccessor ? (name: RegExp | string, options?: MatchFixtureOptions) => Promise<void> & R : T extends {
|
|
2017
2356
|
readonly capturedInPage: true;
|
|
2018
|
-
} ? (name: RegExp | string, options?: MatchFixtureOptions) => Promise<void> : T extends JsonAccessor | ResponseAccessor | TextAccessor ? (name: RegExp | string, options?: MatchFixtureOptions) =>
|
|
2357
|
+
} ? (name: RegExp | string, options?: MatchFixtureOptions) => Promise<void> & R : T extends JsonAccessor | ResponseAccessor | TextAccessor ? (name: RegExp | string, options?: MatchFixtureOptions) => R : (expected: RegExp | string, options?: MatchFixtureOptions) => R;
|
|
2019
2358
|
/**
|
|
2020
2359
|
* Assert the table contains exactly the given rows for the given
|
|
2021
2360
|
* columns. Cells accept `match.*` dynamic-value matchers. Async —
|
|
@@ -2028,17 +2367,39 @@ declare module 'vitest' {
|
|
|
2028
2367
|
}
|
|
2029
2368
|
}
|
|
2030
2369
|
//#endregion
|
|
2031
|
-
//#region src/
|
|
2370
|
+
//#region src/model/chain/reporter.d.ts
|
|
2371
|
+
/**
|
|
2372
|
+
* What the subject IS, for the line the startup report closes on.
|
|
2373
|
+
*
|
|
2374
|
+
* Four shapes, because there are four: an app built and served inside this
|
|
2375
|
+
* process (api), a child process the runner started and polls (a site's dev
|
|
2376
|
+
* server), a deployment already running somewhere (`url`), and a subject that
|
|
2377
|
+
* is not an app at all — a binary, a pipeline, a module, a screen — which gets
|
|
2378
|
+
* no line rather than a wrong one.
|
|
2379
|
+
*/
|
|
2380
|
+
type AppInfo = {
|
|
2381
|
+
/** The command, for a child process. */
|
|
2382
|
+
command?: string | undefined;
|
|
2383
|
+
type: 'http' | 'in-process' | 'none' | 'process';
|
|
2384
|
+
url?: string | undefined;
|
|
2385
|
+
};
|
|
2386
|
+
//#endregion
|
|
2387
|
+
//#region src/model/chain/orchestrator.d.ts
|
|
2032
2388
|
type OrchestratorOptions = {
|
|
2033
2389
|
/**
|
|
2034
|
-
*
|
|
2035
|
-
*
|
|
2036
|
-
*
|
|
2037
|
-
*
|
|
2038
|
-
|
|
2390
|
+
* What the subject IS — the line the startup report closes on. Defaults to
|
|
2391
|
+
* the api shape, which is the only facet that serves an app in this
|
|
2392
|
+
* process; a facet whose subject is a binary, a pipeline or a screen says
|
|
2393
|
+
* `{ type: 'none' }` and gets no line rather than a wrong one.
|
|
2394
|
+
*/
|
|
2395
|
+
subject?: AppInfo | undefined;
|
|
2396
|
+
/**
|
|
2397
|
+
* Named infrastructure record. Keys are the database vocabulary of the spec
|
|
2398
|
+
* (`.seed()` / `.table()` `database` option) and the name each handle is
|
|
2399
|
+
* reported and initialised under: the kebab-case form of the key is the
|
|
2400
|
+
* directory its init script lives in (`analyticsDb` → `docker/analytics-db/`).
|
|
2039
2401
|
*/
|
|
2040
2402
|
services: Record<string, ServiceHandle>;
|
|
2041
|
-
mode: 'e2e' | 'integration';
|
|
2042
2403
|
/**
|
|
2043
2404
|
* The project root, already resolved. REQUIRED: the constructors derive it
|
|
2044
2405
|
* by walking up from the calling specification file (CONVENTIONS A9), and
|
|
@@ -2048,68 +2409,40 @@ type OrchestratorOptions = {
|
|
|
2048
2409
|
* launched, which in a workspace is rarely the same directory.
|
|
2049
2410
|
*/
|
|
2050
2411
|
root: string;
|
|
2051
|
-
/** Compose project name — used for per-worker stack isolation. */
|
|
2052
|
-
projectName?: string;
|
|
2053
2412
|
};
|
|
2054
2413
|
/**
|
|
2055
|
-
*
|
|
2056
|
-
*
|
|
2057
|
-
*
|
|
2414
|
+
* The infrastructure a specification declares, started and stopped as a unit.
|
|
2415
|
+
*
|
|
2416
|
+
* Every declared handle becomes a real thing: a container through
|
|
2417
|
+
* testcontainers, or an embedded service (SQLite) that needs no container. The
|
|
2418
|
+
* record KEY is the only name in play — it is the spec's database vocabulary,
|
|
2419
|
+
* the name the startup report prints, and, kebab-cased, the directory the
|
|
2420
|
+
* service reads its init script from.
|
|
2421
|
+
*
|
|
2422
|
+
* It is internal wiring: the constructors drive it, and no consumer names it.
|
|
2058
2423
|
*/
|
|
2059
2424
|
declare class Orchestrator {
|
|
2060
2425
|
private readonly services;
|
|
2061
|
-
private readonly mode;
|
|
2062
2426
|
private readonly root;
|
|
2063
|
-
private readonly
|
|
2427
|
+
private readonly subject;
|
|
2064
2428
|
private running;
|
|
2065
|
-
private composeStack;
|
|
2066
|
-
private composeHandles;
|
|
2067
2429
|
private started;
|
|
2068
2430
|
constructor(options: OrchestratorOptions);
|
|
2069
2431
|
/**
|
|
2070
|
-
*
|
|
2071
|
-
* key resolves to the service named exactly like it, else the kebab-case
|
|
2072
|
-
* conversion of the key; an explicit `composeService` wins. Throws on an
|
|
2073
|
-
* ambiguous binding (both names present). Runs once compose config is known
|
|
2074
|
-
* so the two-step resolution can consult the real service list.
|
|
2075
|
-
*/
|
|
2076
|
-
private resolveBindings;
|
|
2077
|
-
/**
|
|
2078
|
-
* Start declared services via testcontainers (integration mode).
|
|
2432
|
+
* Start every declared service.
|
|
2079
2433
|
* Phase 1: start all containers in parallel (the slow part).
|
|
2080
2434
|
* Phase 2: wire connections, healthcheck, and init sequentially (fast).
|
|
2081
2435
|
*/
|
|
2082
2436
|
start(): Promise<void>;
|
|
2083
|
-
/**
|
|
2084
|
-
* Stop testcontainers (integration mode).
|
|
2085
|
-
*/
|
|
2437
|
+
/** Stop every container this stack started. */
|
|
2086
2438
|
stop(): Promise<void>;
|
|
2087
|
-
/**
|
|
2088
|
-
* Start full docker compose stack (e2e mode).
|
|
2089
|
-
* Auto-detects infra services and creates handles for them.
|
|
2090
|
-
*/
|
|
2091
|
-
startCompose(): Promise<void>;
|
|
2092
|
-
/**
|
|
2093
|
-
* Stop docker compose stack (e2e mode).
|
|
2094
|
-
*/
|
|
2095
|
-
stopCompose(): Promise<void>;
|
|
2096
|
-
/**
|
|
2097
|
-
* Get the default database — the first declared handle that is one.
|
|
2098
|
-
*/
|
|
2439
|
+
/** The default database — the first declared handle that is one. */
|
|
2099
2440
|
getDatabase(): DatabasePort | null;
|
|
2100
|
-
/**
|
|
2101
|
-
* Get all database services keyed by their record key (declared services)
|
|
2102
|
-
* or compose service name (stack-detected services).
|
|
2103
|
-
*/
|
|
2441
|
+
/** Every database service, keyed by its record key. */
|
|
2104
2442
|
getDatabases(): Map<string, DatabasePort>;
|
|
2105
|
-
private allHandles;
|
|
2106
|
-
/**
|
|
2107
|
-
* Get app URL from compose (e2e mode).
|
|
2108
|
-
*/
|
|
2109
|
-
getAppUrl(): null | string;
|
|
2110
2443
|
}
|
|
2111
2444
|
//#endregion
|
|
2112
|
-
//#region src/
|
|
2445
|
+
//#region src/model/chain/services.d.ts
|
|
2113
2446
|
/**
|
|
2114
2447
|
* Infrastructure services declared as a named record. Keys become the typed
|
|
2115
2448
|
* vocabulary of the whole spec: the server factory receives the same record,
|
|
@@ -2119,41 +2452,29 @@ type ServiceRecord = Record<string, ServiceHandle>;
|
|
|
2119
2452
|
/** Keys of a services record whose handles are databases. */
|
|
2120
2453
|
type DatabaseKeys<Services extends ServiceRecord> = { [K in keyof Services]: Services[K] extends DatabasePort ? K & string : never; }[keyof Services];
|
|
2121
2454
|
//#endregion
|
|
2122
|
-
//#region src/
|
|
2455
|
+
//#region src/facets/api/api.specification.d.ts
|
|
2123
2456
|
/** Any object with a request method compatible with Hono's app.request(). */
|
|
2124
2457
|
type HonoApp = {
|
|
2125
2458
|
request: (path: string, init?: RequestInit) => Promise<Response> | Response;
|
|
2126
2459
|
};
|
|
2127
|
-
/** Execution mode — exists ONLY on `specification.api()` (CONVENTIONS A5). */
|
|
2128
|
-
type SpecificationMode = 'compose' | 'node';
|
|
2129
2460
|
/** Options for {@link startApi | specification.api}. */
|
|
2130
2461
|
type ApiSpecificationOptions<Services extends ServiceRecord = ServiceRecord> = {
|
|
2131
2462
|
/**
|
|
2132
|
-
*
|
|
2133
|
-
*
|
|
2134
|
-
*
|
|
2135
|
-
* project via `env: { TEST_MODE: 'compose' }` (CONVENTIONS A5).
|
|
2136
|
-
*/
|
|
2137
|
-
mode?: SpecificationMode;
|
|
2138
|
-
/**
|
|
2139
|
-
* Project root override for compose detection and init scripts. When
|
|
2140
|
-
* absent, the root is auto-discovered by walking up from the calling
|
|
2141
|
-
* specification file to the first directory containing
|
|
2142
|
-
* `docker/compose.test.yaml`, else the first containing `package.json`
|
|
2143
|
-
* (CONVENTIONS A9).
|
|
2463
|
+
* Project root override for init scripts and artefact paths. When absent,
|
|
2464
|
+
* the root is auto-discovered by walking up from the calling specification
|
|
2465
|
+
* file to the first directory containing `package.json` (CONVENTIONS A9).
|
|
2144
2466
|
*/
|
|
2145
2467
|
root?: string;
|
|
2146
2468
|
/**
|
|
2147
2469
|
* The app factory — receives the started services record (fully typed)
|
|
2148
|
-
* and returns the Hono app.
|
|
2149
|
-
* mode (the app runs as a compose service there).
|
|
2470
|
+
* and returns the Hono app.
|
|
2150
2471
|
*/
|
|
2151
|
-
server
|
|
2472
|
+
server: (services: Services) => HonoApp;
|
|
2152
2473
|
/**
|
|
2153
2474
|
* Named infrastructure record. Keys become the `database` vocabulary of
|
|
2154
|
-
* `.seed()` / `.table()
|
|
2155
|
-
*
|
|
2156
|
-
*
|
|
2475
|
+
* `.seed()` / `.table()`, and, kebab-cased, the folder each service reads
|
|
2476
|
+
* its init script from (`{ analyticsDb: postgres() }` →
|
|
2477
|
+
* `docker/analytics-db/init.sql`).
|
|
2157
2478
|
*/
|
|
2158
2479
|
services?: Services;
|
|
2159
2480
|
};
|
|
@@ -2172,12 +2493,10 @@ type ApiHandle<DatabaseKey extends string = string> = {
|
|
|
2172
2493
|
* usable with `await expect(...).toBeRunning()` and read accessors.
|
|
2173
2494
|
*/
|
|
2174
2495
|
docker: (containerId: string) => ContainerAccessor;
|
|
2175
|
-
/** The orchestrator managing the test infrastructure lifecycle. */
|
|
2176
|
-
orchestrator: Orchestrator;
|
|
2177
2496
|
};
|
|
2178
2497
|
declare function startApi<Services extends ServiceRecord>(options: ApiSpecificationOptions<Services>): Promise<ApiHandle<DatabaseKeys<Services>>>;
|
|
2179
2498
|
//#endregion
|
|
2180
|
-
//#region src/
|
|
2499
|
+
//#region src/facets/cli/cli.specification.d.ts
|
|
2181
2500
|
/** Options for {@link startCli | specification.cli}. */
|
|
2182
2501
|
type CliSpecificationOptions<Services extends ServiceRecord = ServiceRecord> = {
|
|
2183
2502
|
/**
|
|
@@ -2188,6 +2507,14 @@ type CliSpecificationOptions<Services extends ServiceRecord = ServiceRecord> = {
|
|
|
2188
2507
|
* containers get force-removed at scope exit (CONVENTIONS B5).
|
|
2189
2508
|
*/
|
|
2190
2509
|
docker?: DockerSpecConfig;
|
|
2510
|
+
/**
|
|
2511
|
+
* Environment applied to EVERY run of this binary — the variables the
|
|
2512
|
+
* product needs to be deterministic at all (`TZ`, `NO_COLOR`, `LANG`,
|
|
2513
|
+
* a config home under the spec's own workdir). Stated once per app
|
|
2514
|
+
* instead of repeated on every chain and in every document; a chained
|
|
2515
|
+
* `.env()` and a document's `env:` both win over it.
|
|
2516
|
+
*/
|
|
2517
|
+
defaults?: CliEnv;
|
|
2191
2518
|
/**
|
|
2192
2519
|
* Named environment SETS for `<case>.spec.yaml` documents. An `env:` entry
|
|
2193
2520
|
* `frozen` applies the whole `frozen` record; `$WORKDIR` expands and `null`
|
|
@@ -2205,9 +2532,9 @@ type CliSpecificationOptions<Services extends ServiceRecord = ServiceRecord> = {
|
|
|
2205
2532
|
serve?: Record<string, LiterateServeRegistration>;
|
|
2206
2533
|
/**
|
|
2207
2534
|
* Project-root override (CONVENTIONS A9) — the single meaning of `root`:
|
|
2208
|
-
* it anchors
|
|
2209
|
-
*
|
|
2210
|
-
* or `$FIXTURES/` paths on its own.
|
|
2535
|
+
* it anchors the local-bin resolution of the tested binary, in place of
|
|
2536
|
+
* the walk to the nearest `package.json`. It is NOT a fixtures root;
|
|
2537
|
+
* `.fixture()` resolves feature-local or `$FIXTURES/` paths on its own.
|
|
2211
2538
|
*/
|
|
2212
2539
|
root?: string;
|
|
2213
2540
|
/**
|
|
@@ -2245,13 +2572,50 @@ type CliHandle<DatabaseKey extends string = string> = {
|
|
|
2245
2572
|
};
|
|
2246
2573
|
declare function startCli<Services extends ServiceRecord>(bin: string, options?: CliSpecificationOptions<Services>): Promise<CliHandle<DatabaseKeys<Services>>>;
|
|
2247
2574
|
//#endregion
|
|
2248
|
-
//#region src/
|
|
2575
|
+
//#region src/facets/integration/integration.specification.d.ts
|
|
2576
|
+
/** Options for {@link startIntegration | specification.integration}. */
|
|
2577
|
+
type IntegrationSpecificationOptions<Services extends ServiceRecord = ServiceRecord> = {
|
|
2578
|
+
/** Project-root override — see the `root` option of `specification.api()`. */
|
|
2579
|
+
root?: string;
|
|
2580
|
+
/**
|
|
2581
|
+
* Named infrastructure the module is specified AGAINST — a real database,
|
|
2582
|
+
* a real cache, a `process()` it talks to. Omit it for the other half of
|
|
2583
|
+
* the facet: a pure module whose oracle is a golden.
|
|
2584
|
+
*/
|
|
2585
|
+
services?: Services;
|
|
2586
|
+
};
|
|
2587
|
+
/**
|
|
2588
|
+
* The record returned by {@link startIntegration | specification.integration}.
|
|
2589
|
+
* Destructure with the canonical names (CONVENTIONS A3):
|
|
2590
|
+
*
|
|
2591
|
+
* const { integration, cleanup } = await specification.integration(…);
|
|
2592
|
+
*/
|
|
2593
|
+
type IntegrationHandle<Services extends ServiceRecord = ServiceRecord, DatabaseKey extends string = string> = {
|
|
2594
|
+
/** Stop all infrastructure started by this specification. */
|
|
2595
|
+
cleanup: () => Promise<void>;
|
|
2596
|
+
integration: IntegrationSpecification<Services, DatabaseKey>;
|
|
2597
|
+
};
|
|
2598
|
+
/**
|
|
2599
|
+
* Specify a module the way the assembled product is specified: against real
|
|
2600
|
+
* services and declared contracts, with the golden mechanism on what it
|
|
2601
|
+
* produces.
|
|
2602
|
+
*
|
|
2603
|
+
* The fork is the SUBJECT. A module alone is a module test, beside its code,
|
|
2604
|
+
* with no runner at all. A module that needs a real database, or whose oracle
|
|
2605
|
+
* is a golden file, is this one — which is what spares it the shape it would
|
|
2606
|
+
* otherwise take: plain vitest under a `specs/` folder, a hand-built sqlite
|
|
2607
|
+
* template, a `beforeAll` starting a container, and a `toMatchSnapshot` beside
|
|
2608
|
+
* a framework that owns a golden engine.
|
|
2609
|
+
*/
|
|
2610
|
+
declare function startIntegration<Services extends ServiceRecord>(options?: IntegrationSpecificationOptions<Services>): Promise<IntegrationHandle<Services, DatabaseKeys<Services>>>;
|
|
2611
|
+
//#endregion
|
|
2612
|
+
//#region src/facets/jobs/jobs.specification.d.ts
|
|
2249
2613
|
/** Options for {@link startJobs | specification.jobs}. */
|
|
2250
2614
|
type JobsSpecificationOptions<Services extends ServiceRecord = ServiceRecord> = {
|
|
2251
2615
|
/**
|
|
2252
2616
|
* Named jobs triggerable via `jobs.trigger(name)` — a factory receiving
|
|
2253
2617
|
* the started services record, or a static array. Jobs run in-process by
|
|
2254
|
-
* definition (CONVENTIONS
|
|
2618
|
+
* definition (CONVENTIONS A8).
|
|
2255
2619
|
*/
|
|
2256
2620
|
jobs: ((services: Services) => JobHandle[]) | JobHandle[];
|
|
2257
2621
|
/** Project root override — see the `root` option of `specification.api()`. */
|
|
@@ -2274,7 +2638,7 @@ type JobsHandle<DatabaseKey extends string = string> = {
|
|
|
2274
2638
|
};
|
|
2275
2639
|
declare function startJobs<Services extends ServiceRecord>(options: JobsSpecificationOptions<Services>): Promise<JobsHandle<DatabaseKeys<Services>>>;
|
|
2276
2640
|
//#endregion
|
|
2277
|
-
//#region src/
|
|
2641
|
+
//#region src/facets/mobile/mobile.specification.d.ts
|
|
2278
2642
|
/**
|
|
2279
2643
|
* The declared stub backend behind the app under test. The framework owns
|
|
2280
2644
|
* the simulator and appium but NOT the JS bundler (Metro belongs to the
|
|
@@ -2291,7 +2655,7 @@ type MobileBackendOptions = {
|
|
|
2291
2655
|
port?: number;
|
|
2292
2656
|
};
|
|
2293
2657
|
/** Options for {@link startMobile | specification.mobile}. */
|
|
2294
|
-
type MobileSpecificationOptions = {
|
|
2658
|
+
type MobileSpecificationOptions<Services extends ServiceRecord = ServiceRecord> = {
|
|
2295
2659
|
/** The app under test — terminated and relaunched by every `.open()`. */
|
|
2296
2660
|
app: {
|
|
2297
2661
|
/** Bundle id of the installed app (`com.example.app`). */
|
|
@@ -2324,6 +2688,14 @@ type MobileSpecificationOptions = {
|
|
|
2324
2688
|
* driving a DEV build states the wait its bundler's cold boot needs
|
|
2325
2689
|
* (`{ action: 45_000 }`) instead of sleeping inside its scenarios.
|
|
2326
2690
|
*/
|
|
2691
|
+
/**
|
|
2692
|
+
* Named services started with the runner and stopped with it — a
|
|
2693
|
+
* `process()` for the bundler the app loads from, a database a dev build
|
|
2694
|
+
* reads. Metro stops being a `beforeAll` the repository maintains: it is
|
|
2695
|
+
* `process({ command: 'expo start', ready: /Metro waiting on .*:(\\d+)/ })`
|
|
2696
|
+
* like every other external process the framework owns.
|
|
2697
|
+
*/
|
|
2698
|
+
services?: Services;
|
|
2327
2699
|
timeouts?: DeviceTimeouts;
|
|
2328
2700
|
};
|
|
2329
2701
|
/**
|
|
@@ -2345,13 +2717,35 @@ type MobileHandle = {
|
|
|
2345
2717
|
/** The resolved simulator UDID the specs run against. */
|
|
2346
2718
|
udid: string;
|
|
2347
2719
|
};
|
|
2348
|
-
declare function startMobile(options: MobileSpecificationOptions): Promise<MobileHandle>;
|
|
2720
|
+
declare function startMobile<Services extends ServiceRecord>(options: MobileSpecificationOptions<Services>): Promise<MobileHandle>;
|
|
2349
2721
|
//#endregion
|
|
2350
|
-
//#region src/
|
|
2351
|
-
/**
|
|
2352
|
-
|
|
2353
|
-
|
|
2722
|
+
//#region src/seams/process/serve.adapter.d.ts
|
|
2723
|
+
/**
|
|
2724
|
+
* The ONE shape an external process takes — the site under test, a backend a
|
|
2725
|
+
* page talks to, a bundler a simulator loads from. Built by `process()`
|
|
2726
|
+
* (chapter 17) and accepted wherever a facet starts something outside this
|
|
2727
|
+
* process: `website({ server })`, `services: { api: process(…) }`, and the
|
|
2728
|
+
* literate `serve:` registry.
|
|
2729
|
+
*/
|
|
2730
|
+
type ProcessOptions = {
|
|
2731
|
+
/**
|
|
2732
|
+
* A one-shot command run ONCE per specification, before the process is
|
|
2733
|
+
* spawned, which must exit 0 — a build, a migration, a fixture load. Its
|
|
2734
|
+
* failure is the specification's, and it says so.
|
|
2735
|
+
*/
|
|
2736
|
+
before?: string;
|
|
2737
|
+
/** Shell command that starts the process. Receives the chosen port as `PORT`. */
|
|
2354
2738
|
command: string;
|
|
2739
|
+
/** Working directory, relative to the project root. Default: the root itself. */
|
|
2740
|
+
cwd?: string;
|
|
2741
|
+
/**
|
|
2742
|
+
* Environment for the child, on top of the runner's own. A function
|
|
2743
|
+
* receives the services already started beside this one, so a process can
|
|
2744
|
+
* be handed a sibling's connection string.
|
|
2745
|
+
*/
|
|
2746
|
+
env?: ((services: Record<string, {
|
|
2747
|
+
connectionString: string;
|
|
2748
|
+
}>) => Record<string, string>) | Record<string, string>;
|
|
2355
2749
|
/** Fixed port. Default: an OS-assigned free port, injected as `PORT`. */
|
|
2356
2750
|
port?: number;
|
|
2357
2751
|
/**
|
|
@@ -2370,7 +2764,85 @@ type ServeOptions = {
|
|
|
2370
2764
|
timeout?: number;
|
|
2371
2765
|
};
|
|
2372
2766
|
//#endregion
|
|
2373
|
-
//#region src/
|
|
2767
|
+
//#region src/model/chain/process.d.ts
|
|
2768
|
+
/**
|
|
2769
|
+
* An external process, declared like any other service.
|
|
2770
|
+
*
|
|
2771
|
+
* One shape for every one of them: a site's dev server, an API behind the
|
|
2772
|
+
* site, the bundler a simulator loads from. Without it each is declared
|
|
2773
|
+
* somewhere else — a facet option, a document's `serve:` entry, a `beforeAll`
|
|
2774
|
+
* spawning a child and an `afterAll` that sometimes forgets to kill it — and
|
|
2775
|
+
* the lifetime belongs to whoever remembered. Here it is the framework's.
|
|
2776
|
+
*
|
|
2777
|
+
* It sits in a `services` record beside `postgres()`, `redis()` and
|
|
2778
|
+
* `sqlite()`, so it starts after them and can be handed their connection
|
|
2779
|
+
* strings through `env`, and it is stopped with the specification.
|
|
2780
|
+
*/
|
|
2781
|
+
declare class ProcessHandle implements ServiceHandle {
|
|
2782
|
+
/** Embedded: nothing to pull, nothing to run in a container. */
|
|
2783
|
+
readonly defaultImage = "";
|
|
2784
|
+
readonly defaultPort = 0;
|
|
2785
|
+
readonly environment: Record<string, string>;
|
|
2786
|
+
readonly type = "process";
|
|
2787
|
+
serviceName: null | string;
|
|
2788
|
+
connectionString: string;
|
|
2789
|
+
started: boolean;
|
|
2790
|
+
private adapter;
|
|
2791
|
+
private readonly options;
|
|
2792
|
+
constructor(options: ProcessOptions);
|
|
2793
|
+
buildConnectionString(host: string, port: number): string;
|
|
2794
|
+
createDatabaseAdapter(): DatabasePort | null;
|
|
2795
|
+
healthcheck(): Promise<void>;
|
|
2796
|
+
/**
|
|
2797
|
+
* A process is not a container, so the orchestrator never starts it: the
|
|
2798
|
+
* facet does, once its siblings are up and their connection strings are
|
|
2799
|
+
* readable ({@link startProcessServices}).
|
|
2800
|
+
*/
|
|
2801
|
+
initialize(): Promise<void>;
|
|
2802
|
+
/**
|
|
2803
|
+
* Isolation is the RUN's, not the worker's — and a run here is one
|
|
2804
|
+
* EVALUATION of the specification module. Under vitest's default
|
|
2805
|
+
* isolation that is once per test file, so two files that import the same
|
|
2806
|
+
* specification get a child each, with the id the facet minted for it.
|
|
2807
|
+
* Nothing is acquired or reset between tests: the id is what tells two
|
|
2808
|
+
* runs apart, never a label a spec samples.
|
|
2809
|
+
*/
|
|
2810
|
+
isolation(): IsolationStrategy;
|
|
2811
|
+
reset(): Promise<void>;
|
|
2812
|
+
/**
|
|
2813
|
+
* Spawn it and wait until it answers. `services` is every handle declared
|
|
2814
|
+
* beside this one, already started — what an `env` function reads.
|
|
2815
|
+
*
|
|
2816
|
+
* @internal
|
|
2817
|
+
*/
|
|
2818
|
+
spawnWith(root: string, services: Record<string, {
|
|
2819
|
+
connectionString: string;
|
|
2820
|
+
}>, runId: string): Promise<void>;
|
|
2821
|
+
/** Terminate the process group. Idempotent. */
|
|
2822
|
+
shutdown(): Promise<void>;
|
|
2823
|
+
/** What it was declared as — what a facet reads to start it in a slot of its own. */
|
|
2824
|
+
get spec(): ProcessOptions;
|
|
2825
|
+
/** The base URL it came up on — known only once it is started. */
|
|
2826
|
+
get url(): string;
|
|
2827
|
+
}
|
|
2828
|
+
/**
|
|
2829
|
+
* Declare an external process the framework owns for the life of the
|
|
2830
|
+
* specification.
|
|
2831
|
+
*
|
|
2832
|
+
* @example
|
|
2833
|
+
* const { website, cleanup } = await specification.website({
|
|
2834
|
+
* services: { api: process({ command: 'bin/server web', ready: '/health' }) },
|
|
2835
|
+
* server: ({ api }) => process({
|
|
2836
|
+
* command: 'next dev',
|
|
2837
|
+
* env: { NEXT_PUBLIC_API_URL: api.connectionString },
|
|
2838
|
+
* }),
|
|
2839
|
+
* });
|
|
2840
|
+
*/
|
|
2841
|
+
declare function processService(options: ProcessOptions): ProcessHandle;
|
|
2842
|
+
//#endregion
|
|
2843
|
+
//#region src/facets/website/website.specification.d.ts
|
|
2844
|
+
/** What `server` may be handed as, and what it resolves to. */
|
|
2845
|
+
type ServerSpec<Services extends ServiceRecord> = ((services: Services) => ProcessHandle | ProcessOptions) | ProcessHandle | ProcessOptions;
|
|
2374
2846
|
/**
|
|
2375
2847
|
* The declared stub backend behind the site under test — started before the
|
|
2376
2848
|
* server command, torn down with the runner. Its URL is injected into the
|
|
@@ -2391,7 +2863,7 @@ type WebsiteBackendOptions = {
|
|
|
2391
2863
|
* mode for the same reason: a deployed site cannot be pointed at a local
|
|
2392
2864
|
* stub.
|
|
2393
2865
|
*/
|
|
2394
|
-
type WebsiteSpecificationOptions = {
|
|
2866
|
+
type WebsiteSpecificationOptions<Services extends ServiceRecord = ServiceRecord> = {
|
|
2395
2867
|
/**
|
|
2396
2868
|
* Cross-origin request policy for visits. Default: `'block'` with a
|
|
2397
2869
|
* local `server` (deterministic — analytics and CDNs never leave the
|
|
@@ -2403,6 +2875,14 @@ type WebsiteSpecificationOptions = {
|
|
|
2403
2875
|
* `server` command. Auto-discovered from the calling file when absent.
|
|
2404
2876
|
*/
|
|
2405
2877
|
root?: string;
|
|
2878
|
+
/**
|
|
2879
|
+
* Named services started with the runner, before the site is: a database
|
|
2880
|
+
* the site reads, a `process()` backend it calls. They are started in
|
|
2881
|
+
* declaration order and stopped with the specification, and `server` may
|
|
2882
|
+
* be a function of them — which is how a site is handed the URL of the
|
|
2883
|
+
* API it was started beside.
|
|
2884
|
+
*/
|
|
2885
|
+
services?: Services;
|
|
2406
2886
|
} & ({
|
|
2407
2887
|
/**
|
|
2408
2888
|
* Declared stub backend: started BEFORE the server command, its
|
|
@@ -2411,10 +2891,11 @@ type WebsiteSpecificationOptions = {
|
|
|
2411
2891
|
*/
|
|
2412
2892
|
backend?: WebsiteBackendOptions;
|
|
2413
2893
|
/**
|
|
2414
|
-
* Start the site locally: a shell command receiving
|
|
2415
|
-
* as `PORT`, polled on `ready` (default `/`) until it
|
|
2894
|
+
* Start the site locally: a `process()` — a shell command receiving
|
|
2895
|
+
* a free port as `PORT`, polled on `ready` (default `/`) until it
|
|
2896
|
+
* answers — or a function of the started services returning one.
|
|
2416
2897
|
*/
|
|
2417
|
-
server:
|
|
2898
|
+
server: ServerSpec<Services>;
|
|
2418
2899
|
url?: never;
|
|
2419
2900
|
} | {
|
|
2420
2901
|
backend?: never;
|
|
@@ -2435,11 +2916,11 @@ type WebsiteHandle = {
|
|
|
2435
2916
|
url: string;
|
|
2436
2917
|
website: WebsiteSpecification;
|
|
2437
2918
|
};
|
|
2438
|
-
declare function startWebsite(options: WebsiteSpecificationOptions): Promise<WebsiteHandle>;
|
|
2919
|
+
declare function startWebsite<Services extends ServiceRecord>(options: WebsiteSpecificationOptions<Services>): Promise<WebsiteHandle>;
|
|
2439
2920
|
//#endregion
|
|
2440
|
-
//#region src/
|
|
2921
|
+
//#region src/facets/specification.d.ts
|
|
2441
2922
|
/**
|
|
2442
|
-
* The
|
|
2923
|
+
* The six specification constructors (CONVENTIONS A2) — created in a
|
|
2443
2924
|
* `*.specification.ts` file under `specs/`, destructured with canonical
|
|
2444
2925
|
* names, and cleaned up via `afterAll(cleanup)` (A1/A3/A4).
|
|
2445
2926
|
*
|
|
@@ -2462,21 +2943,22 @@ declare function startWebsite(options: WebsiteSpecificationOptions): Promise<Web
|
|
|
2462
2943
|
* export const { cli, cleanup } = await specification.cli('my-cli');
|
|
2463
2944
|
* afterAll(cleanup);
|
|
2464
2945
|
*/
|
|
2465
|
-
/** The
|
|
2946
|
+
/** The six constructors, and only six — the framework's whole entry surface. */
|
|
2466
2947
|
type Specification = {
|
|
2467
2948
|
api: typeof startApi;
|
|
2468
2949
|
cli: typeof startCli;
|
|
2950
|
+
integration: typeof startIntegration;
|
|
2469
2951
|
jobs: typeof startJobs;
|
|
2470
2952
|
mobile: typeof startMobile;
|
|
2471
2953
|
website: typeof startWebsite;
|
|
2472
2954
|
};
|
|
2473
2955
|
declare const specification: Specification;
|
|
2474
2956
|
//#endregion
|
|
2475
|
-
//#region src/
|
|
2957
|
+
//#region src/facets/component/component.specification.d.ts
|
|
2476
2958
|
/** Present, typed, and refusing — every setup and the terminal action alike. */
|
|
2477
2959
|
declare const component: ComponentChain;
|
|
2478
2960
|
//#endregion
|
|
2479
|
-
//#region src/
|
|
2961
|
+
//#region src/model/literate/spec-document.d.ts
|
|
2480
2962
|
/**
|
|
2481
2963
|
* The facets a document may describe. `cli` is the only one implemented; the
|
|
2482
2964
|
* key exists so a second facet can arrive without a second grammar.
|
|
@@ -2556,15 +3038,7 @@ type SpecDocument = {
|
|
|
2556
3038
|
serve: SpecServeEntry[];
|
|
2557
3039
|
};
|
|
2558
3040
|
//#endregion
|
|
2559
|
-
//#region src/
|
|
2560
|
-
/** Return all container IDs (running or stopped) that carry `key=value`. */
|
|
2561
|
-
declare function findContainersByLabel(key: string, value: string): string[];
|
|
2562
|
-
/** Return the raw `docker inspect` payload (object, not array) for a container. */
|
|
2563
|
-
declare function inspectContainer(id: string): unknown;
|
|
2564
|
-
/** Force-remove the given container IDs in a single call. Errors are swallowed. */
|
|
2565
|
-
declare function removeContainers(ids: string[]): void;
|
|
2566
|
-
//#endregion
|
|
2567
|
-
//#region src/specification/facets/_common/result/text-subject.d.ts
|
|
3041
|
+
//#region src/model/result/text-subject.d.ts
|
|
2568
3042
|
/**
|
|
2569
3043
|
* Wrap an arbitrary string into a {@link TextAccessor} anchored on the calling
|
|
2570
3044
|
* test's directory — the same caller-detection the builders use.
|
|
@@ -2590,7 +3064,7 @@ declare function removeContainers(ids: string[]): void;
|
|
|
2590
3064
|
*/
|
|
2591
3065
|
declare function text(value: string): TextAccessor;
|
|
2592
3066
|
//#endregion
|
|
2593
|
-
//#region src/
|
|
3067
|
+
//#region src/model/ports/container.port.d.ts
|
|
2594
3068
|
/**
|
|
2595
3069
|
* Abstract container interface.
|
|
2596
3070
|
* Represents a running service (database, cache, etc.)
|
|
@@ -2610,13 +3084,8 @@ type ContainerPort = {
|
|
|
2610
3084
|
getLogs: () => Promise<string>;
|
|
2611
3085
|
};
|
|
2612
3086
|
//#endregion
|
|
2613
|
-
//#region src/
|
|
3087
|
+
//#region src/seams/postgres/postgres.d.ts
|
|
2614
3088
|
type PostgresOptions = {
|
|
2615
|
-
/**
|
|
2616
|
-
* Map to a service in docker/compose.test.yaml. Defaults to the handle's
|
|
2617
|
-
* key in the declared services record.
|
|
2618
|
-
*/
|
|
2619
|
-
composeService?: string;
|
|
2620
3089
|
/** Override image. */
|
|
2621
3090
|
image?: string;
|
|
2622
3091
|
/** Override environment variables. */
|
|
@@ -2624,7 +3093,7 @@ type PostgresOptions = {
|
|
|
2624
3093
|
};
|
|
2625
3094
|
declare class PostgresHandle implements DatabasePort, ServiceHandle {
|
|
2626
3095
|
readonly type = "postgres";
|
|
2627
|
-
|
|
3096
|
+
serviceName: null | string;
|
|
2628
3097
|
readonly defaultPort = 5432;
|
|
2629
3098
|
readonly defaultImage: string;
|
|
2630
3099
|
readonly environment: Record<string, string>;
|
|
@@ -2637,7 +3106,7 @@ declare class PostgresHandle implements DatabasePort, ServiceHandle {
|
|
|
2637
3106
|
buildConnectionString(host: string, port: number): string;
|
|
2638
3107
|
createDatabaseAdapter(): DatabasePort;
|
|
2639
3108
|
healthcheck(): Promise<void>;
|
|
2640
|
-
initialize(
|
|
3109
|
+
initialize(dockerDir: string): Promise<void>;
|
|
2641
3110
|
private getClient;
|
|
2642
3111
|
seed(sql: string): Promise<void>;
|
|
2643
3112
|
reset(): Promise<void>;
|
|
@@ -2648,28 +3117,23 @@ declare class PostgresHandle implements DatabasePort, ServiceHandle {
|
|
|
2648
3117
|
* Create a PostgreSQL service handle.
|
|
2649
3118
|
*
|
|
2650
3119
|
* @example
|
|
2651
|
-
* //
|
|
2652
|
-
* //
|
|
2653
|
-
* // {
|
|
2654
|
-
* //
|
|
2655
|
-
* const events = postgres(
|
|
3120
|
+
* // The record key is the name: it is the spec's `database` vocabulary, and
|
|
3121
|
+
* // Kebab-cased it is the folder the init script is read from.
|
|
3122
|
+
* // { db: postgres() } → docker/db/init.sql, then docker/postgres/init.sql
|
|
3123
|
+
* // { analyticsDb: postgres() } → docker/analytics-db/init.sql
|
|
3124
|
+
* const events = postgres();
|
|
2656
3125
|
* // After start: events.connectionString is populated
|
|
2657
3126
|
*/
|
|
2658
3127
|
declare function postgres(options?: PostgresOptions): PostgresHandle;
|
|
2659
3128
|
//#endregion
|
|
2660
|
-
//#region src/
|
|
3129
|
+
//#region src/seams/redis/redis.d.ts
|
|
2661
3130
|
type RedisOptions = {
|
|
2662
|
-
/**
|
|
2663
|
-
* Map to a service in docker/compose.test.yaml. Defaults to the handle's
|
|
2664
|
-
* key in the declared services record.
|
|
2665
|
-
*/
|
|
2666
|
-
composeService?: string;
|
|
2667
3131
|
/** Override image. */
|
|
2668
3132
|
image?: string;
|
|
2669
3133
|
};
|
|
2670
3134
|
declare class RedisHandle implements ServiceHandle {
|
|
2671
3135
|
readonly type = "redis";
|
|
2672
|
-
|
|
3136
|
+
serviceName: null | string;
|
|
2673
3137
|
readonly defaultPort = 6379;
|
|
2674
3138
|
readonly defaultImage: string;
|
|
2675
3139
|
readonly environment: Record<string, string>;
|
|
@@ -2688,13 +3152,13 @@ declare class RedisHandle implements ServiceHandle {
|
|
|
2688
3152
|
* Create a Redis service handle.
|
|
2689
3153
|
*
|
|
2690
3154
|
* @example
|
|
2691
|
-
* // The record key
|
|
3155
|
+
* // The record key is the name it reports under: { cache: redis() } → "cache".
|
|
2692
3156
|
* const cache = redis();
|
|
2693
3157
|
* // After start: cache.connectionString is populated
|
|
2694
3158
|
*/
|
|
2695
3159
|
declare function redis(options?: RedisOptions): RedisHandle;
|
|
2696
3160
|
//#endregion
|
|
2697
|
-
//#region src/
|
|
3161
|
+
//#region src/seams/sqlite/sqlite.d.ts
|
|
2698
3162
|
type SqliteOptions = {
|
|
2699
3163
|
/**
|
|
2700
3164
|
* Path to a SQL file used to initialize the database schema.
|
|
@@ -2711,7 +3175,7 @@ type SqliteOptions = {
|
|
|
2711
3175
|
};
|
|
2712
3176
|
declare class SqliteHandle implements DatabasePort, ServiceHandle {
|
|
2713
3177
|
readonly type = "sqlite";
|
|
2714
|
-
|
|
3178
|
+
serviceName: null | string;
|
|
2715
3179
|
readonly defaultPort = 0;
|
|
2716
3180
|
readonly defaultImage = "";
|
|
2717
3181
|
readonly environment: Record<string, string>;
|
|
@@ -2741,7 +3205,7 @@ declare class SqliteHandle implements DatabasePort, ServiceHandle {
|
|
|
2741
3205
|
* rather than proceeding into a second concurrent `prisma db push` on the
|
|
2742
3206
|
* same file (which is what `database is locked` was).
|
|
2743
3207
|
*/
|
|
2744
|
-
initialize(
|
|
3208
|
+
initialize(_dockerDir?: string, root?: string): Promise<void>;
|
|
2745
3209
|
/**
|
|
2746
3210
|
* Build the template on a private path, then rename it into place.
|
|
2747
3211
|
*
|
|
@@ -2774,4 +3238,11 @@ declare class SqliteHandle implements DatabasePort, ServiceHandle {
|
|
|
2774
3238
|
*/
|
|
2775
3239
|
declare function sqlite(options?: SqliteOptions): SqliteHandle;
|
|
2776
3240
|
//#endregion
|
|
2777
|
-
|
|
3241
|
+
//#region src/index.d.ts
|
|
3242
|
+
/**
|
|
3243
|
+
* The module-scope network double, on msw's node server — what a test with no
|
|
3244
|
+
* chain to hang contracts on reaches for (chapter 16).
|
|
3245
|
+
*/
|
|
3246
|
+
declare const intercept: Intercept;
|
|
3247
|
+
//#endregion
|
|
3248
|
+
export { type ApiHandle, type ApiSpecification, type ApiSpecificationOptions, type BaseResult, type BrowserConsoleMessage, type BrowserLinkElement, type BrowserMetaElement, type BrowserOpenOptions, type BrowserPage, type BrowserPort, type CallResult, type CaptureScope, type CliEnv, type CliHandle, type CliInput, type CliOutput, type CliPort, type CliResult, type CliSpecification, type CliSpecificationOptions, type ComponentChain, type ComponentScenario, type ComponentUi, type ComponentVisitor, type ContainerAccessor, type ContainerPort, type Contract, type ContractInput, type ContractRequest, type ContractResponder, type ContractResponse, type ContractResponseValue, type Contracts, type DatabaseKeys, type DatabasePort, type DeviceOpenOptions, type DevicePort, type DeviceScreen, type DeviceTimeouts, type DirectoryAccessor, type DockerSpecConfig, type DomMount, type ElementKind, type ElementMatch, type ElementOptions, type ElementRef, type ExecOptions, type FetchResult, type FileAccessor, type FilesystemAccessor, type HonoApp, type HttpContractFilter, type HttpResponseInit, type HttpResult, type HttpStreamInit, type IntegrationHandle, type IntegrationSpecification, type IntegrationSpecificationOptions, type Intercept, type InterceptOptions, type InterceptScope, type IsolationStrategy, type JobHandle, type JobsHandle, type JobsResult, type JobsSpecification, type JobsSpecificationOptions, type JsonAccessor, type LandmarkKind, type LiterateRunFlags, type LiterateServeRegistration, type MatchFixtureOptions, type MatchableRequest, Matcher, type MatcherKind, type MobileBackendOptions, type MobileElementKind, type MobileElementMatch, type MobileElementRef, type MobileHandle, type MobileScenario, type MobileSpecification, type MobileSpecificationOptions, type MobileVisitor, type MockOfOptions, type MockPort, type PageResult, type PinnedClock, type PostgresOptions, ProcessHandle, type ProcessOptions, type RedisOptions, type RenderResult, type RenderSubject, type RenderedText, type ResponseAccessor, type ScreenNode, type ScreenResult, type ServerPort, type ServerResponse, type ServerSpec, type ServiceHandle, type ServiceRecord, type SpecDocument, type SpecEnvToken, type SpecFileAssertion, type SpecFixture, type SpecKind, type SpecRun, type SpecServeEntry, type SpecStream, type SpecificationConfig, type SqliteOptions, type SseEvent, type StreamBody, type TableAccessor, type TextAccessor, type TextFilter, type VisitScenario, type Visitor, type WaitUntilOptions, type WebsiteBackendOptions, type WebsiteHandle, type WebsiteSpecification, type WebsiteSpecificationOptions, anthropic, banner, button, clock, complementary, component, content, contentinfo, defineContract, defineContracts, dialog, disabled, enabled, field, focused, form, heading, http, intercept, link, listitem, main, match, mockOf, navigation, openai, option, postgres, processService as process, redis, region, required, row, search, selected, specification, sqlite, status, table, testId, text, valued, waitUntil, within };
|