@jterrazz/test 17.0.0 → 18.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +180 -69
  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 +13 -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
@@ -39,4 +39,5 @@ export declare function routeCoverageSetup(): string;
39
39
  export declare function setupRoutes(project: RouteProject): () => Promise<void>;
40
40
  export default function setup(project: TestProject): () => Promise<void>;
41
41
  //#endregion
42
+ export {};
42
43
  //# sourceMappingURL=route-coverage.global-setup.d.ts.map
@@ -1,2 +1 @@
1
- import { n as setup, r as setupRoutes, t as routeCoverageSetup } from "./shared-route-coverage.global-setup.js";
2
- export { setup as default, routeCoverageSetup, setupRoutes };
1
+ export { n as default, t as routeCoverageSetup, r as setupRoutes } from "./shared-route-coverage.global-setup.js";
@@ -26,7 +26,6 @@ declare function serveContainers(containers: RunContainers): Promise<ServedConta
26
26
  //#endregion
27
27
  //#region src/services/containers/services.global-setup.d.ts
28
28
  /** Every project's setup joins the one run's containers. Default images are pulled in the background, so the first file waits on a pull at most once; a project declaring no service needs no Docker. */
29
- export declare const DOCKER_ABSENT = "no Docker daemon answered";
30
29
  /** `connect` stands in for the runtime's own client lookup, so the refusal is reachable without a daemon to stop. */
31
30
  export declare function prepareDocker(connect?: () => Promise<ContainerRuntimeClient>): Promise<void>;
32
31
  export declare function servicesSetup(): string;
@@ -40,4 +39,5 @@ type RunSeams = {
40
39
  };
41
40
  export default function setup(project: RunProject, seams?: Partial<RunSeams>): Promise<() => Promise<void>>;
42
41
  //#endregion
42
+ export {};
43
43
  //# sourceMappingURL=services.global-setup.d.ts.map
@@ -1,2 +1 @@
1
- import { i as setup, n as prepareDocker, r as servicesSetup, t as DOCKER_ABSENT } from "./shared-services.global-setup.js";
2
- export { DOCKER_ABSENT, setup as default, prepareDocker, servicesSetup };
1
+ export { r as default, t as prepareDocker, n as servicesSetup } from "./shared-services.global-setup.js";
package/dist/services.js CHANGED
@@ -3,7 +3,7 @@ import { n as refusal, t as invariant } from "./shared-refusal.js";
3
3
  import { t as pause } from "./shared-pause.js";
4
4
  import { n as processService, t as ProcessHandle } from "./shared-process.adapter.js";
5
5
  import { n as requireBuiltPeer, t as loadPeer } from "./shared-peer.js";
6
- import { c as SQLITE_TEMPLATE_DIR } from "./shared-artifacts.js";
6
+ import { l as SQLITE_TEMPLATE_DIR } from "./shared-artifacts.js";
7
7
  import { t as SERVICE_IMAGES } from "./shared-service-images.js";
8
8
  import { closeSync, copyFileSync, existsSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, renameSync, rmSync, statSync, unlinkSync, writeSync } from "node:fs";
9
9
  import { join, resolve } from "node:path";
@@ -1,11 +1,10 @@
1
1
  import { n as refusal } from "./shared-refusal.js";
2
- import { t as pause } from "./shared-pause.js";
3
- import { u as evidenceDirectory } from "./shared-artifacts.js";
2
+ import { n as untilDeadline } from "./shared-pause.js";
3
+ import { d as evidenceFile } from "./shared-artifacts.js";
4
4
  import { n as SERVER_CALL_TIMEOUT_MS } from "./shared-session-ledger.js";
5
5
  import { c as scopeChain } from "./shared-landmarks.js";
6
6
  import { a as ambiguityError, c as asWrittenUnscoped, i as shortened, s as asWritten, t as scenarioFailure } from "./shared-scenario-failure.js";
7
- import { mkdtempSync, writeFileSync } from "node:fs";
8
- import { resolve } from "node:path";
7
+ import { writeFileSync } from "node:fs";
9
8
  //#region src/specifications/mobile/session/driver-session.ts
10
9
  const IDLE_SESSION_BUDGET_S = 300;
11
10
  const IOS_PREFIX = /^ios\s+/iu;
