@jterrazz/test 17.0.0 → 18.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.
Files changed (53) hide show
  1. package/dist/browser/index.js +1 -2
  2. package/dist/browser/services.js +1 -2
  3. package/dist/browser/stubs.js +1 -2
  4. package/dist/{shared-index.browser.js → browser-shared-index.browser.js} +82 -40
  5. package/dist/checker.js +15 -824
  6. package/dist/in-place.global-setup.d.ts +1 -0
  7. package/dist/in-place.global-setup.js +1 -1
  8. package/dist/index.d.ts +2 -14
  9. package/dist/index.js +83 -204
  10. package/dist/mobile-session.global-setup.d.ts +1 -0
  11. package/dist/oxlint.cjs +2 -2
  12. package/dist/oxlint.js +67 -2321
  13. package/dist/route-coverage.global-setup.d.ts +1 -0
  14. package/dist/route-coverage.global-setup.js +1 -2
  15. package/dist/services.global-setup.d.ts +1 -1
  16. package/dist/services.global-setup.js +1 -2
  17. package/dist/services.js +1 -1
  18. package/dist/shared-appium.adapter.js +14 -14
  19. package/dist/shared-artifacts.js +7 -5
  20. package/dist/shared-derived-projects.js +23 -8
  21. package/dist/shared-in-place.js +1 -1
  22. package/dist/shared-ledger.js +1 -1
  23. package/dist/shared-listened-port.js +19 -0
  24. package/dist/shared-literate-plugin.js +2 -1
  25. package/dist/shared-pause.js +12 -1
  26. package/dist/shared-playwright.adapter.js +17 -19
  27. package/dist/shared-process-alive.js +13 -0
  28. package/dist/shared-record-key.js +13 -0
  29. package/dist/shared-serve.js +107 -65
  30. package/dist/shared-services.global-setup.js +4 -9
  31. package/dist/shared-session-ledger.js +10 -6
  32. package/dist/shared-spec-document.js +2 -64
  33. package/dist/shared-spec-grammar.js +66 -0
  34. package/dist/shared-specs-modules.js +1033 -0
  35. package/dist/{shared-stream-body.js → shared-stub-reply.js} +40 -3
  36. package/dist/shared-stub-server.js +30 -78
  37. package/dist/shared-temp-root.global-setup.js +3 -10
  38. package/dist/shared-testcontainers.adapter.js +111 -8
  39. package/dist/temp-root.global-setup.js +1 -2
  40. package/dist/vitest.d.ts +17 -22
  41. package/dist/vitest.js +37 -53
  42. package/docs/concepts/architecture.md +20 -20
  43. package/docs/concepts/conventions.md +2 -2
  44. package/docs/guides/cli-specs.md +2 -0
  45. package/docs/guides/develop.md +20 -17
  46. package/docs/guides/mobile-specs.md +3 -3
  47. package/docs/guides/release.md +4 -3
  48. package/docs/guides/website-specs.md +1 -1
  49. package/docs/reference/linting.md +2 -2
  50. package/docs/reference/services.md +9 -9
  51. package/docs/reference/stubs.md +4 -2
  52. package/docs/start/set-up-a-project.md +31 -17
  53. package/package.json +10 -10
@@ -109,6 +109,16 @@ var StubQueue = class {
109
109
  };
110
110
  //#endregion
111
111
  //#region src/stubs/delivery/stream-body.ts
112
+ async function nextTask() {
113
+ const { port1, port2 } = new MessageChannel();
114
+ await new Promise((resolve) => {
115
+ port1.onmessage = () => {
116
+ port1.close();
117
+ resolve();
118
+ };
119
+ port2.postMessage(null);
120
+ });
121
+ }
112
122
  /** One serialisation for all three deliveries (msw server, msw worker, `node:http` stub). */
113
123
  function toReadableStream(body) {
114
124
  const encoder = new TextEncoder();
@@ -119,12 +129,39 @@ function toReadableStream(body) {
119
129
  controller.close();
120
130
  return;
121
131
  }
122
- if (body.delayBetweenChunks > 0 && index > 0) await pause(body.delayBetweenChunks);
132
+ if (index > 0) {
133
+ await pause(body.delayBetweenChunks);
134
+ await nextTask();
135
+ }
123
136
  index += 1;
124
137
  controller.enqueue(encoder.encode(chunk));
125
138
  } });
