@jterrazz/test 15.2.0 → 15.3.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/dist/intercept.js CHANGED
@@ -1,4 +1,4 @@
1
- import { t as ContractQueue } from "./queue.js";
1
+ import { a as isStreamBody, n as ContractQueue, t as toReadableStream } from "./stream.js";
2
2
  //#region src/integrations/msw/handlers.ts
3
3
  /**
4
4
  * What a chain with zero contracts gets where the facet's scope is KNOWN but
@@ -31,6 +31,14 @@ function toMswResponse(msw, response) {
31
31
  },
32
32
  status
33
33
  });
34
+ if (isStreamBody(body)) return new msw.HttpResponse(toReadableStream(body), {
35
+ headers: {
36
+ ...NEVER_CACHED,
37
+ "content-type": body.contentType,
38
+ ...headers
39
+ },
40
+ status
41
+ });
34
42
  if (typeof body === "string") return new msw.HttpResponse(body, {
35
43
  headers: {
36
44
  ...NEVER_CACHED,
package/dist/oxlint.cjs CHANGED
@@ -284,7 +284,7 @@ const RULE_DOCS = {
284
284
  },
285
285
  "a2-known-constructors": {
286
286
  channel: "statique",
287
- convention: "Cinq constructeurs et seulement cinq : `specification.api()`, `specification.jobs()`, `specification.cli(bin)`, `specification.website()`, `specification.mobile()` ; tout autre membre (`.app`, `.http`, `.stack`…) est une erreur.",
287
+ convention: "Six constructeurs et seulement six : `specification.api()`, `specification.jobs()`, `specification.cli(bin)`, `specification.integration()`, `specification.website()`, `specification.mobile()` ; tout autre membre (`.app`, `.http`, `.stack`…) est une erreur. Un composant rendu ne démarre rien : c’est une chaîne, pas un membre de ce record.",
288
288
  family: "A",
289
289
  id: "A2",
290
290
  rationale: "Une surface fermée empêche l’invention de constructeurs parallèles et garde l’API mémorisable."
@@ -531,7 +531,7 @@ const RULE_DOCS = {
531
531
  },
532
532
  "f3-specs-public-entry": {
533
533
  channel: "statique",
534
- convention: "Depuis `specs/`, seul l’import en profondeur des INTERNES du framework est interdit : un chemin relatif résolvant dans `src/{specification,integrations,vitest,lint}/` du dépôt du framework, ou tout `@jterrazz/test/<subpath>` que la map `exports` du paquet ne publie pas — les subpaths publiés sont exemptés, LUS du manifeste comme en F1, qui en tient la liste. Les imports de la source de SA PROPRE app par un consommateur sont toujours permis (c’est le motif documenté) ; seul `specs/integrations/` peut importer en profondeur `src/integrations/**`.",
534
+ convention: "Depuis `specs/`, seul l’import en profondeur des INTERNES du framework est interdit : un chemin relatif résolvant dans `src/{specification,integrations,vitest,lint}/` du dépôt du framework, ou tout `@jterrazz/test/<subpath>` que la map `exports` du paquet ne publie pas — les subpaths publiés sont exemptés, LUS du manifeste comme en F1, qui en tient la liste. Les imports de la source de SA PROPRE app par un consommateur sont toujours permis (c’est le motif documenté). Aucune exception de dossier : une sonde qui ne peut pas atteindre son sujet par l’entrée publique est un test de module à côté de son module, pas une spec.",
535
535
  family: "F",
536
536
  id: "F3",
537
537
  rationale: "Tester par la surface publique garde les specs découplées des chemins internes du framework, sans gêner le consommateur qui importe sa propre app."
@@ -623,6 +623,39 @@ const RULE_DOCS = {
623
623
  * visits data fixtures nor reads two files at once, so these ship separately.
624
624
  */
