@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/README.md +38 -29
- package/dist/artifacts.js +41 -4
- package/dist/browser/index.js +1364 -1045
- package/dist/checker.js +461 -26
- package/dist/index.d.ts +648 -133
- package/dist/index.js +726 -39
- package/dist/intercept.js +9 -1
- package/dist/oxlint.cjs +47 -14
- package/dist/oxlint.d.cts +1 -1
- package/dist/oxlint.d.ts +1 -1
- package/dist/oxlint.js +47 -14
- package/dist/playwright.adapter.js +1 -0
- package/dist/{queue.js → stream.js} +60 -1
- package/dist/vitest.d.ts +38 -8
- package/dist/vitest.js +59 -1
- package/package.json +1 -1
package/dist/intercept.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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: "
|
|
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é)
|
|
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
|
|
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.
|
|
1037
|
-
* `specification.mobile()` are the only
|
|
1038
|
-
* (`specification.app`, `.http`, `.stack`, …) is
|
|
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
|
-
*
|
|
3266
|
-
*
|
|
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
|
-
|
|
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}}
|
|
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: "
|
|
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é)
|
|
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
|
|
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.
|
|
1033
|
-
* `specification.mobile()` are the only
|
|
1034
|
-
* (`specification.app`, `.http`, `.stack`, …) is
|
|
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
|
-
*
|
|
3262
|
-
*
|
|
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
|
-
|
|
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}}
|
|
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
|
-
|
|
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
|
|
74
|
+
type FacetProjectOptions = {
|
|
75
75
|
/**
|
|
76
|
-
*
|
|
77
|
-
* `
|
|
78
|
-
* `
|
|
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 =
|
|
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 =
|
|
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
|
-
} &
|
|
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.
|
|
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",
|