@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
@@ -8,4 +8,5 @@ type RunProject = {
8
8
  };
9
9
  export default function setup(project: RunProject): () => void;
10
10
  //#endregion
11
+ export {};
11
12
  //# sourceMappingURL=in-place.global-setup.d.ts.map
@@ -1,5 +1,5 @@
1
1
  import { i as writePendingRewrites } from "./shared-in-place.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 { t as setupEntry } from "./shared-setup-entry.js";
4
4
  import { join } from "node:path";
5
5
  import { randomUUID } from "node:crypto";
package/dist/index.d.ts CHANGED
@@ -514,9 +514,6 @@ interface ApiSpecification<DatabaseKey extends string = string> extends ChainOf<
514
514
  }, ApiSpecification<DatabaseKey>> {}
515
515
  //#endregion
516
516
  //#region src/specifications/api/api.handle.d.ts
517
- type ApiConfig = ServicesConfig & {
518
- server?: ServerPort | undefined;
519
- };
520
517
  /** Options for {@link startApi | specification.api}. */
521
518
  type ApiSpecificationOptions<Services extends ServiceRecord = ServiceRecord> = {
522
519
  /** Project root override; by default the nearest `package.json` above the specification file (`no-redundant-root`). */
@@ -1005,9 +1002,6 @@ type Job = {
1005
1002
  name: string;
1006
1003
  execute: () => Promise<void>;
1007
1004
  };
1008
- type JobConfig = ServicesConfig & {
1009
- jobs?: Job[] | undefined;
1010
- };
1011
1005
  /** Options for {@link startJob | specification.job}. */
1012
1006
  type JobSpecificationOptions<Services extends ServiceRecord = ServiceRecord> = {
1013
1007
  /** Named jobs `job.trigger(name)` runs: a factory of the started services, or an array. */
@@ -1239,10 +1233,6 @@ interface MobileSpecification extends ChainOf<typeof composition$2, {
1239
1233
  }, MobileSpecification> {}
1240
1234
  //#endregion
1241
1235
  //#region src/specifications/mobile/mobile.handle.d.ts
1242
- type MobileConfig = StubsConfig & {
1243
- bundleId?: string | undefined;
1244
- device?: (() => Promise<DevicePort>) | undefined;
1245
- };
1246
1236
  /** The stub backend behind the app; nothing is injected: wire `backendUrl` into your own bundler env. */
1247
1237
  type MobileBackendOptions = {
1248
1238
  /** Fixed port: Metro inlines `EXPO_PUBLIC_*` at serve time, so a stable port keeps a warm Metro valid. */
@@ -1562,9 +1552,6 @@ type WebsiteHandle = {
1562
1552
  declare function startWebsite<Services extends ServiceRecord>(options: WebsiteSpecificationOptions<Services>): Promise<WebsiteHandle>;
1563
1553
  //#endregion
1564
1554
  //#region src/specifications/specification.d.ts
1565
- /** Every specification's configuration, as one. */
1566
- type SpecificationConfig = ApiConfig & CliConfig & JobConfig & MobileConfig & WebsiteConfig;
1567
- /** Created in a `*.specification.ts` under `specs/`, destructured with canonical names, cleaned up via `afterAll(cleanup)` (`runner-in-specification-file`, `no-runner-alias`, `cleanup-passed-to-after-all`). */
1568
1555
  /** The seven constructors, and only seven: the framework's whole entry surface. */
1569
1556
  type Specification = {
1570
1557
  api: typeof startApi;
@@ -1575,6 +1562,7 @@ type Specification = {
1575
1562
  mobile: typeof startMobile;
1576
1563
  website: typeof startWebsite;
1577
1564
  };
1565
+ /** Created in a `*.specification.ts` under `specs/`, destructured with canonical names, cleaned up via `afterAll(cleanup)` (`runner-in-specification-file`, `no-runner-alias`, `cleanup-passed-to-after-all`). */
1578
1566
  export declare const specification: Specification;
1579
1567
  //#endregion
1580
1568
  //#region src/specifications/component/render/scenario.d.ts
@@ -1924,5 +1912,5 @@ declare module 'vitest' {
1924
1912
  }
1925
1913
  }
1926
1914
  //#endregion
1927
- 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 DeviceOpenOptions, type DevicePort, type DeviceScreen, type DeviceTimeouts, type DirectoryAccessor, type DiskDirectory, type DiskFile, type DiskTarget, type DockerSpecConfig, type DomMount, type ElementKind, type ElementMatch, type ExecOptions, type FetchResult, type FileAccessor, type FileAt, type FilesystemAccessor, type HonoApp, type HttpResult, type IntegrationHandle, type IntegrationSpecification, type IntegrationSpecificationOptions, type Job, type JobHandle, type JobResult, type JobSpecification, type JobSpecificationOptions, type JsonAccessor, type LandmarkKind, type LintConfig, type LintHandle, type LintResult, type LintRules, type LintRunOptions, type LintSpecification, type LintSpecificationOptions, type LiterateRunFlags, type LiterateServeRegistration, type MatchFixtureOptions, Matcher, type MatcherKind, type MobileBackendOptions, type MobileElementKind, type MobileElementMatch, type MobileHandle, type MobileScenario, type MobileSpecification, type MobileSpecificationOptions, type MobileVisitor, type MockOfOptions, type MockPort, type PackedPackage, type PageResult, type PinnedClock, type PositionedCases, type PositionedRuleTester, type PrepareStep, type RelativePath, type RenderResult, type RenderSubject, type RenderedText, type ResponseAccessor, type ScreenNode, type ScreenResult, type ServerPort, type ServerRequestOptions, type ServerResponse, type ServerSpec, type SpecDocument, type SpecEnvToken, type SpecFileAssertion, type SpecFixture, type SpecRun, type SpecServeEntry, type SpecStream, type SpecificationConfig, type TableAccessor, type Target, type TargetOptions, type TextAccessor, type VisitScenario, type WaitUntilOptions, type WebsiteBackendOptions, type WebsiteHandle, type WebsiteSpecification, type WebsiteSpecificationOptions, type WebsiteVisitor, directory, file, match };
1915
+ 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 DeviceOpenOptions, type DevicePort, type DeviceScreen, type DeviceTimeouts, type DirectoryAccessor, type DiskDirectory, type DiskFile, type DiskTarget, type DockerSpecConfig, type DomMount, type ElementKind, type ElementMatch, type ExecOptions, type FetchResult, type FileAccessor, type FileAt, type FilesystemAccessor, type HonoApp, type HttpResult, type IntegrationHandle, type IntegrationSpecification, type IntegrationSpecificationOptions, type Job, type JobHandle, type JobResult, type JobSpecification, type JobSpecificationOptions, type JsonAccessor, type LandmarkKind, type LintConfig, type LintHandle, type LintResult, type LintRules, type LintRunOptions, type LintSpecification, type LintSpecificationOptions, type LiterateRunFlags, type LiterateServeRegistration, type MatchFixtureOptions, Matcher, type MatcherKind, type MobileBackendOptions, type MobileElementKind, type MobileElementMatch, type MobileHandle, type MobileScenario, type MobileSpecification, type MobileSpecificationOptions, type MobileVisitor, type MockOfOptions, type MockPort, type PackedPackage, type PageResult, type PinnedClock, type PositionedCases, type PositionedRuleTester, type PrepareStep, type RelativePath, type RenderResult, type RenderSubject, type RenderedText, type ResponseAccessor, type ScreenNode, type ScreenResult, type ServerPort, type ServerRequestOptions, type ServerResponse, type ServerSpec, type SpecDocument, type SpecEnvToken, type SpecFileAssertion, type SpecFixture, type SpecRun, type SpecServeEntry, type SpecStream, type TableAccessor, type Target, type TargetOptions, type TextAccessor, type VisitScenario, type WaitUntilOptions, type WebsiteBackendOptions, type WebsiteHandle, type WebsiteSpecification, type WebsiteSpecificationOptions, type WebsiteVisitor, directory, file, match };
1928
1916
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -1,16 +1,19 @@
1
- import { a as signalGroup, i as SIGNALS_GROUPS, r as KILL_GRACE_MS, t as ServedProcess } from "./shared-serve.js";
1
+ import { a as endGroup, i as SIGNALS_GROUPS, o as signalGroup, r as KILL_GRACE_MS, t as ServedProcess } from "./shared-serve.js";
2
2
  import { i as safeRealpath, r as resolveRoot } from "./shared-resolve.js";
3
- import { n as deferRewritesTo, o as getCallerDir, r as writeInPlace, t as callSiteOf } from "./shared-in-place.js";
3
+ import { a as getCallerDir, n as deferRewritesTo, r as writeInPlace, t as callSiteOf } from "./shared-in-place.js";
4
4
  import { t as isRecord } from "./shared-json-shape.js";
5
5
  import { n as refusal$1, t as invariant } from "./shared-refusal.js";
6
+ import { a as field$1, c as containerLogs, d as findContainersByLabel, f as inspectContainer, h as requireDockerDaemon, l as execInContainer, m as removeContainers, p as readContainerFile, r as TestcontainersAdapter, u as fieldOf } from "./shared-testcontainers.adapter.js";
6
7
  import { _ as Matcher, d as structuralEquals, g as placeholderStringMatches, h as hasPlaceholders, l as isPlainObject, m as textEquals, n as isStreamBody, p as textContains, r as declared, u as renderExpected, v as match, y as CaptureScope } from "./shared-exchange.js";
7
- import { i as readSpecFile, l as SpecSyntaxError, s as RUN_KEYS, u as dedent } from "./shared-spec-document.js";
8
+ import { a as RUN_KEYS, s as SpecSyntaxError } from "./shared-spec-grammar.js";
8
9
  import { a as renderValue, c as file, d as isDiskTarget, f as requireDiskFile, i as renderText, l as isDiskDirectory, o as resolveSpecPath, p as requireDiskTarget, r as renderRows, s as directory, t as grammarRefusal, u as isDiskFile } from "./shared-literate-plugin.js";
9
10
  import { c as writeLedger, l as missingHint, o as routesLedger, u as shouldUpdateSnapshots } from "./shared-ledger.js";
10
- import { t as pause } from "./shared-pause.js";
11
+ import { i as readSpecFile, o as dedent } from "./shared-spec-document.js";
12
+ import { n as untilDeadline } from "./shared-pause.js";
13
+ import { t as listenedPort } from "./shared-listened-port.js";
11
14
  import { t as ProcessHandle } from "./shared-process.adapter.js";
12
- import { a as field$1, r as TestcontainersAdapter } from "./shared-testcontainers.adapter.js";
13
- import { n as StubQueue, t as toReadableStream } from "./shared-stream-body.js";
15
+ import { n as toKebabCase, t as toConstantCase } from "./shared-record-key.js";
16
+ import { a as toReadableStream, i as unmatchedSummary, n as replyOf, o as StubQueue, r as requestBody, t as refusedReply } from "./shared-stub-reply.js";
14
17
  import { d as renderYamlSource, n as blockScalar, o as isScalar, r as isMap, s as isSeq } from "./shared-yaml-document.js";
15
18
  import { a as sessionLedger, i as mobileSessions, o as mobileServer, s as reachAppium } from "./shared-session-ledger.js";
16
19
  import { S as testId, _ as listitem, a as main, b as status, d as button, f as content, g as link, h as heading, i as form, l as search, m as field, n as complementary, o as navigation, p as dialog, r as contentinfo, s as region, t as banner, u as within, v as option, x as table, y as row } from "./shared-landmarks.js";
@@ -18,8 +21,8 @@ import { createRequire } from "node:module";
18
21
  import { LESSON_PARTS, lessonParts, presetChecks } from "@jterrazz/typescript/rules";
19
22
  import { cpSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs";
20
23
  import { basename, dirname, isAbsolute, join, normalize, relative, resolve, sep } from "node:path";
24
+ import { execFileSync, spawn, spawnSync } from "node:child_process";
21
25
  import { readdir } from "node:fs/promises";
22
- import { execFileSync, execSync, spawn, spawnSync } from "node:child_process";
23
26
  import { randomUUID } from "node:crypto";
24
27
  import { createServer } from "node:http";
25
28
  import { tmpdir } from "node:os";
@@ -177,7 +180,7 @@ var TableAccessor = class {
177
180
  const ANSI_RE = /\u001B\[[0-9;]*m/gu;
178
181
  /** rule `no-transform-duplicating-tokens`: a golden compares the text, never its colour. */
179
182
  function stripAnsiCodes(value) {
180
- return value.replace(ANSI_RE, "");
183
+ return value.replaceAll(ANSI_RE, "");
181
184
  }
182
185
  //#endregion
183
186
  //#region src/expectations/text/grep.ts
@@ -338,110 +341,6 @@ var JsonAccessor = class {
338
341
  }
339
342
  };
340
343
  //#endregion
341
- //#region src/expectations/command/docker-cli.ts
342
- const DEFAULT_TIMEOUT = 1e4;
343
- /** Docker-aware mode reads what the daemon answers: a run where none answers is refused, never passed. */
344
- function requireDockerDaemon() {
345
- try {
346
- execSync("docker version --format \"{{.Server.Version}}\"", {
347
- stdio: "ignore",
348
- timeout: DEFAULT_TIMEOUT
349
- });
350
- } catch (error) {
351
- throw new Error(refusal$1({
352
- fix: "start Docker (Docker Desktop, OrbStack, colima or another) and run again",
353
- found: "specification.cli({ docker }) asked for Docker, and no Docker daemon answered",
354
- read: "docs/guides/cli-specs.md#docker-aware-mode",
355
- why: "Docker-aware mode reads the containers a run spawned, so it needs a Docker daemon."
356
- }), { cause: error });
357
- }
358
- }
359
- function findContainersByLabel(key, value) {
360
- try {
361
- return execSync(`docker ps -aq -f label=${key}=${value}`, {
362
- encoding: "utf8",
363
- timeout: DEFAULT_TIMEOUT
364
- }).split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
365
- } catch {
366
- return [];
367
- }
368
- }
369
- /** A field of a value read off docker's JSON or a failed process: absent wherever the value does not hold it. */
370
- function fieldOf(value, ...keys) {
371
- if (typeof value !== "object" || value === null) return;
372
- const key = keys.find((candidate) => candidate in value);
373
- return key === void 0 ? void 0 : Reflect.get(value, key);
374
- }
375
- const streamOf = (error, key) => {
376
- const stream = fieldOf(error, key);
377
- return typeof stream === "string" ? stream : void 0;
378
- };
379
- function inspectContainer(id) {
380
- const raw = execSync(`docker inspect ${id}`, {
381
- encoding: "utf8",
382
- timeout: DEFAULT_TIMEOUT
383
- });
384
- const parsed = JSON.parse(raw);
385
- if (!Array.isArray(parsed)) return parsed;
386
- return parsed[0];
387
- }
388
- function removeContainers(ids) {
389
- if (ids.length === 0) return;
390
- try {
391
- execSync(`docker rm -f ${ids.join(" ")}`, {
392
- encoding: "utf8",
393
- stdio: "pipe",
394
- timeout: DEFAULT_TIMEOUT
395
- });
396
- } catch {}
397
- }
398
- function containerLogs(id) {
399
- try {
400
- return execSync(`docker logs ${id}`, {
401
- encoding: "utf8",
402
- stdio: [
403
- "ignore",
404
- "pipe",
405
- "pipe"
406
- ],
407
- timeout: DEFAULT_TIMEOUT
408
- });
409
- } catch (error) {
410
- return streamOf(error, "stdout") ?? streamOf(error, "stderr") ?? "";
411
- }
412
- }
413
- function readContainerFile(id, path) {
414
- return execSync(`docker exec ${id} cat ${JSON.stringify(path)}`, {
415
- encoding: "utf8",
416
- timeout: DEFAULT_TIMEOUT
417
- });
418
- }
419
- function execInContainer(id, argv) {
420
- try {
421
- const quoted = argv.map((a) => JSON.stringify(a)).join(" ");
422
- return {
423
- exitCode: 0,
424
- stderr: "",
425
- stdout: execSync(`docker exec ${id} ${quoted}`, {
426
- encoding: "utf8",
427
- stdio: [
428
- "ignore",
429
- "pipe",
430
- "pipe"
431
- ],
432
- timeout: DEFAULT_TIMEOUT
433
- })
434
- };
435
- } catch (error) {
436
- const status = fieldOf(error, "status");
437
- return {
438
- exitCode: typeof status === "number" ? status : 1,
439
- stderr: streamOf(error, "stderr") ?? (error instanceof Error ? error.message : ""),
440
- stdout: streamOf(error, "stdout") ?? ""
441
- };
442
- }
443
- }
444
- //#endregion
445
344
  //#region src/expectations/command/container.ts
446
345
  function readInspectState(inspect) {
447
346
  const state = fieldOf(inspect, "State", "state");
@@ -1693,16 +1592,6 @@ function declaredDatabaseKeys(services) {
1693
1592
  return Object.entries(services).filter(([, service]) => service.createDatabaseAdapter() !== null).map(([key]) => key);
1694
1593
  }
1695
1594
  //#endregion
1696
- //#region lint/kit/project/record-key.ts
1697
- /** `analyticsDb` gives `analytics-db`. */
1698
- function toKebabCase(key) {
1699
- return key.replaceAll(/(?<lower>[a-z0-9])(?<upper>[A-Z])/gu, "$<lower>-$<upper>").toLowerCase();
1700
- }
1701
- /** `analyticsDb` gives `ANALYTICS_DB`, `db-main` gives `DB_MAIN`. */
1702
- function toConstantCase(key) {
1703
- return key.replaceAll(/(?<lower>[a-z0-9])(?<upper>[A-Z])/gu, "$<lower>_$<upper>").replaceAll(/[^A-Za-z0-9]/gu, "_").toUpperCase();
1704
- }
1705
- //#endregion
1706
1595
  //#region src/services/containers/run-setup-container.adapter.ts
1707
1596
  function answered(value) {
1708
1597
  const error = field$1(value, "error");
@@ -1970,12 +1859,12 @@ var Orchestrator = class {
1970
1859
  function getWorkerId() {
1971
1860
  return process.env.VITEST_POOL_ID ?? "0";
1972
1861
  }
1973
- async function acquireIsolation(services) {
1862
+ async function acquireIsolation(services, acquired) {
1974
1863
  const workerId = getWorkerId();
1975
- for (const service of Object.values(services)) await service.isolation().acquire(workerId);
1976
- }
1977
- async function releaseIsolation(services) {
1978
- for (const service of Object.values(services)) await service.isolation().release();
1864
+ for (const service of Object.values(services)) {
1865
+ await service.isolation().acquire(workerId);
1866
+ acquired.push(service);
1867
+ }
1979
1868
  }
1980
1869
  function splitProcesses(services) {
1981
1870
  const infrastructure = {};
@@ -2012,16 +1901,30 @@ async function startServices(services, root, subject) {
2012
1901
  subject
2013
1902
  });
2014
1903
  await orchestrator.start();
2015
- await acquireIsolation(infrastructure);
1904
+ const acquired = [];
1905
+ const stopInfrastructure = async () => {
1906
+ for (const service of acquired.splice(0)) await service.isolation().release();
1907
+ await orchestrator.stop();
1908
+ };
2016
1909
  const runId = randomUUID();
2017
- const stopProcesses = await startProcessServices(services, root, runId);
1910
+ let stopProcesses;
1911
+ try {
1912
+ await acquireIsolation(infrastructure, acquired);
1913
+ stopProcesses = await startProcessServices(services, root, runId);
1914
+ } catch (error) {
1915
+ await stopInfrastructure();
1916
+ throw error;
1917
+ }
2018
1918
  const databases = orchestrator.getDatabases();
2019
1919
  return {
2020
1920
  database: orchestrator.getDatabase() ?? void 0,
2021
1921
  databases: databases.size > 0 ? databases : void 0,
2022
1922
  orchestrator,
2023
1923
  runId,
2024
- stopProcesses
1924
+ stop: async () => {
1925
+ await stopProcesses();
1926
+ await stopInfrastructure();
1927
+ }
2025
1928
  };
2026
1929
  }
2027
1930
  //#endregion
@@ -2656,7 +2559,7 @@ async function startApi(options) {
2656
2559
  const root = resolveRoot(options.root, callerDir);
2657
2560
  const services = declaredServices(options.services);
2658
2561
  const databaseKeys = declaredDatabaseKeys(services);
2659
- const { database, databases, orchestrator, stopProcesses } = await startServices(services, root);
2562
+ const { database, databases, stop } = await startServices(services, root);
2660
2563
  const server = new HonoAdapter(options.server(services));
2661
2564
  const config = {
2662
2565
  database,
@@ -2672,9 +2575,7 @@ async function startApi(options) {
2672
2575
  declared: server.declared(),
2673
2576
  reached: server.reached
2674
2577
  });
2675
- await stopProcesses();
2676
- await releaseIsolation(services);
2677
- await orchestrator.stop();
2578
+ await stop();
2678
2579
  },
2679
2580
  docker: createDockerReader(callerDir)
2680
2581
  };
@@ -2699,15 +2600,10 @@ var StubBackend = class {
2699
2600
  this.handle(request, response);
2700
2601
  });
2701
2602
  this.server = server;
2702
- await new Promise((resolve, reject) => {
2703
- server.once("error", reject);
2704
- server.listen(this.options.port ?? 0, "127.0.0.1", () => {
2705
- resolve();
2706
- });
2707
- });
2708
- const address = server.address();
2709
- if (address === null || typeof address === "string") throw invariant("the stub backend listens on no port");
2710
- return `http://127.0.0.1:${address.port}`;
2603
+ return `http://127.0.0.1:${await listenedPort(server, {
2604
+ host: "127.0.0.1",
2605
+ port: this.options.port ?? 0
2606
+ })}`;
2711
2607
  }
2712
2608
  async stop() {
2713
2609
  const { server } = this;
@@ -2765,18 +2661,18 @@ var StubBackend = class {
2765
2661
  const stub = this.queue.take(observed);
2766
2662
  if (!stub) {
2767
2663
  this.record(method, url);
2664
+ const error = refusedReply(unmatchedSummary(method, url));
2768
2665
  response.writeHead(501, {
2769
2666
  ...cors,
2770
2667
  "content-type": "application/json"
2771
2668
  });
2772
2669
  response.end(JSON.stringify({
2773
2670
  declared: this.queue.declaredRoutes(),
2774
- error: `@jterrazz/test declared backend: no stub matches ${method} ${url}`
2671
+ error
2775
2672
  }));
2776
2673
  return;
2777
2674
  }
2778
- const reply = typeof stub.response === "function" ? stub.response(observed) : stub.response;
2779
- await pause(reply.delay ?? 0);
2675
+ const reply = await replyOf(stub.response, observed);
2780
2676
  if (reply.transport === "network-error") {
2781
2677
  request.socket.destroy();
2782
2678
  return;
@@ -2835,13 +2731,7 @@ async function writeReply(response, reply, cors) {
2835
2731
  async function readBody(request) {
2836
2732
  const chunks = [];
2837
2733
  for await (const chunk of request) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
2838
- const raw = Buffer.concat(chunks).toString("utf8");
2839
- if (raw.length === 0) return null;
2840
- try {
2841
- return JSON.parse(raw);
2842
- } catch {
2843
- return raw;
2844
- }
2734
+ return requestBody(Buffer.concat(chunks).toString("utf8"));
2845
2735
  }
2846
2736
  function corsHeaders(request) {
2847
2737
  return {
@@ -3903,12 +3793,6 @@ async function observeProcess(child, options) {
3903
3793
  });
3904
3794
  });
3905
3795
  }
3906
- function endGroup(child) {
3907
- signalGroup(child);
3908
- setTimeout(() => {
3909
- signalGroup(child, "SIGKILL");
3910
- }, KILL_GRACE_MS).unref();
3911
- }
3912
3796
  /** Spawned, never `spawnSync`: the test's event loop keeps running while the binary does, so a server the test started (a collector, a stub) answers it. stdout and stderr are captured whatever the exit code, and many CLIs write banners to stderr. */
3913
3797
  var ExecAdapter = class {
3914
3798
  command;
@@ -3923,8 +3807,8 @@ var ExecAdapter = class {
3923
3807
  env,
3924
3808
  shell: true
3925
3809
  });
3926
- onTestFinished(() => {
3927
- endGroup(child);
3810
+ onTestFinished(async () => {
3811
+ await endGroup(child);
3928
3812
  });
3929
3813
  return await new Promise((resolve) => {
3930
3814
  const output = {
@@ -3980,7 +3864,7 @@ var ExecAdapter = class {
3980
3864
  function dockerTestRunId() {
3981
3865
  return `t-${Math.random().toString(36).slice(2, 10)}-${Date.now().toString(36)}`;
3982
3866
  }
3983
- async function startedBackend(declared, defaults, started) {
3867
+ async function startedBackend$1(declared, defaults, started) {
3984
3868
  if (declared === void 0) return {
3985
3869
  backend: null,
3986
3870
  defaults
@@ -3995,7 +3879,7 @@ async function startedBackend(declared, defaults, started) {
3995
3879
  }
3996
3880
  };
3997
3881
  } catch (error) {
3998
- await started?.orchestrator.stop();
3882
+ await started?.stop();
3999
3883
  throw error;
4000
3884
  }
4001
3885
  }
@@ -4009,7 +3893,7 @@ async function startCli(bin, options = {}) {
4009
3893
  if (options.docker !== void 0) requireDockerDaemon();
4010
3894
  let started = null;
4011
3895
  if (Object.keys(services).length > 0) started = await startServices(services, root, { type: "none" });
4012
- const { backend, defaults } = await startedBackend(options.backend, options.defaults, started);
3896
+ const { backend, defaults } = await startedBackend$1(options.backend, options.defaults, started);
4013
3897
  const dockerTestRunIdOf = options.docker === void 0 ? void 0 : dockerTestRunId();
4014
3898
  const declaredServicesOf = Object.keys(services).length > 0 ? services : void 0;
4015
3899
  const config = {
@@ -4029,10 +3913,8 @@ async function startCli(bin, options = {}) {
4029
3913
  };
4030
3914
  return {
4031
3915
  cleanup: async () => {
3916
+ await started?.stop();
4032
3917
  await backend?.stop();
4033
- await started?.stopProcesses();
4034
- await releaseIsolation(services);
4035
- if (started) await started.orchestrator.stop();
4036
3918
  },
4037
3919
  cli: cliDefinition.open(config),
4038
3920
  docker: createDockerReader(callerDir),
@@ -4178,9 +4060,7 @@ async function startIntegration(options = {}) {
4178
4060
  };
4179
4061
  return {
4180
4062
  cleanup: async () => {
4181
- await started?.stopProcesses();
4182
- await releaseIsolation(services);
4183
- if (started) await started.orchestrator.stop();
4063
+ await started?.stop();
4184
4064
  },
4185
4065
  integration: integrationDefinition.open(config)
4186
4066
  };
@@ -4278,9 +4158,7 @@ async function startJob(options) {
4278
4158
  };
4279
4159
  return {
4280
4160
  cleanup: async () => {
4281
- await started?.stopProcesses();
4282
- await releaseIsolation(services);
4283
- if (started) await started.orchestrator.stop();
4161
+ await started?.stop();
4284
4162
  },
4285
4163
  job: jobDefinition.open(config),
4286
4164
  orchestrator: started?.orchestrator ?? null
@@ -4956,19 +4834,30 @@ async function startBesideAppium(services, root, appium) {
4956
4834
  throw error;
4957
4835
  }
4958
4836
  }
4837
+ async function startedBackend(declared, started, appium) {
4838
+ if (!declared) return {
4839
+ backend: null,
4840
+ backendUrl: void 0
4841
+ };
4842
+ const backend = new StubBackend({ port: declared.port });
4843
+ try {
4844
+ return {
4845
+ backend,
4846
+ backendUrl: await backend.start()
4847
+ };
4848
+ } catch (error) {
4849
+ await started?.stop();
4850
+ await appium.stop();
4851
+ throw error;
4852
+ }
4853
+ }
4959
4854
  async function startMobile(options) {
4960
4855
  const callerDir = getCallerDir();
4961
4856
  await registerMatchers();
4962
4857
  const root = resolveRoot(options.root, callerDir);
4963
4858
  const { appium, device: driven, outlivesFile } = await reachAppium(options, root, await mobileServer());
4964
- const services = declaredServices(options.services);
4965
- const started = await startBesideAppium(services, root, appium);
4966
- let backend = null;
4967
- let backendUrl;
4968
- if (options.backend) {
4969
- backend = new StubBackend({ port: options.backend.port });
4970
- backendUrl = await backend.start();
4971
- }
4859
+ const started = await startBesideAppium(declaredServices(options.services), root, appium);
4860
+ const { backend, backendUrl } = await startedBackend(options.backend, started, appium);
4972
4861
  const device = lazyDevice({
4973
4862
  driven,
4974
4863
  ledger: async () => await runLedger(outlivesFile),
@@ -4985,11 +4874,7 @@ async function startMobile(options) {
4985
4874
  backendUrl,
4986
4875
  cleanup: async () => {
4987
4876
  await device.close();
4988
- if (started) {
4989
- await started.stopProcesses();
4990
- await releaseIsolation(services);
4991
- await started.orchestrator.stop();
4992
- }
4877
+ await started?.stop();
4993
4878
  await appium.stop();
4994
4879
  if (backend) await backend.stop();
4995
4880
  },
@@ -5449,8 +5334,7 @@ async function startSite(options, context) {
5449
5334
  serve
5450
5335
  };
5451
5336
  } catch (error) {
5452
- await context.started?.stopProcesses();
5453
- await context.started?.orchestrator.stop();
5337
+ await context.started?.stop();
5454
5338
  await context.backend?.stop();
5455
5339
  throw error;
5456
5340
  }
@@ -5533,11 +5417,7 @@ async function startWebsite(options) {
5533
5417
  cleanup: async () => {
5534
5418
  await browser.close();
5535
5419
  if (serve) await serve.stop();
5536
- if (started) {
5537
- await started.stopProcesses();
5538
- await releaseIsolation(services);
5539
- await started.orchestrator.stop();
5540
- }
5420
+ await started?.stop();
5541
5421
  if (backend) await backend.stop();
5542
5422
  },
5543
5423
  url: baseUrl,
@@ -5546,6 +5426,7 @@ async function startWebsite(options) {
5546
5426
  }
5547
5427
  //#endregion
5548
5428
  //#region src/specifications/specification.ts
5429
+ /** Created in a `*.specification.ts` under `specs/`, destructured with canonical names, cleaned up via `afterAll(cleanup)` (`runner-in-specification-file`, `no-runner-alias`, `cleanup-passed-to-after-all`). */
5549
5430
  const specification = {
5550
5431
  /** Test an HTTP app: services via testcontainers, the app in this process. */
5551
5432
  api: startApi,
@@ -5676,17 +5557,15 @@ const DEFAULT_INTERVAL_MS = 50;
5676
5557
  async function waitUntil(predicate, options = {}) {
5677
5558
  const timeout = options.timeout ?? DEFAULT_TIMEOUT_MS;
5678
5559
  const interval = options.interval ?? DEFAULT_INTERVAL_MS;
5679
- const deadline = performance.now() + timeout;
5680
- for (;;) {
5681
- if (await predicate()) return;
5682
- if (performance.now() >= deadline) throw new Error(refusal$1({
5683
- fix: "raise the budget if the wait is legitimate, or wait on what actually changes",
5684
- found: `waitUntil(): ${options.why ?? "the condition"} did not hold within ${timeout}ms`,
5685
- read: "docs/reference/assertions.md#two-helpers-that-are-not-matchers",
5686
- why: "`waitUntil()` waits on a condition, within a budget."
5687
- }));
5688
- await pause(interval);
5689
- }
5560
+ if (await untilDeadline(async () => await predicate() || void 0, {
5561
+ interval,
5562
+ timeout
5563
+ }) === void 0) throw new Error(refusal$1({
5564
+ fix: "raise the budget if the wait is legitimate, or wait on what actually changes",
5565
+ found: `waitUntil(): ${options.why ?? "the condition"} did not hold within ${timeout}ms`,
5566
+ read: "docs/reference/assertions.md#two-helpers-that-are-not-matchers",
5567
+ why: "`waitUntil()` waits on a condition, within a budget."
5568
+ }));
5690
5569
  }
5691
5570
  //#endregion
5692
5571
  export { Matcher, banner, button, clock, complementary, component, content, contentinfo, dialog, directory, disabled, enabled, field, file, focused, form, heading, link, listitem, main, match, mockOf, navigation, option, packed, region, required, row, ruleTester, search, selected, specification, status, table, testId, text, valued, waitUntil, within };
@@ -11,4 +11,5 @@ export declare function mobileSessionSetup(): string;
11
11
  /** One driver session per run: the first runner records it in the provided folder, later files attach, the teardown ends them. With `mobile({ device, root })` it also owns the Appium server. */
12
12
  export default function setup(project: SessionProject): Promise<() => Promise<void>>;
13
13
  //#endregion
14
+ export {};
14
15
  //# sourceMappingURL=mobile-session.global-setup.d.ts.map
package/dist/oxlint.cjs CHANGED
@@ -5628,14 +5628,14 @@ function literalOf(property) {
5628
5628
  value
5629
5629
  };
5630
5630
  }
5631
- /** Only under `test` or in `projects`, listed or keyed: `optimizeDeps.include` and its kin list package specifiers, not globs over a tree. */
5631
+ /** Only under `test` or in `projects`: `optimizeDeps.include` and its kin list package specifiers, not globs over a tree. */
5632
5632
  function includedGlobs(program) {
5633
5633
  const globs = [];
5634
5634
  walk(program, (node) => {
5635
5635
  const key = propertyKeyName(node);
5636
5636
  if (key !== "projects" && key !== "test") return;
5637
5637
  const value = child(node, "value");
5638
- const projects = key === "test" ? [value] : [...childList(value, "elements"), ...childList(value, "properties").map((property) => child(property, "value"))];
5638
+ const projects = key === "test" ? [value] : childList(value, "properties").map((property) => child(property, "value"));
5639
5639
  for (const project of projects) {
5640
5640
  const include = child(findProperty(project, "include"), "value");
5641
5641
  for (const element of childList(include, "elements")) {