@@ -355,15 +354,17 @@ var AppiumAdapter = class {
355
354
  async resolveOne(driver, target, verb) {
356
355
  const chain = scopeChain(target);
357
356
  const anyTarget = verb === "see";
358
- const deadline = Date.now() + this.actionTimeoutMs;
359
357
  let tryScroll = true;
360
- for (;;) {
361
- const resolved = await this.resolveChain(driver, chain, anyTarget, tryScroll);
358
+ const resolved = await untilDeadline(async () => {
359
+ const found = await this.resolveChain(driver, chain, anyTarget, tryScroll);
362
360
  tryScroll = false;
363
- if (resolved) return resolved;
364
- if (Date.now() > deadline) throw await this.timeoutError(driver, target, verb);
365
- await pause(POLL_INTERVAL_MS);
366
- }
361
+ return found ?? void 0;
362
+ }, {
363
+ interval: POLL_INTERVAL_MS,
364
+ timeout: this.actionTimeoutMs
365
+ });
366
+ if (resolved === void 0) throw await this.timeoutError(driver, target, verb);
367
+ return resolved;
367
368
  }
368
369
  async resolveChain(driver, chain, anyTarget, tryScroll) {
369
370
  let scope = driver;
@@ -385,11 +386,10 @@ var AppiumAdapter = class {
385
386
  }
386
387
  return element;
387
388
  }
388
- /** Screenshot the failing state into a temp file; never masks the original error. */
389
+ /** Screenshot the failing state into the evidence folder; never masks the original error. */
389
390
  async captureEvidence(driver) {
390
391
  try {
391
- const dir = mkdtempSync(evidenceDirectory("mobile-"));
392
- const path = resolve(dir, "failure.png");
392
+ const path = evidenceFile("mobile");
393
393
  writeFileSync(path, Buffer.from(await driver.takeScreenshot(), "base64"));
394
394
  return path;
395
395
  } catch {
@@ -1,4 +1,4 @@
1
- import { mkdirSync } from "node:fs";
1
+ import { mkdirSync, mkdtempSync } from "node:fs";
2
2
  import { basename, join, resolve } from "node:path";
3
3
  import { tmpdir } from "node:os";
4
4
  //#region lint/kit/project/artifacts.ts
@@ -11,14 +11,16 @@ const SQLITE_TEMPLATE_DIR = `${VITEST_ARTIFACTS_DIR}/sqlite`;
11
11
  const ATTACHMENTS_DIR = `${VITEST_ARTIFACTS_DIR}/attachments`;
12
12
  /** Under the project: the run's temp root is gone by the time the error naming the file is read. */
13
13
  const EVIDENCE_DIR = `${VITEST_ARTIFACTS_DIR}/evidence`;
14
- /** Filed under the run's temp root name, `<pid>-...`: the next run's start sweeps every run that is no longer alive. */
15
- function evidenceDirectory(prefix) {
14
+ /** Where a failing scenario's screenshot goes, in a folder of its own filed under the run's temp root name, `<pid>-...`: the next run's start sweeps every run that is no longer alive. */
15
+ function evidenceFile(kind) {
16
16
  const parent = resolve(EVIDENCE_DIR, basename(tmpdir()));
17
17
  mkdirSync(parent, { recursive: true });
18
- return join(parent, prefix);
18
+ return join(mkdtempSync(join(parent, `${kind}-`)), "failure.png");
19
19
  }
20
20
  const SCREENSHOTS_DIR = `${VITEST_ARTIFACTS_DIR}/screenshots`;
21
21
  const BLOB_DIR = `${VITEST_ARTIFACTS_DIR}/blob`;
22
+ /** Node's compile cache, which every Node process of a run reads and writes: keyed by content, so an edit is never served old code. */
23
+ const NODE_COMPILE_CACHE_DIR = `${ARTIFACTS_DIR}/node-compile-cache`;
22
24
  /** Vitest 5's file-writing reporters, each pinned under the artifacts folder. */
23
25
  const REPORTER_OUTPUT_FILES = {
24
26
  blob: `${BLOB_DIR}/blob.json`,
@@ -27,6 +29,6 @@ const REPORTER_OUTPUT_FILES = {
27
29
  junit: `${VITEST_ARTIFACTS_DIR}/junit/output.xml`
28
30
  };
29
31
  //#endregion
30
- export { EVIDENCE_DIR as a, SQLITE_TEMPLATE_DIR as c, COVERAGE_DIR as i, VITEST_ARTIFACTS_DIR as l, ATTACHMENTS_DIR as n, REPORTER_OUTPUT_FILES as o, BLOB_DIR as r, SCREENSHOTS_DIR as s, ARTIFACTS_DIR as t, evidenceDirectory as u };
32
+ export { EVIDENCE_DIR as a, SCREENSHOTS_DIR as c, evidenceFile as d, COVERAGE_DIR as i, SQLITE_TEMPLATE_DIR as l, ATTACHMENTS_DIR as n, NODE_COMPILE_CACHE_DIR as o, BLOB_DIR as r, REPORTER_OUTPUT_FILES as s, ARTIFACTS_DIR as t, VITEST_ARTIFACTS_DIR as u };
31
33
 
32
34
  //# sourceMappingURL=shared-artifacts.js.map
@@ -1,15 +1,17 @@
1
1
  import { t as SPECIFICATIONS } from "./shared-specifications.js";
2
2
  import { TESTS_FOLDER } from "@jterrazz/typescript/rules";
3
- import { existsSync, readdirSync } from "node:fs";
3
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
4
4
  import { basename, join } from "node:path";
5
5
  //#region lint/kit/project/derived-projects.ts
6
6
  const SPECS = "specs";
7
7
  const UNIT = "unit";
8
8
  const FOLDERED = /* @__PURE__ */ new Set([...SPECIFICATIONS, "component"]);
9
- const IN_PROCESS = /* @__PURE__ */ new Set([
9
+ const RUNNER_SHARED = /* @__PURE__ */ new Set([
10
10
  "api",
11
+ "cli",
11
12
  "integration",
12
- "job"
13
+ "job",
14
+ "website"
13
15
  ]);
14
16
  const SPECIFICATION_MODULE = ".specification.ts";
15
17
  const MODULE_TEST = /\.test\.ts$/u;
@@ -23,14 +25,27 @@ function holds(path, pattern) {
23
25
  });
24
26
  }
25
27
  const foldersIn = (path) => existsSync(path) ? readdirSync(path, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !isUnwalked(entry.name)).map(({ name }) => name).toSorted() : [];
28
+ function inImportOrder(directory, modules) {
29
+ const ordered = [];
30
+ const seen = /* @__PURE__ */ new Set();
31
+ const visit = (module) => {
32
+ if (seen.has(module)) return;
33
+ seen.add(module);
34
+ const text = readFileSync(join(directory, module), "utf8");
35
+ for (const imported of modules.filter((other) => text.includes(`'./${other.slice(0, -2)}js'`))) visit(imported);
36
+ ordered.push(module);
37
+ };
38
+ for (const module of modules) visit(module);
39
+ return ordered;
40
+ }
26
41
  function runnerOf(root, folder, specification) {
27
- if (!IN_PROCESS.has(specification)) return {};
28
- const modules = readdirSync(join(root, folder)).filter((name) => name.endsWith(SPECIFICATION_MODULE));
29
- const own = `${specification}${SPECIFICATION_MODULE}`;
30
- return modules.length === 1 && modules[0] === own ? { runner: `./${folder}/${own}` } : {};
42
+ if (!RUNNER_SHARED.has(specification)) return {};
43
+ const directory = join(root, folder);
44
+ const modules = readdirSync(directory).filter((name) => name.endsWith(SPECIFICATION_MODULE)).toSorted();
45
+ return modules.length === 0 ? {} : { runner: inImportOrder(directory, modules).map((name) => `./${folder}/${name}`) };
31
46
  }
32
47
  const isSpecsMember = (root) => basename(root) === SPECS && existsSync(join(root, "package.json"));
33
- /** The projects a tree names, each with the options it derives: one per `specs/<specification>/` folder (an in-process one handed as its `runner` the folder's one specification module, `<specification>.specification.ts`), `unit` over each root holding a module test (`specs/unit/` is one root among them), `component` over each holding a rendered one. */
48
+ /** The projects a tree names, each with the options it derives: one per `specs/<specification>/` folder (one whose spec files meet their subject through a runner handed as its `runner` every specification module of the folder, `*.specification.ts`, a module after those it imports), `unit` over each root holding a module test (`specs/unit/` is one root among them), `component` over each holding a rendered one. */
34
49
  function derivedProjects(root) {
35
50
  const member = isSpecsMember(root);
36
51
  const prefix = member ? "" : `${SPECS}/`;
@@ -167,6 +167,6 @@ function writeInPlace(site, render, folder = pendingFolder?.()) {
167
167
  save(site.file, source, folder);
168
168
  }
169
169
  //#endregion
170
- export { callerFrames as a, writePendingRewrites as i, deferRewritesTo as n, getCallerDir as o, writeInPlace as r, callSiteOf as t };
170
+ export { getCallerDir as a, writePendingRewrites as i, deferRewritesTo as n, writeInPlace as r, callSiteOf as t };
171
171
 
172
172
  //# sourceMappingURL=shared-in-place.js.map
@@ -1,5 +1,5 @@
1
1
  import { t as isRecord } from "./shared-json-shape.js";
2
- import { l as VITEST_ARTIFACTS_DIR } from "./shared-artifacts.js";
2
+ import { u as VITEST_ARTIFACTS_DIR } from "./shared-artifacts.js";
3
3
  import { mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
4
4
  import { join } from "node:path";
5
5
  import { randomUUID } from "node:crypto";
@@ -0,0 +1,19 @@
1
+ import { t as invariant } from "./shared-refusal.js";
2
+ //#region src/chain/listened-port.ts
3
+ /** Listens, on `host` when one is named, and answers the port it got; a port another server holds rejects with its error. */
4
+ async function listenedPort(server, { host, port = 0 } = {}) {
5
+ await new Promise((resolve, reject) => {
6
+ server.once("error", reject);
7
+ server.listen(port, host, () => {
8
+ server.off("error", reject);
9
+ resolve();
10
+ });
11
+ });
12
+ const address = server.address();
13
+ if (address === null || typeof address === "string") throw invariant("a server listening on a TCP port answered no port");
14
+ return address.port;
15
+ }
16
+ //#endregion
17
+ export { listenedPort as t };
18
+
19
+ //# sourceMappingURL=shared-listened-port.js.map
@@ -1,5 +1,6 @@
1
1
  import { n as refusal } from "./shared-refusal.js";
2
- import { d as isRelativePath, i as readSpecFile, l as SpecSyntaxError, u as dedent } from "./shared-spec-document.js";
2
+ import { c as isRelativePath, s as SpecSyntaxError } from "./shared-spec-grammar.js";
3
+ import { i as readSpecFile, o as dedent } from "./shared-spec-document.js";
3
4
  import { readFileSync } from "node:fs";
4
5
  import { basename, isAbsolute, resolve } from "node:path";
5
6
  //#region src/disk/spec-path.ts
@@ -5,7 +5,18 @@ async function pause(ms) {
5
5
  setTimeout(resolve, ms);
6
6
  });
7
7
  }
8
+ /** Asks `probe` until it answers something other than `undefined`, which it hands back, or answers `undefined` once `timeout` has passed. Measured on `performance.now()`, so `clock.at()`, which fakes `Date`, cannot freeze it. */
9
+ async function untilDeadline(probe, { interval = 0, timeout, wait }) {
10
+ const deadline = performance.now() + timeout;
11
+ for (;;) {
12
+ const answer = await probe();
13
+ if (answer !== void 0) return answer;
14
+ const remaining = deadline - performance.now();
15
+ if (remaining <= 0) return;
16
+ await (wait === void 0 ? pause(interval) : wait(remaining));
17
+ }
18
+ }
8
19
  //#endregion
9
- export { pause as t };
20
+ export { untilDeadline as n, pause as t };
10
21
 
11
22
  //# sourceMappingURL=shared-pause.js.map
@@ -1,10 +1,8 @@
1
1
  import { n as refusal } from "./shared-refusal.js";
2
- import { u as evidenceDirectory } from "./shared-artifacts.js";
2
+ import { n as untilDeadline } from "./shared-pause.js";
3
+ import { d as evidenceFile } from "./shared-artifacts.js";
3
4
  import { c as scopeChain } from "./shared-landmarks.js";
4
5
  import { n as CANDIDATE_LIMITS, o as ambiguousTargetError, r as candidatesOf, s as asWritten, t as scenarioFailure } from "./shared-scenario-failure.js";
5
- import { mkdtempSync } from "node:fs";
6
- import { resolve } from "node:path";
7
- import { setTimeout } from "node:timers/promises";
8
6
  //#region src/specifications/website/browser/page-scripts.ts
9
7
  function extractPage() {
10
8
  const links = [...document.querySelectorAll("link")].map((link) => ({
@@ -135,20 +133,21 @@ async function currentValue(locator) {
135
133
  return null;
136
134
  }
137
135
  }
138
- async function waitForValue(locator, target, wanted) {
136
+ async function waitForValue(locator, target, wanted, timeout = VALUE_TIMEOUT_MS) {
139
137
  const value = target.value ?? "";
140
- const deadline = Date.now() + VALUE_TIMEOUT_MS;
141
- for (;;) {
142
- const actual = await currentValue(locator);
143
- if (actual === null ? !wanted : actual === value === wanted) return;
144
- if (Date.now() >= deadline) throw new Error(refusal({
145
- fix: "correct the value the scenario states, or what the page puts in the field",
146
- found: `${asWritten(target)} ${wanted ? "never held" : "still holds"} ${JSON.stringify(value)}${actual === null ? " (it is not on the page)" : `, it holds ${JSON.stringify(actual)}`}`,
147
- read: "docs/reference/visitor.md#see-and-gone-are-the-synchronization",
148
- why: "A verb waits for what it states to hold, within its budget, and never sleeps."
149
- }));
150
- await setTimeout(VALUE_POLL_MS);
151
- }
138
+ let actual = null;
139
+ if (await untilDeadline(async () => {
140
+ actual = await currentValue(locator);
141
+ return actual === value === wanted || void 0;
142
+ }, {
143
+ interval: VALUE_POLL_MS,
144
+ timeout
145
+ }) === void 0) throw new Error(refusal({
146
+ fix: "correct the value the scenario states, or what the page puts in the field",
147
+ found: `${asWritten(target)} ${wanted ? "never held" : "still holds"} ${JSON.stringify(value)}${actual === null ? " (it is not on the page)" : `, it holds ${JSON.stringify(actual)}`}`,
148
+ read: "docs/reference/visitor.md#see-and-gone-are-the-synchronization",
149
+ why: "A verb waits for what it states to hold, within its budget, and never sleeps."
150
+ }));
152
151
  }
153
152
  async function settle(page, locator, target, present) {
154
153
  if (target.value !== void 0) {
@@ -312,8 +311,7 @@ var PlaywrightAdapter = class {
312
311
  /** Screenshot the failing state into the evidence folder; never masks the original error. */
313
312
  async captureEvidence(page) {
314
313
  try {
315
- const dir = mkdtempSync(evidenceDirectory("website-"));
316
- const path = resolve(dir, "failure.png");
314
+ const path = evidenceFile("website");
317
315
  await page.screenshot({
318
316
  fullPage: true,
319
317
  path
@@ -0,0 +1,13 @@
1
+ //#region src/chain/process-alive.ts
2
+ function processAlive(pid, probe = (target) => process.kill(target, 0)) {
3
+ try {
4
+ probe(pid);
5
+ return true;
6
+ } catch (error) {
7
+ return Reflect.get(new Object(error), "code") !== "ESRCH";
8
+ }
9
+ }
10
+ //#endregion
11
+ export { processAlive as t };
12
+
13
+ //# sourceMappingURL=shared-process-alive.js.map
@@ -0,0 +1,13 @@
1
+ //#region lint/kit/project/record-key.ts
2
+ /** `analyticsDb` gives `analytics-db`. */
3
+ function toKebabCase(key) {
4
+ return key.replaceAll(/(?<lower>[a-z0-9])(?<upper>[A-Z])/gu, "$<lower>-$<upper>").toLowerCase();
5
+ }
6
+ /** `analyticsDb` gives `ANALYTICS_DB`, `db-main` gives `DB_MAIN`. */
7
+ function toConstantCase(key) {
8
+ return key.replaceAll(/(?<lower>[a-z0-9])(?<upper>[A-Z])/gu, "$<lower>_$<upper>").replaceAll(/[^A-Za-z0-9]/gu, "_").toUpperCase();
9
+ }
10
+ //#endregion
11
+ export { toKebabCase as n, toConstantCase as t };
12
+
13
+ //# sourceMappingURL=shared-record-key.js.map