625
625
  const CHECKER_PASSES = [
626
+ {
627
+ channel: "checker",
628
+ convention: "Chaque membre du workspace qui a des tests À LUI (un script `test` qui ne délègue pas aux membres, ou un `*.test.ts(x)` hors de l’arbre d’un autre paquet) déclare une config vitest (`vitest.config.*`). La passe MEMBRE juge le membre lui-même, avec ou sans arbre `specs/` ; une racine qui ne fait que déléguer ne doit rien.",
629
+ facet: "shared",
630
+ family: "E",
631
+ fix: "Écrire `export default defineSpecConfig()` dans le membre — le préréglage porte les budgets, le dossier d’artefacts et l’exclusion de `_fixtures/`.",
632
+ id: "E3",
633
+ name: "e3-config-present",
634
+ rationale: "Sans config, le budget de 5 s de vitest et un `_fixtures/` non exclu font tourner les tests du membre : le défaut n’est pas choisi, il est hérité.",
635
+ reach: "config"
636
+ },
637
+ {
638
+ channel: "checker",
639
+ convention: "La passe MEMBRE relit la règle E5b là où oxlint peut ne pas atteindre la config d’un membre : `environment: 'happy-dom'|'jsdom'` dans un `vitest.config.*` est une erreur.",
640
+ facet: "component",
641
+ family: "E",
642
+ fix: "Supprimer `environment` et collecter le rendu avec `component()`.",
643
+ id: "E5b",
644
+ name: "e5b-no-simulated-dom-config-member",
645
+ rationale: "La passe statique ne voit que ce que la configuration oxlint du dépôt lui donne ; la passe membre part du manifeste racine et atteint chaque membre déclaré.",
646
+ reach: "config"
647
+ },
648
+ {
649
+ channel: "checker",
650
+ convention: "Un membre ne déclare aucune dépendance que `@jterrazz/test` porte déjà (`msw`, `vitest-mock-extended` ; `yaml` en devDependencies seulement — en dépendance de production c’est la bibliothèque du produit) ni aucun seam retiré (`happy-dom`, `jsdom`, `@testing-library/*`, `@playwright/test`, `mockdate`). Les peers optionnels — `playwright`, `vite`, `react`, `better-sqlite3`, `pg`, `redis`, `testcontainers`… — sont déclarés par le consommateur, à dessein.",
651
+ facet: "shared",
652
+ family: "F",
653
+ fix: "Retirer la déclaration : une transitive est déjà résolue par le paquet ; un seam retiré a une facette qui le remplace (`component()`, `website()`, `clock`, `intercept()`). Les deux cas ont leur propre phrase dans le diagnostic.",
654
+ id: "F8",
655
+ name: "f8-no-seam-dependency",
656
+ rationale: "Une déclaration en double laisse deux versions d’un même seam coexister, et un seam retiré garde un vocabulaire que le framework a remplacé.",
657
+ reach: "config"
658
+ },
626
659
  {
627
660
  channel: "checker",
628
661
  convention: "Tout `{{token}}` dans une fixture `_expected/` — ou dans un flux attendu d’un document `<cas>.spec.yaml` — appartient au vocabulaire figé ; un token inconnu est une erreur.",
@@ -1023,19 +1056,22 @@ const a1SpecificationFile = {
1023
1056
  };
1024
1057
  //#endregion
1025
1058
  //#region src/lint/rules/a2-known-constructors.ts
1026
- /** The five constructors, and only five (A2 — see docs/13-linting.md). */
1059
+ /** The six constructors, and only six (A2 — see docs/13-linting.md). */
1027
1060
  const KNOWN_CONSTRUCTORS = /* @__PURE__ */ new Set([
1028
1061
  "api",
1029
1062
  "cli",
1063
+ "integration",
1030
1064
  "jobs",
1031
1065
  "mobile",
1032
1066
  "website"
1033
1067
  ]);
1034
1068
  /**
1035
1069
  * CONVENTIONS A2 — `specification.api()`, `specification.jobs()`,
1036
- * `specification.cli()`, `specification.website()` and
1037
- * `specification.mobile()` are the only members. Any other access
1038
- * (`specification.app`, `.http`, `.stack`, …) is flagged at the member site.
1070
+ * `specification.cli()`, `specification.integration()`,
1071
+ * `specification.website()` and `specification.mobile()` are the only
1072
+ * members. Any other access (`specification.app`, `.http`, `.stack`, …) is
1073
+ * flagged at the member site. A rendered component has no constructor: it
1074
+ * starts nothing, so it is a chain, not a member of this record.
1039
1075
  */
1040
1076
  const a2KnownConstructors = {
1041
1077
  create(context) {
@@ -1052,7 +1088,7 @@ const a2KnownConstructors = {
1052
1088
  },
1053
1089
  meta: {
1054
1090
  docs: RULE_DOCS["a2-known-constructors"],
1055
- messages: { unknownConstructor: "specification.{{member}} does not exist — the only constructors are specification.api(), specification.jobs(), specification.cli(), specification.website() and specification.mobile() (A2 — see docs/13-linting.md)." },
1091
+ messages: { unknownConstructor: "specification.{{member}} does not exist — the only constructors are specification.api(), specification.jobs(), specification.cli(), specification.integration(), specification.website() and specification.mobile() (A2 — see docs/13-linting.md)." },
1056
1092
  type: "problem"
1057
1093
  }
1058
1094
  };
@@ -3262,16 +3298,14 @@ const FRAMEWORK_LAYERS = /* @__PURE__ */ new Set([
3262
3298
  *
3263
3299
  * Distinct from F1 (which judges the same specifiers from any file): F3 is the
3264
3300
  * specs-specific guard that also reaches relative framework-internal paths.
3265
- * Documented exception: specs under `specs/integrations/` may deep-import the
3266
- * integration module they cover (`src/integrations/**`).
3301
+ * There is no folder exception: a probe that cannot reach its subject through
3302
+ * the public entry is a module test beside its module, not a spec.
3267
3303
  */
3268
3304
  const f3SpecsPublicEntry = {
3269
3305
  create(context) {
3270
- const anchor = specsAnchor(context.filename);
3271
- if (anchor === void 0) return {};
3306
+ if (specsAnchor(context.filename) === void 0) return {};
3272
3307
  const published = new Set(declaredSubpaths());
3273
3308
  const exempt = published.size === 0 ? "none" : [...published].join(", ");
3274
- const underIntegrations = anchor.relative[0] === "integrations";
3275
3309
  return { ...importSourceVisitor(({ node, source }) => {
3276
3310
  if (source.startsWith(`${PACKAGE}/`)) {
3277
3311
  if (published.has(source)) return;
@@ -3289,7 +3323,6 @@ const f3SpecsPublicEntry = {
3289
3323
  if (marker === -1 && !source.startsWith("src/")) return;
3290
3324
  const layer = (marker === -1 ? source.slice(4) : source.slice(marker + 5)).split("/")[0] ?? "";
3291
3325
  if (!FRAMEWORK_LAYERS.has(layer)) return;
3292
- if (underIntegrations && layer === "integrations") return;
3293
3326
  context.report({
3294
3327
  data: {
3295
3328
  published: exempt,
@@ -3302,7 +3335,7 @@ const f3SpecsPublicEntry = {
3302
3335
  },
3303
3336
  meta: {
3304
3337
  docs: RULE_DOCS["f3-specs-public-entry"],
3305
- messages: { deepImport: "specs/ must not deep-import framework internals — reach the framework via its public entry (@jterrazz/test, or src/index.js in this repo), not \"{{source}}\" (F3 — see docs/13-linting.md). Exempt: the published subpaths {{published}}, and src/integrations/** from specs/integrations/." },
3338
+ messages: { deepImport: "specs/ must not deep-import framework internals — reach the framework via its public entry (@jterrazz/test, or src/index.js in this repo), not \"{{source}}\" (F3 — see docs/13-linting.md). Exempt: the published subpaths {{published}}. A probe whose subject is not on the public entry is a module test beside its module." },
3306
3339
  type: "problem"
3307
3340
  }
3308
3341
  };
package/dist/oxlint.d.cts CHANGED
@@ -7129,7 +7129,7 @@ type RuleDoc = {
7129
7129
  * The specification facet the rule guards — segments the catalogue like
7130
7130
  * the constructors segment the API. `'shared'` for cross-facet rules.
7131
7131
  */
7132
- facet?: 'api' | 'cli' | 'component' | 'jobs' | 'mobile' | 'shared' | 'website';
7132
+ facet?: 'api' | 'cli' | 'component' | 'integration' | 'jobs' | 'mobile' | 'shared' | 'website';
7133
7133
  /** Convention family letter, e.g. `'A'`. */
7134
7134
  family: string;
7135
7135
  /** Convention code, e.g. `'A1'`. */
package/dist/oxlint.d.ts CHANGED
@@ -7129,7 +7129,7 @@ type RuleDoc = {
7129
7129
  * The specification facet the rule guards — segments the catalogue like
7130
7130
  * the constructors segment the API. `'shared'` for cross-facet rules.
7131
7131
  */
7132
- facet?: 'api' | 'cli' | 'component' | 'jobs' | 'mobile' | 'shared' | 'website';
7132
+ facet?: 'api' | 'cli' | 'component' | 'integration' | 'jobs' | 'mobile' | 'shared' | 'website';
7133
7133
  /** Convention family letter, e.g. `'A'`. */
7134
7134
  family: string;
7135
7135
  /** Convention code, e.g. `'A1'`. */
package/dist/oxlint.js CHANGED
@@ -280,7 +280,7 @@ const RULE_DOCS = {
280
280
  },
281
281
  "a2-known-constructors": {
282
282
  channel: "statique",
283
- convention: "Cinq constructeurs et seulement cinq : `specification.api()`, `specification.jobs()`, `specification.cli(bin)`, `specification.website()`, `specification.mobile()` ; tout autre membre (`.app`, `.http`, `.stack`…) est une erreur.",
283
+ convention: "Six constructeurs et seulement six : `specification.api()`, `specification.jobs()`, `specification.cli(bin)`, `specification.integration()`, `specification.website()`, `specification.mobile()` ; tout autre membre (`.app`, `.http`, `.stack`…) est une erreur. Un composant rendu ne démarre rien : c’est une chaîne, pas un membre de ce record.",
284
284
  family: "A",
285
285
  id: "A2",
286
286
  rationale: "Une surface fermée empêche l’invention de constructeurs parallèles et garde l’API mémorisable."
@@ -527,7 +527,7 @@ const RULE_DOCS = {
527
527
  },
528
528
  "f3-specs-public-entry": {
529
529
  channel: "statique",
530
- convention: "Depuis `specs/`, seul l’import en profondeur des INTERNES du framework est interdit : un chemin relatif résolvant dans `src/{specification,integrations,vitest,lint}/` du dépôt du framework, ou tout `@jterrazz/test/<subpath>` que la map `exports` du paquet ne publie pas — les subpaths publiés sont exemptés, LUS du manifeste comme en F1, qui en tient la liste. Les imports de la source de SA PROPRE app par un consommateur sont toujours permis (c’est le motif documenté) ; seul `specs/integrations/` peut importer en profondeur `src/integrations/**`.",
530
+ convention: "Depuis `specs/`, seul l’import en profondeur des INTERNES du framework est interdit : un chemin relatif résolvant dans `src/{specification,integrations,vitest,lint}/` du dépôt du framework, ou tout `@jterrazz/test/<subpath>` que la map `exports` du paquet ne publie pas — les subpaths publiés sont exemptés, LUS du manifeste comme en F1, qui en tient la liste. Les imports de la source de SA PROPRE app par un consommateur sont toujours permis (c’est le motif documenté). Aucune exception de dossier : une sonde qui ne peut pas atteindre son sujet par l’entrée publique est un test de module à côté de son module, pas une spec.",
531
531
  family: "F",
532
532
  id: "F3",
533
533
  rationale: "Tester par la surface publique garde les specs découplées des chemins internes du framework, sans gêner le consommateur qui importe sa propre app."
@@ -619,6 +619,39 @@ const RULE_DOCS = {
619
619
  * visits data fixtures nor reads two files at once, so these ship separately.
620
620
  */
621
621
  const CHECKER_PASSES = [
622
+ {
623
+ channel: "checker",
624
+ convention: "Chaque membre du workspace qui a des tests À LUI (un script `test` qui ne délègue pas aux membres, ou un `*.test.ts(x)` hors de l’arbre d’un autre paquet) déclare une config vitest (`vitest.config.*`). La passe MEMBRE juge le membre lui-même, avec ou sans arbre `specs/` ; une racine qui ne fait que déléguer ne doit rien.",
625
+ facet: "shared",
626
+ family: "E",
627
+ fix: "Écrire `export default defineSpecConfig()` dans le membre — le préréglage porte les budgets, le dossier d’artefacts et l’exclusion de `_fixtures/`.",
628
+ id: "E3",
629
+ name: "e3-config-present",
630
+ rationale: "Sans config, le budget de 5 s de vitest et un `_fixtures/` non exclu font tourner les tests du membre : le défaut n’est pas choisi, il est hérité.",
631
+ reach: "config"
632
+ },
633
+ {
634
+ channel: "checker",
635
+ convention: "La passe MEMBRE relit la règle E5b là où oxlint peut ne pas atteindre la config d’un membre : `environment: 'happy-dom'|'jsdom'` dans un `vitest.config.*` est une erreur.",
636
+ facet: "component",
637
+ family: "E",
638
+ fix: "Supprimer `environment` et collecter le rendu avec `component()`.",
639
+ id: "E5b",
640
+ name: "e5b-no-simulated-dom-config-member",
641
+ rationale: "La passe statique ne voit que ce que la configuration oxlint du dépôt lui donne ; la passe membre part du manifeste racine et atteint chaque membre déclaré.",
642
+ reach: "config"
643
+ },
644
+ {
645
+ channel: "checker",
646
+ convention: "Un membre ne déclare aucune dépendance que `@jterrazz/test` porte déjà (`msw`, `vitest-mock-extended` ; `yaml` en devDependencies seulement — en dépendance de production c’est la bibliothèque du produit) ni aucun seam retiré (`happy-dom`, `jsdom`, `@testing-library/*`, `@playwright/test`, `mockdate`). Les peers optionnels — `playwright`, `vite`, `react`, `better-sqlite3`, `pg`, `redis`, `testcontainers`… — sont déclarés par le consommateur, à dessein.",
647
+ facet: "shared",
648
+ family: "F",
649
+ fix: "Retirer la déclaration : une transitive est déjà résolue par le paquet ; un seam retiré a une facette qui le remplace (`component()`, `website()`, `clock`, `intercept()`). Les deux cas ont leur propre phrase dans le diagnostic.",
650
+ id: "F8",
651
+ name: "f8-no-seam-dependency",
652
+ rationale: "Une déclaration en double laisse deux versions d’un même seam coexister, et un seam retiré garde un vocabulaire que le framework a remplacé.",
653
+ reach: "config"
654
+ },
622
655
  {
623
656
  channel: "checker",
624
657
  convention: "Tout `{{token}}` dans une fixture `_expected/` — ou dans un flux attendu d’un document `<cas>.spec.yaml` — appartient au vocabulaire figé ; un token inconnu est une erreur.",
@@ -1019,19 +1052,22 @@ const a1SpecificationFile = {
1019
1052
  };
1020
1053
  //#endregion
1021
1054
  //#region src/lint/rules/a2-known-constructors.ts
1022
- /** The five constructors, and only five (A2 — see docs/13-linting.md). */
1055
+ /** The six constructors, and only six (A2 — see docs/13-linting.md). */
1023
1056
  const KNOWN_CONSTRUCTORS = /* @__PURE__ */ new Set([
1024
1057
  "api",
1025
1058
  "cli",
1059
+ "integration",
1026
1060
  "jobs",
1027
1061
  "mobile",
1028
1062
  "website"
1029
1063
  ]);
1030
1064
  /**
1031
1065
  * CONVENTIONS A2 — `specification.api()`, `specification.jobs()`,
1032
- * `specification.cli()`, `specification.website()` and
1033
- * `specification.mobile()` are the only members. Any other access
1034
- * (`specification.app`, `.http`, `.stack`, …) is flagged at the member site.
1066
+ * `specification.cli()`, `specification.integration()`,
1067
+ * `specification.website()` and `specification.mobile()` are the only
1068
+ * members. Any other access (`specification.app`, `.http`, `.stack`, …) is
1069
+ * flagged at the member site. A rendered component has no constructor: it
1070
+ * starts nothing, so it is a chain, not a member of this record.
1035
1071
  */
1036
1072
  const a2KnownConstructors = {
1037
1073
  create(context) {
@@ -1048,7 +1084,7 @@ const a2KnownConstructors = {
1048
1084
  },
1049
1085
  meta: {
1050
1086
  docs: RULE_DOCS["a2-known-constructors"],
1051
- messages: { unknownConstructor: "specification.{{member}} does not exist — the only constructors are specification.api(), specification.jobs(), specification.cli(), specification.website() and specification.mobile() (A2 — see docs/13-linting.md)." },
1087
+ messages: { unknownConstructor: "specification.{{member}} does not exist — the only constructors are specification.api(), specification.jobs(), specification.cli(), specification.integration(), specification.website() and specification.mobile() (A2 — see docs/13-linting.md)." },
1052
1088
  type: "problem"
1053
1089
  }
1054
1090
  };
@@ -3258,16 +3294,14 @@ const FRAMEWORK_LAYERS = /* @__PURE__ */ new Set([
3258
3294
  *
3259
3295
  * Distinct from F1 (which judges the same specifiers from any file): F3 is the
3260
3296
  * specs-specific guard that also reaches relative framework-internal paths.
3261
- * Documented exception: specs under `specs/integrations/` may deep-import the
3262
- * integration module they cover (`src/integrations/**`).
3297
+ * There is no folder exception: a probe that cannot reach its subject through
3298
+ * the public entry is a module test beside its module, not a spec.
3263
3299
  */
3264
3300
  const f3SpecsPublicEntry = {
3265
3301
  create(context) {
3266
- const anchor = specsAnchor(context.filename);
3267
- if (anchor === void 0) return {};
3302
+ if (specsAnchor(context.filename) === void 0) return {};
3268
3303
  const published = new Set(declaredSubpaths());
3269
3304
  const exempt = published.size === 0 ? "none" : [...published].join(", ");
3270
- const underIntegrations = anchor.relative[0] === "integrations";
3271
3305
  return { ...importSourceVisitor(({ node, source }) => {
3272
3306
  if (source.startsWith(`${PACKAGE}/`)) {
3273
3307
  if (published.has(source)) return;
@@ -3285,7 +3319,6 @@ const f3SpecsPublicEntry = {
3285
3319
  if (marker === -1 && !source.startsWith("src/")) return;
3286
3320
  const layer = (marker === -1 ? source.slice(4) : source.slice(marker + 5)).split("/")[0] ?? "";
3287
3321
  if (!FRAMEWORK_LAYERS.has(layer)) return;
3288
- if (underIntegrations && layer === "integrations") return;
3289
3322
  context.report({
3290
3323
  data: {
3291
3324
  published: exempt,
@@ -3298,7 +3331,7 @@ const f3SpecsPublicEntry = {
3298
3331
  },
3299
3332
  meta: {
3300
3333
  docs: RULE_DOCS["f3-specs-public-entry"],
3301
- messages: { deepImport: "specs/ must not deep-import framework internals — reach the framework via its public entry (@jterrazz/test, or src/index.js in this repo), not \"{{source}}\" (F3 — see docs/13-linting.md). Exempt: the published subpaths {{published}}, and src/integrations/** from specs/integrations/." },
3334
+ messages: { deepImport: "specs/ must not deep-import framework internals — reach the framework via its public entry (@jterrazz/test, or src/index.js in this repo), not \"{{source}}\" (F3 — see docs/13-linting.md). Exempt: the published subpaths {{published}}. A probe whose subject is not on the public entry is a module test beside its module." },
3302
3335
  type: "problem"
3303
3336
  }
3304
3337
  };
@@ -222,6 +222,7 @@ var PlaywrightAdapter = class {
222
222
  }
223
223
  try {
224
224
  const page = await context.newPage();
225
+ if (options.clock !== void 0) await page.clock.setFixedTime(new Date(options.clock));
225
226
  const consoleMessages = [];
226
227
  page.on("console", (message) => {
227
228
  consoleMessages.push({
@@ -86,6 +86,35 @@ function contractsOf(input) {
86
86
  return flatten([input]);
87
87
  }
88
88
  //#endregion
89
+ //#region src/specification/contracts/types.ts
90
+ /**
91
+ * The marker that tells a STREAMED body from a value that happens to be an
92
+ * object. A body reaches three different engines (msw's server, msw's worker,
93
+ * the `node:http` stub) and each of them serialises "an object" as JSON, so a
94
+ * stream declared as a plain value arrives as `{}`. The tag is what each
95
+ * engine recognises before it reaches for `JSON.stringify`.
96
+ */
97
+ const STREAM_BODY = "@jterrazz/test:stream";
98
+ /** Is this body a declared stream rather than a value to serialise? */
99
+ function isStreamBody(body) {
100
+ return typeof body === "object" && body !== null && "kind" in body && body.kind === "@jterrazz/test:stream";
101
+ }
102
+ //#endregion
103
+ //#region src/specification/timing/pause.ts
104
+ /**
105
+ * One answer to "wait" — the only `setTimeout` the model owns.
106
+ *
107
+ * Two things in the framework genuinely have to wait: the spacing between the
108
+ * pieces of a declared stream, and the poll behind `waitUntil`. Everything
109
+ * else that waits is a spec doing it by hand, which is what J2 refuses. Stated
110
+ * once so there is one place to look when time stops behaving.
111
+ */
112
+ async function pause(ms) {
113
+ await new Promise((resolve) => {
114
+ setTimeout(resolve, ms);
115
+ });
116
+ }
117
+ //#endregion
89
118
  //#region src/specification/contracts/queue.ts
90
119
  /**
91
120
  * The ONE contract queue — pure, engine-agnostic (no MSW, no node:http).
@@ -259,4 +288,34 @@ var ContractQueue = class {
259
288
  }
260
289
  };
261
290
  //#endregion
262
- export { isContract as a, defineContracts as i, contractsOf as n, isContracts as o, defineContract as r, ContractQueue as t };
291
+ //#region src/specification/contracts/stream.ts
292
+ /**
293
+ * A declared stream, made real — ONE serialisation for all three engines.
294
+ *
295
+ * msw's server and msw's worker take a `ReadableStream` body verbatim
296
+ * (https://mswjs.io/docs/http/mocking-responses/streaming) and the `node:http`
297
+ * stub pipes one into its response, so what "arriving in pieces" MEANS —
298
+ * the order, the spacing, the close after the last chunk — is stated here and
299
+ * nowhere else. A second copy of it would be a second answer to "what does the
300
+ * subject read", and the facets would drift.
301
+ */
302
+ /**
303
+ * The pieces of `body`, in order, `delayBetweenChunks` milliseconds apart,
304
+ * closing after the last one.
305
+ */
306
+ function toReadableStream(body) {
307
+ const encoder = new TextEncoder();
308
+ let index = 0;
309
+ return new ReadableStream({ async pull(controller) {
310
+ const chunk = body.chunks[index];
311
+ if (chunk === void 0) {
312
+ controller.close();
313
+ return;
314
+ }
315
+ if (body.delayBetweenChunks > 0 && index > 0) await pause(body.delayBetweenChunks);
316
+ index += 1;
317
+ controller.enqueue(encoder.encode(chunk));
318
+ } });
319
+ }
320
+ //#endregion
321
+ export { isStreamBody as a, defineContracts as c, STREAM_BODY as i, isContract as l, ContractQueue as n, contractsOf as o, pause as r, defineContract as s, toReadableStream as t, isContracts as u };
package/dist/vitest.d.ts CHANGED
@@ -71,11 +71,13 @@ declare function defineSpecConfig(config?: SpecConfig): ViteUserConfig;
71
71
  * states only what is theirs.
72
72
  */
73
73
  /** What every helper accepts on top of its canonical project. */
74
- type CommonOptions = {
74
+ type FacetProjectOptions = {
75
75
  /**
76
- * Replace the canonical `exclude` globs. A helper whose canonical
77
- * `exclude` only guards its canonical `include` drops it when you state
78
- * `include` alone, so state both or neither.
76
+ * Globs this project stays out of. On a facet helper they ADD to the
77
+ * preset's (`_fixtures/` and vitest's own), because vite concatenates. On
78
+ * `unit()` and `component()`, whose canonical `exclude` exists only to
79
+ * guard their canonical `include`, stating one replaces it — so state
80
+ * both or neither there.
79
81
  */
80
82
  exclude?: string[];
81
83
  /** Replace the canonical `include` globs. */
@@ -90,7 +92,7 @@ type CommonOptions = {
90
92
  timeout?: number;
91
93
  };
92
94
  /** `unit()` — module tests, beside the modules they cover (CONVENTIONS I2). */
93
- type UnitProjectOptions = CommonOptions & {
95
+ type UnitProjectOptions = FacetProjectOptions & {
94
96
  /**
95
97
  * The trees module tests live in, when they do not live in `src/`. Each
96
98
  * root becomes `<root>/**\/*.test.ts`; `include` states the globs outright.
@@ -98,7 +100,7 @@ type UnitProjectOptions = CommonOptions & {
98
100
  roots?: string[];
99
101
  };
100
102
  /** `website()` — the assembled product, met through a served page. */
101
- type WebsiteProjectOptions = CommonOptions;
103
+ type WebsiteProjectOptions = FacetProjectOptions;
102
104
  /** `component()` — rendered units, beside the components they cover. */
103
105
  type ComponentProjectOptions = {
104
106
  /** Freeze the page's `Date` for every render of the project. */
@@ -130,7 +132,7 @@ type ComponentProjectOptions = {
130
132
  * provider). A router belongs on the chain instead: it is a test's Given.
131
133
  */
132
134
  wrap?: string;
133
- } & CommonOptions;
135
+ } & FacetProjectOptions;
134
136
  /**
135
137
  * A module test runs beside the module it covers: no DOM, no browser, no
136
138
  * pipeline. `.test.tsx` is excluded because a rendered thing is a component
@@ -154,5 +156,33 @@ declare function website(options?: WebsiteProjectOptions): TestProjectInlineConf
154
156
  * your own `include` and the exclusion is yours to state too.
155
157
  */
156
158
  declare function component(options?: ComponentProjectOptions): Promise<TestProjectInlineConfiguration>;
159
+ /** `api()` — the app met through HTTP. */
160
+ declare function api(options?: FacetProjectOptions): TestProjectInlineConfiguration;
161
+ /** `jobs()` — what a name triggers in-process. */
162
+ declare function jobs(options?: FacetProjectOptions): TestProjectInlineConfiguration;
163
+ /** `integration()` — a module against real services, or against a golden. */
164
+ declare function integration(options?: FacetProjectOptions): TestProjectInlineConfiguration;
165
+ /**
166
+ * `mobile()` — the app on a simulator. A simulator is the one thing that
167
+ * cannot share a machine with itself, so it runs last and alone (group 3).
168
+ */
169
+ declare function mobile(options?: FacetProjectOptions): TestProjectInlineConfiguration;
170
+ /** `cli()` — a binary, and the documents that specify it. */
171
+ type CliProjectOptions = FacetProjectOptions & {
172
+ /**
173
+ * The literate door, wired by default: every `specs/cli/**\/*.spec.yaml`
174
+ * becomes a test file bound to `specs/cli/cli.specification.ts`. State
175
+ * `specification` to name another runner, `include` to narrow the glob, or
176
+ * `false` to collect no documents at all.
177
+ */
178
+ literate?: false | Partial<LiterateOptions>;
179
+ };
180
+ /**
181
+ * A cli project collects the facet's `.test.ts` files AND its documents: a
182
+ * `<case>.spec.yaml` is a test file of its own (docs/07 § the literate door),
183
+ * so the plugin's glob has to join the include of the project that collects
184
+ * them — which is this one, never the root config.
185
+ */
186
+ declare function cli(options?: CliProjectOptions): TestProjectInlineConfiguration;
157
187
  //#endregion
158
- export { type ComponentProjectOptions, type LiterateOptions, type LiteratePlugin, type SpecConfig, type UnitProjectOptions, type WebsiteProjectOptions, component, defineSpecConfig, literate, unit, website };
188
+ export { type CliProjectOptions, type ComponentProjectOptions, type FacetProjectOptions, type LiterateOptions, type LiteratePlugin, type SpecConfig, type UnitProjectOptions, type WebsiteProjectOptions, api, cli, component, defineSpecConfig, integration, jobs, literate, mobile, unit, website };
package/dist/vitest.js CHANGED
@@ -535,5 +535,63 @@ async function component(options = {}) {
535
535
  };
536
536
  return mergeConfig(projectDefaults(), project);
537
537
  }
538
+ /**
539
+ * The node facets' canonical projects.
540
+ *
541
+ * Each one collects the facet's own tree, carries the preset's budgets and
542
+ * artefact directory, and runs in group 0 — the browser facets follow, so two
543
+ * Chromiums never share a slot on a two-vCPU runner. `{ include, exclude,
544
+ * timeout, serial }` is what a repository states when its tree, its budget or
545
+ * its parallelism differs;
546
+ * everything else is the framework's.
547
+ */
548
+ /** The globs a node facet collects, and the ones a caller stated instead. */
549
+ function facetProject(facet, options) {
550
+ return mergeConfig(projectDefaults(), { test: {
551
+ ...options.exclude === void 0 ? {} : { exclude: options.exclude },
552
+ include: options.include ?? [`specs/${facet}/**/*.test.ts`],
553
+ ...options.serial === true ? { fileParallelism: false } : {},
554
+ name: facet,
555
+ sequence: { groupOrder: 0 },
556
+ ...options.timeout === void 0 ? {} : { testTimeout: options.timeout }
557
+ } });
558
+ }
559
+ /** `api()` — the app met through HTTP. */
560
+ function api(options = {}) {
561
+ return facetProject("api", options);
562
+ }
563
+ /** `jobs()` — what a name triggers in-process. */
564
+ function jobs(options = {}) {
565
+ return facetProject("jobs", options);
566
+ }
567
+ /** `integration()` — a module against real services, or against a golden. */
568
+ function integration(options = {}) {
569
+ return facetProject("integration", options);
570
+ }
571
+ /**
572
+ * `mobile()` — the app on a simulator. A simulator is the one thing that
573
+ * cannot share a machine with itself, so it runs last and alone (group 3).
574
+ */
575
+ function mobile(options = {}) {
576
+ return mergeConfig(facetProject("mobile", options), { test: { sequence: { groupOrder: 3 } } });
577
+ }
578
+ /** The literate defaults — the path convention `cli()` assumes when nothing says otherwise. */
579
+ const CLI_SPECIFICATION = "./specs/cli/cli.specification.ts";
580
+ const CLI_DOCUMENTS = ["specs/cli/**/*.spec.yaml"];
581
+ /**
582
+ * A cli project collects the facet's `.test.ts` files AND its documents: a
583
+ * `<case>.spec.yaml` is a test file of its own (docs/07 § the literate door),
584
+ * so the plugin's glob has to join the include of the project that collects
585
+ * them — which is this one, never the root config.
586
+ */
587
+ function cli(options = {}) {
588
+ const { literate: literateOptions, ...common } = options;
589
+ const project = facetProject("cli", common);
590
+ if (literateOptions === false) return project;
591
+ return mergeConfig(project, { plugins: [literate({
592
+ include: literateOptions?.include ?? CLI_DOCUMENTS,
593
+ specification: literateOptions?.specification ?? CLI_SPECIFICATION
594
+ })] });
595
+ }
538
596
  //#endregion
539
- export { component, defineSpecConfig, literate, unit, website };
597
+ export { api, cli, component, defineSpecConfig, integration, jobs, literate, mobile, unit, website };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jterrazz/test",
3
- "version": "15.2.0",
3
+ "version": "15.3.0",
4
4
  "description": "Declarative testing framework for HTTP APIs, CLIs, background jobs, websites, mobile apps and rendered components — one entry point, real infrastructure and a real browser, golden-file assertions, plus an oxlint convention plugin.",
5
5
  "keywords": [
6
6
  "api-testing",