126
139
  }
127
140
  //#endregion
128
- export { StubQueue as n, toReadableStream as t };
141
+ //#region src/stubs/delivery/stub-reply.ts
142
+ /** A request's body as a stub's filter reads it: the parsed JSON, the raw text when it is none, `null` when there is none. */
143
+ function requestBody(raw) {
144
+ if (raw.length === 0) return null;
145
+ try {
146
+ return JSON.parse(raw);
147
+ } catch {
148
+ return raw;
149
+ }
150
+ }
151
+ /** The reply a matched stub states for this request, once its delay has passed. */
152
+ async function replyOf(response, observed) {
153
+ const reply = typeof response === "function" ? response(observed) : response;
154
+ await pause(reply.delay ?? 0);
155
+ return reply;
156
+ }
157
+ /** The `error` of the 501 every delivery answers a request it refuses, whichever process serves it. */
158
+ function refusedReply(summary) {
159
+ return `@jterrazz/test strict stubs: ${summary}`;
160
+ }
161
+ function unmatchedSummary(method, url) {
162
+ return `unmatched request ${method} ${url}`;
163
+ }
164
+ //#endregion
165
+ export { toReadableStream as a, unmatchedSummary as i, replyOf as n, StubQueue as o, requestBody as r, refusedReply as t };
129
166
 
130
- //# sourceMappingURL=shared-stream-body.js.map
167
+ //# sourceMappingURL=shared-stub-reply.js.map
@@ -1,6 +1,7 @@
1
+ import { n as refusal } from "./shared-refusal.js";
1
2
  import { n as isStreamBody } from "./shared-exchange.js";
2
- import { t as pause } from "./shared-pause.js";
3
- import { n as StubQueue, t as toReadableStream } from "./shared-stream-body.js";
3
+ import { a as toReadableStream, i as unmatchedSummary, n as replyOf, o as StubQueue, r as requestBody, t as refusedReply } from "./shared-stub-reply.js";
4
+ import { METHODS } from "node:http";
4
5
  //#region src/stubs/delivery/msw-handlers.ts
5
6
  const NEVER_CACHED = { "cache-control": "no-store" };
6
7
  function toMswResponse(msw, response) {
@@ -52,13 +53,14 @@ function handlerOf(msw, method) {
52
53
  function buildStubHandlers(msw, stubs, bypass) {
53
54
  const queue = new StubQueue(stubs);
54
55
  let violation = null;
55
- const recordViolation = (method, url) => {
56
- violation ??= queue.unmatchedError(method, url);
57
- return msw.HttpResponse.json({ error: `@jterrazz/test strict stubs: unmatched request ${method} ${url}` }, {
56
+ const refuse = (error, summary) => {
57
+ violation ??= error;
58
+ return msw.HttpResponse.json({ error: refusedReply(summary) }, {
58
59
  headers: NEVER_CACHED,
59
60
  status: 501
60
61
  });
61
62
  };
63
+ const recordViolation = (method, url) => refuse(queue.unmatchedError(method, url), unmatchedSummary(method, url));
62
64
  const handlers = [];
63
65
  for (const route of queue.routes) for (const method of route.methods) {
64
66
  const handlerFn = handlerOf(msw, method);
@@ -67,90 +69,25 @@ function buildStubHandlers(msw, stubs, bypass) {
67
69
  const observed = await observe(request);
68
70
  const stub = queue.take(observed);
69
71
  if (!stub) return recordViolation(request.method, request.url);
70
- const response = typeof stub.response === "function" ? stub.response(observed) : stub.response;
71
- await pause(response.delay ?? 0);
72
- return toMswResponse(msw, response);
72
+ return toMswResponse(msw, await replyOf(stub.response, observed));
73
73
  }));
74
74
  }
75
75
  handlers.push(msw.http.all("*", ({ request }) => bypass?.(request.url) === true ? msw.passthrough() : recordViolation(request.method, request.url)));
76
76
  return {
77
77
  handlers,
78
+ refuse,
78
79
  violation: () => queue.verdict(violation)
79
80
  };
80
81
  }
81
82
  async function observe(request) {
82
- let body = null;
83
- const rawText = await request.clone().text();
84
- if (rawText) try {
85
- body = JSON.parse(rawText);
86
- } catch {
87
- body = rawText;
88
- }
89
83
  return {
90
- body,
84
+ body: requestBody(await request.clone().text()),
91
85
  headers: Object.fromEntries(request.headers),
92
86
  method: request.method.toUpperCase(),
93
87
  url: request.url
94
88
  };
95
89
  }
96
90
  //#endregion
97
- //#region src/stubs/delivery/same-process/fetch-refusals.ts
98
- const PROBE = "http://request-init.invalid/";
99
- const probeOf = (init) => init.body instanceof ReadableStream ? {
100
- ...init,
101
- body: new Blob()
102
- } : init;
103
- /**
104
- * Wraps `underlying` so a RequestInit that fetch itself refuses rejects with fetch's own TypeError, before any stub can serve it.
105
- * @see https://fetch.spec.whatwg.org/#dom-request
106
- */
107
- function refusingWhatFetchRefuses(underlying) {
108
- return async (input, init) => {
109
- if (init === void 0) return await underlying(input);
110
- const { method } = new Request(PROBE, probeOf(init));
111
- return await underlying(input, {
112
- ...init,
113
- method
114
- });
115
- };
116
- }
117
- //#endregion
118
- //#region src/stubs/delivery/same-process/owned-body.ts
119
- async function release(reader, reason) {
120
- try {
121
- await reader.cancel(reason);
122
- } catch {}
123
- }
124
- function withOwnedBody(response) {
125
- const { body } = response;
126
- if (body === null) return response;
127
- const reader = body.getReader();
128
- const owned = new ReadableStream({
129
- cancel: (reason) => {
130
- release(reader, reason);
131
- },
132
- pull: async (controller) => {
133
- const { done, value } = await reader.read();
134
- if (done) controller.close();
135
- else controller.enqueue(value);
136
- }
137
- });
138
- const reply = new Response(owned, {
139
- headers: response.headers,
140
- status: response.status,
141
- statusText: response.statusText
142
- });
143
- Object.defineProperties(reply, {
144
- redirected: { value: response.redirected },
145
- type: { value: response.type },
146
- url: { value: response.url }
147
- });
148
- return reply;
149
- }
150
- function owningBodies(underlying) {
151
- return async (input, init) => withOwnedBody(await underlying(input, init));
152
- }
153
- //#endregion
154
91
  //#region src/stubs/delivery/same-process/stub-server.ts
155
92
  async function loadMsw() {
156
93
  const [{ setupServer }, msw] = await Promise.all([import("msw/node"), import("msw")]);
@@ -160,25 +97,40 @@ async function loadMsw() {
160
97
  };
161
98
  }
162
99
  let serverInstance = null;
163
- let installedFetch = null;
100
+ let refuseOffTheWire = null;
101
+ function offTheWireError(request) {
102
+ return new Error(refusal("network-strict-after-first-stub", {
103
+ fix: "send a method in the capitals the HTTP parser reads (`PATCH`, not `patch`): fetch upper-cases DELETE, GET, HEAD, OPTIONS, POST and PUT, and sends any other as written",
104
+ found: `an outgoing request the network double cannot read off the wire, ${request.method} ${request.url}: its method is not one of node's \`http.METHODS\`, so no stub could answer it and it would reach the network`
105
+ }));
106
+ }
107
+ function guardOffTheWireMethods() {
108
+ const next = globalThis.fetch;
109
+ globalThis.fetch = async (input, init) => {
110
+ const request = new Request(input, init);
111
+ if (refuseOffTheWire !== null && !METHODS.includes(request.method)) return refuseOffTheWire(request);
112
+ return await next(request);
113
+ };
114
+ }
164
115
  async function ensureStubServer() {
165
116
  if (serverInstance !== null) return serverInstance;
166
117
  const { setupServer } = await loadMsw();
167
118
  const server = setupServer();
168
- server.listen({ onUnhandledRequest: "bypass" });
119
+ server.listen({ onUnhandledFrame: "bypass" });
120
+ guardOffTheWireMethods();
169
121
  serverInstance = server;
170
- installedFetch = globalThis.fetch;
171
- globalThis.fetch = owningBodies(refusingWhatFetchRefuses(installedFetch));
172
122
  return server;
173
123
  }
174
124
  /** The trailing catch-all records any request no stub accepted; the chain's pipeline rethrows it. */
175
125
  async function registerStubs(stubs) {
176
126
  const server = await ensureStubServer();
177
127
  const { msw } = await loadMsw();
178
- const { handlers, violation } = buildStubHandlers(msw, stubs);
128
+ const { handlers, refuse, violation } = buildStubHandlers(msw, stubs);
179
129
  server.use(...handlers);
130
+ refuseOffTheWire = (request) => refuse(offTheWireError(request), `unreadable request ${request.method} ${request.url}`);
180
131
  return {
181
132
  cleanup: () => {
133
+ refuseOffTheWire = null;
182
134
  server.resetHandlers();
183
135
  },
184
136
  violation
@@ -1,17 +1,10 @@
1
1
  import { a as EVIDENCE_DIR } from "./shared-artifacts.js";
2
2
  import { t as setupEntry } from "./shared-setup-entry.js";
3
+ import { t as processAlive } from "./shared-process-alive.js";
3
4
  import { existsSync, mkdirSync, mkdtempSync, readdirSync, rmSync } from "node:fs";
4
5
  import { basename, dirname, join, resolve } from "node:path";
5
6
  import { tmpdir } from "node:os";
6
7
  //#region src/chain/paths/run-folders.ts
7
- function isAlive(pid) {
8
- try {
9
- process.kill(pid, 0);
10
- return true;
11
- } catch (error) {
12
- return error instanceof Error && "code" in error && error.code === "EPERM";
13
- }
14
- }
15
8
  function runOf(entry) {
16
9
  const pid = Number(entry.split("-")[0]);
17
10
  return Number.isInteger(pid) && pid > 0 ? pid : null;
@@ -20,7 +13,7 @@ function runOf(entry) {
20
13
  function sweepOrphanRoots(parent) {
21
14
  for (const entry of readdirSync(parent)) {
22
15
  const pid = runOf(entry);
23
- if (pid !== null && !isAlive(pid)) rmSync(join(parent, entry), {
16
+ if (pid !== null && !processAlive(pid)) rmSync(join(parent, entry), {
24
17
  force: true,
25
18
  recursive: true
26
19
  });
@@ -31,7 +24,7 @@ function sweepFinishedEvidence(folder) {
31
24
  if (!existsSync(folder)) return;
32
25
  for (const entry of readdirSync(folder)) {
33
26
  const pid = runOf(entry);
34
- if (pid === null || !isAlive(pid)) rmSync(join(folder, entry), {
27
+ if (pid === null || !processAlive(pid)) rmSync(join(folder, entry), {
35
28
  force: true,
36
29
  recursive: true
37
30
  });
@@ -1,10 +1,118 @@
1
1
  import { t as isRecord } from "./shared-json-shape.js";
2
- import { t as invariant } from "./shared-refusal.js";
2
+ import { n as refusal, t as invariant } from "./shared-refusal.js";
3
+ import { t as listenedPort } from "./shared-listened-port.js";
3
4
  import { t as loadPeer } from "./shared-peer.js";
5
+ import { execSync } from "node:child_process";
4
6
  import { once } from "node:events";
5
7
  import { createServer } from "node:http";
6
8
  import { text } from "node:stream/consumers";
7
9
  import { hostname } from "node:os";
10
+ //#region src/services/containers/docker-cli.ts
11
+ const DEFAULT_TIMEOUT = 1e4;
12
+ /** How every refusal of a run that asked for Docker names a daemon that did not answer. */
13
+ const DOCKER_ABSENT = "no Docker daemon answered";
14
+ /** Docker-aware mode reads what the daemon answers: a run where none answers is refused, never passed. */
15
+ function requireDockerDaemon() {
16
+ try {
17
+ execSync("docker version --format \"{{.Server.Version}}\"", {
18
+ stdio: "ignore",
19
+ timeout: DEFAULT_TIMEOUT
20
+ });
21
+ } catch (error) {
22
+ throw new Error(refusal({
23
+ fix: "start Docker (Docker Desktop, OrbStack, colima or another) and run again",
24
+ found: `specification.cli({ docker }) asked for Docker, and ${DOCKER_ABSENT}`,
25
+ read: "docs/guides/cli-specs.md#docker-aware-mode",
26
+ why: "Docker-aware mode reads the containers a run spawned, so it needs a Docker daemon."
27
+ }), { cause: error });
28
+ }
29
+ }
30
+ function findContainersByLabel(key, value) {
31
+ try {
32
+ return execSync(`docker ps -aq -f label=${key}=${value}`, {
33
+ encoding: "utf8",
34
+ timeout: DEFAULT_TIMEOUT
35
+ }).split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
36
+ } catch {
37
+ return [];
38
+ }
39
+ }
40
+ /** A field of a value read off docker's JSON or a failed process: absent wherever the value does not hold it. */
41
+ function fieldOf(value, ...keys) {
42
+ if (typeof value !== "object" || value === null) return;
43
+ const key = keys.find((candidate) => candidate in value);
44
+ return key === void 0 ? void 0 : Reflect.get(value, key);
45
+ }
46
+ const streamOf = (error, key) => {
47
+ const stream = fieldOf(error, key);
48
+ return typeof stream === "string" ? stream : void 0;
49
+ };
50
+ function inspectContainer(id) {
51
+ const raw = execSync(`docker inspect ${id}`, {
52
+ encoding: "utf8",
53
+ timeout: DEFAULT_TIMEOUT
54
+ });
55
+ const parsed = JSON.parse(raw);
56
+ if (!Array.isArray(parsed)) return parsed;
57
+ return parsed[0];
58
+ }
59
+ function removeContainers(ids) {
60
+ if (ids.length === 0) return;
61
+ try {
62
+ execSync(`docker rm -f ${ids.join(" ")}`, {
63
+ encoding: "utf8",
64
+ stdio: "pipe",
65
+ timeout: DEFAULT_TIMEOUT
66
+ });
67
+ } catch {}
68
+ }
69
+ function containerLogs(id) {
70
+ try {
71
+ return execSync(`docker logs ${id}`, {
72
+ encoding: "utf8",
73
+ stdio: [
74
+ "ignore",
75
+ "pipe",
76
+ "pipe"
77
+ ],
78
+ timeout: DEFAULT_TIMEOUT
79
+ });
80
+ } catch (error) {
81
+ return streamOf(error, "stdout") ?? streamOf(error, "stderr") ?? "";
82
+ }
83
+ }
84
+ function readContainerFile(id, path) {
85
+ return execSync(`docker exec ${id} cat ${JSON.stringify(path)}`, {
86
+ encoding: "utf8",
87
+ timeout: DEFAULT_TIMEOUT
88
+ });
89
+ }
90
+ function execInContainer(id, argv) {
91
+ try {
92
+ const quoted = argv.map((a) => JSON.stringify(a)).join(" ");
93
+ return {
94
+ exitCode: 0,
95
+ stderr: "",
96
+ stdout: execSync(`docker exec ${id} ${quoted}`, {
97
+ encoding: "utf8",
98
+ stdio: [
99
+ "ignore",
100
+ "pipe",
101
+ "pipe"
102
+ ],
103
+ timeout: DEFAULT_TIMEOUT
104
+ })
105
+ };
106
+ } catch (error) {
107
+ const status = fieldOf(error, "status");
108
+ return {
109
+ exitCode: typeof status === "number" ? status : 1,
110
+ stderr: streamOf(error, "stderr") ?? (error instanceof Error ? error.message : ""),
111
+ stdout: streamOf(error, "stdout") ?? ""
112
+ };
113
+ }
114
+ }
115
+ //#endregion
8
116
  //#region src/services/containers/run-containers.ts
9
117
  function canonical(value) {
10
118
  return isRecord(value) ? Object.fromEntries(Object.entries(value).toSorted(([a], [b]) => a.localeCompare(b)).map(([key, entry]) => [key, canonical(entry)])) : value;
@@ -95,17 +203,12 @@ async function serveContainers(containers) {
95
203
  const server = createServer((request, response) => {
96
204
  respond(containers, request, response);
97
205
  });
98
- server.listen(0, "127.0.0.1");
99
- await once(server, "listening");
100
- const address = server.address();
101
- if (address === null || typeof address === "string") throw invariant("the run containers are served on no port");
102
- const { port } = address;
103
206
  return {
104
207
  close: async () => {
105
208
  server.close();
106
209
  await once(server, "close");
107
210
  },
108
- url: `http://127.0.0.1:${port}`
211
+ url: `http://127.0.0.1:${await listenedPort(server, { host: "127.0.0.1" })}`
109
212
  };
110
213
  }
111
214
  //#endregion
@@ -198,6 +301,6 @@ var TestcontainersAdapter = class {
198
301
  }
199
302
  };
200
303
  //#endregion
201
- export { field as a, RunContainers as i, REUSED_LABEL as n, serveContainers as o, TestcontainersAdapter as r, OWNER_LABEL as t };
304
+ export { field as a, containerLogs as c, findContainersByLabel as d, inspectContainer as f, requireDockerDaemon as h, RunContainers as i, execInContainer as l, removeContainers as m, REUSED_LABEL as n, serveContainers as o, readContainerFile as p, TestcontainersAdapter as r, DOCKER_ABSENT as s, OWNER_LABEL as t, fieldOf as u };
202
305
 
203
306
  //# sourceMappingURL=shared-testcontainers.adapter.js.map
@@ -1,2 +1 @@
1
- import { n as setup, r as tempRootSetup, t as TEMP_ROOTS_PARENT } from "./shared-temp-root.global-setup.js";
2
- export { TEMP_ROOTS_PARENT, setup as default, tempRootSetup };
1
+ export { t as TEMP_ROOTS_PARENT, n as default, r as tempRootSetup } from "./shared-temp-root.global-setup.js";
package/dist/vitest.d.ts CHANGED
@@ -13,11 +13,18 @@ type SpecificationProjectOptions = {
13
13
  /** Raise (or lower) the 30s budget for this project alone: each test's, and each hook's, which tears down what the test stood on. */
14
14
  timeout?: number;
15
15
  };
16
- /** What a specification whose runner boots its subject in this process accepts on top. */
17
- type InProcessProjectOptions = SpecificationProjectOptions & {
18
- /** The `*.specification.ts` module its spec files import their runner from: stated, a worker's files share one module graph and the module runs again before each file, so every file still boots its own subject. */
19
- runner?: string;
16
+ /** What a specification whose spec files import their runner from its specification modules accepts on top. */
17
+ type RunnerOption = {
18
+ /** The `*.specification.ts` modules its spec files import their runner from: stated, a worker's files share one module graph and each module runs again before each file, so every file still boots its own subject. */
19
+ runner?: readonly string[] | string;
20
20
  };
21
+ /** What a specification whose runner boots its subject in this process accepts on top. */
22
+ type InProcessProjectOptions = RunnerOption & SpecificationProjectOptions;
23
+ //#endregion
24
+ //#region src/specifications/api/api.project.d.ts
25
+ /** `api()`: the app met through HTTP. */
26
+ type ApiProjectOptions = InProcessProjectOptions;
27
+ export declare function api(options?: ApiProjectOptions): TestProjectInlineConfiguration;
21
28
  //#endregion
22
29
  //#region src/specifications/cli/documents/literate-plugin.d.ts
23
30
  type LiterateOptions = {
@@ -50,23 +57,18 @@ type LiterateDoor = {
50
57
  literate?: false | Partial<LiterateOptions>;
51
58
  };
52
59
  /** `cli()`: a binary, and the documents that specify it. */
53
- type CliProjectOptions = LiterateDoor & SpecificationProjectOptions;
60
+ type CliProjectOptions = LiterateDoor & RunnerOption & SpecificationProjectOptions;
54
61
  /** A cli project: the specification's `.spec.ts` files and its documents (docs/guides/spec-documents.md); the plugin's glob joins this project's include, never the root config. */
55
62
  export declare function cli(options?: CliProjectOptions): TestProjectInlineConfiguration;
56
63
  //#endregion
57
- //#region src/specifications/api/api.project.d.ts
58
- /** `api()`: the app met through HTTP. */
59
- type ApiProjectOptions = InProcessProjectOptions;
60
- export declare function api(options?: ApiProjectOptions): TestProjectInlineConfiguration;
61
- //#endregion
62
64
  //#region src/specifications/component/component.project.d.ts
63
65
  type ComponentOptions = {
64
66
  /** Freeze the page's `Date` for every render of the project. */
65
67
  clock?: string;
66
68
  /** `Accept-Language` and `Intl` locale of the page. Default `'en-US'`. */
67
69
  locale?: string;
68
- /** The directory `vite` and `wrap` resolve against; defaults to the calling file's. */
69
- root?: string;
70
+ /** The directory `vite` and `wrap` resolve against, absolute; `vitestConfig` hands it the project root, or the `root` a declared project states read from it. */
71
+ root: string;
70
72
  /** IANA zone the page's `Date` reports in. Default `'UTC'`. */
71
73
  timezone?: string;
72
74
  /** The app's Vite pipeline: a config path, the object, or a framework's function. Only the pipeline is adopted. */
@@ -82,7 +84,7 @@ type ComponentOptions = {
82
84
  /** `component()`: rendered units, in the `__tests__/` of the folder of the components they cover; under a specs tree, beside them. */
83
85
  type ComponentProjectOptions = ComponentOptions & SpecificationProjectOptions;
84
86
  /** A component project: renders in a real Chromium; the default `exclude` keeps it out of `specs/`. */
85
- export declare function component(options?: ComponentProjectOptions): Promise<TestProjectInlineConfiguration>;
87
+ export declare function component(options: ComponentProjectOptions): Promise<TestProjectInlineConfiguration>;
86
88
  //#endregion
87
89
  //#region src/specifications/integration/integration.project.d.ts
88
90
  /** `integration()`: a module against real services, or against a golden. */
@@ -108,12 +110,12 @@ type MobileProjectOptions = SpecificationProjectOptions & ({
108
110
  device?: never;
109
111
  root?: never;
110
112
  });
111
- /** A simulator cannot share a machine with itself: serial. A global setup holds one driver session for the run. */
113
+ /** A simulator cannot share a machine with itself: serial, unless `serial: false` states a server that gives every session an app of its own. A global setup holds the run's driver sessions, one per worker. */
112
114
  export declare function mobile(options?: MobileProjectOptions): TestProjectInlineConfiguration;
113
115
  //#endregion
114
116
  //#region src/specifications/website/website.project.d.ts
115
117
  /** `website()`: the assembled product, met through a served page. */
116
- type WebsiteProjectOptions = SpecificationProjectOptions;
118
+ type WebsiteProjectOptions = RunnerOption & SpecificationProjectOptions;
117
119
  export declare function website(options?: WebsiteProjectOptions): TestProjectInlineConfiguration;
118
120
  //#endregion
119
121
  //#region src/vitest/unit.project.d.ts
@@ -128,13 +130,6 @@ type UnitProjectOptions = SpecificationProjectOptions & {
128
130
  export declare function unit(options?: UnitProjectOptions): TestProjectInlineConfiguration;
129
131
  //#endregion
130
132
  //#region src/index.vitest.d.ts
131
- /** A vite/vitest config, merged over the preset's defaults, plus `literate`. */
132
- export type SpecConfig = ViteUserConfig & {
133
- /** Turns matching `<case>.spec.yaml` files into tests; with `projects`, put the plugin in the collecting project instead. */
134
- literate?: LiterateOptions;
135
- };
136
- /** The shared vitest config: what you pass is merged over the defaults. */
137
- export declare function defineSpecConfig(config?: SpecConfig): ViteUserConfig;
138
133
  export declare const vitestConfig: (configUrl: string) => Promise<ViteUserConfig>;
139
134
  //#endregion
140
135
  export type { ApiProjectOptions, CliProjectOptions, ComponentProjectOptions, InProcessProjectOptions, IntegrationProjectOptions, JobProjectOptions, LintProjectOptions, LiterateDoor, LiterateOptions, LiteratePlugin, MobileProjectOptions, SpecificationProjectOptions, UnitProjectOptions, WebsiteProjectOptions };