@jterrazz/test 9.0.0 → 9.2.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 +81 -28
- package/dist/checker.js +9 -9
- package/dist/index.d.ts +314 -3
- package/dist/index.js +428 -7
- package/dist/oxlint.cjs +132 -66
- package/dist/oxlint.d.cts +5 -0
- package/dist/oxlint.d.ts +5 -0
- package/dist/oxlint.js +132 -66
- package/dist/playwright.adapter.js +135 -0
- package/package.json +12 -4
package/dist/oxlint.js
CHANGED
|
@@ -162,7 +162,7 @@ const RULE_DOCS = {
|
|
|
162
162
|
},
|
|
163
163
|
"a2-known-constructors": {
|
|
164
164
|
channel: "statique",
|
|
165
|
-
convention: "
|
|
165
|
+
convention: "Quatre constructeurs et seulement quatre : `specification.api()`, `specification.jobs()`, `specification.cli(bin)`, `specification.website()` ; tout autre membre (`.app`, `.http`, `.stack`…) est une erreur.",
|
|
166
166
|
family: "A",
|
|
167
167
|
id: "A2",
|
|
168
168
|
rationale: "Une surface fermée empêche l’invention de constructeurs parallèles et garde l’API mémorisable."
|
|
@@ -432,6 +432,22 @@ const RULE_DOCS = {
|
|
|
432
432
|
family: "J",
|
|
433
433
|
id: "J5",
|
|
434
434
|
rationale: "Un titre est un fragment de prose, pas une phrase — la casse minuscule le garde fragmentaire ; minusculiser un symbole nommé le mal-orthographierait."
|
|
435
|
+
},
|
|
436
|
+
"w1-scenario-pure": {
|
|
437
|
+
channel: "statique",
|
|
438
|
+
convention: "Un scénario de visite est le When : le visiteur agit, la capture reflète l’état final ; aucun `expect()` dans le callback — les assertions vivent dans le Then, sur le résultat retourné.",
|
|
439
|
+
facet: "website",
|
|
440
|
+
family: "W",
|
|
441
|
+
id: "W1",
|
|
442
|
+
rationale: "Séparer l’interaction de l’assertion garde la grammaire setup → action → résultat intacte et les scénarios rejouables."
|
|
443
|
+
},
|
|
444
|
+
"w2w-user-facing-elements": {
|
|
445
|
+
channel: "statique",
|
|
446
|
+
convention: "Les éléments d’un scénario sont user-facing (`button`, `link`, `field`, `heading`, `content`) ; `testId()` est l’unique échappatoire et déclenche un avertissement.",
|
|
447
|
+
facet: "website",
|
|
448
|
+
family: "W",
|
|
449
|
+
id: "W2",
|
|
450
|
+
rationale: "Tester ce que l’utilisateur voit (rôles, labels) rend les specs robustes aux refontes DOM ; un test-id contourne cette garantie."
|
|
435
451
|
}
|
|
436
452
|
};
|
|
437
453
|
/**
|
|
@@ -653,7 +669,7 @@ const a1SpecificationFile = {
|
|
|
653
669
|
},
|
|
654
670
|
meta: {
|
|
655
671
|
docs: RULE_DOCS["a1-specification-file"],
|
|
656
|
-
messages: { outsideSpecification: "specification.{{member}}() must be called from a `*.specification.ts` file under specs/ (
|
|
672
|
+
messages: { outsideSpecification: "specification.{{member}}() must be called from a `*.specification.ts` file under specs/ (A1 — see docs/10-linting.md)." },
|
|
657
673
|
type: "problem"
|
|
658
674
|
}
|
|
659
675
|
};
|
|
@@ -730,22 +746,24 @@ const a10DuplicateBinding = {
|
|
|
730
746
|
},
|
|
731
747
|
meta: {
|
|
732
748
|
docs: RULE_DOCS["a10-duplicate-binding"],
|
|
733
|
-
messages: { duplicate: "Service keys \"{{first}}\" and \"{{key}}\" both bind to compose service \"{{binding}}\" — one shadows the other; rename a key or set distinct composeService values (
|
|
749
|
+
messages: { duplicate: "Service keys \"{{first}}\" and \"{{key}}\" both bind to compose service \"{{binding}}\" — one shadows the other; rename a key or set distinct composeService values (A10 — see docs/10-linting.md)." },
|
|
734
750
|
type: "problem"
|
|
735
751
|
}
|
|
736
752
|
};
|
|
737
753
|
//#endregion
|
|
738
754
|
//#region src/lint/rules/a2-known-constructors.ts
|
|
739
|
-
/** The
|
|
755
|
+
/** The four constructors, and only four (A2 — see docs/10-linting.md). */
|
|
740
756
|
const KNOWN_CONSTRUCTORS = /* @__PURE__ */ new Set([
|
|
741
757
|
"api",
|
|
742
758
|
"cli",
|
|
743
|
-
"jobs"
|
|
759
|
+
"jobs",
|
|
760
|
+
"website"
|
|
744
761
|
]);
|
|
745
762
|
/**
|
|
746
|
-
* CONVENTIONS A2 — `specification.api()`, `specification.jobs()
|
|
747
|
-
* `specification.cli()` are the only members.
|
|
748
|
-
* (`specification.app`, `.http`, `.stack`, …) is flagged at
|
|
763
|
+
* CONVENTIONS A2 — `specification.api()`, `specification.jobs()`,
|
|
764
|
+
* `specification.cli()` and `specification.website()` are the only members.
|
|
765
|
+
* Any other access (`specification.app`, `.http`, `.stack`, …) is flagged at
|
|
766
|
+
* the member site.
|
|
749
767
|
*/
|
|
750
768
|
const a2KnownConstructors = {
|
|
751
769
|
create(context) {
|
|
@@ -762,7 +780,7 @@ const a2KnownConstructors = {
|
|
|
762
780
|
},
|
|
763
781
|
meta: {
|
|
764
782
|
docs: RULE_DOCS["a2-known-constructors"],
|
|
765
|
-
messages: { unknownConstructor: "specification.{{member}} does not exist — the only constructors are specification.api(), specification.jobs() and specification.
|
|
783
|
+
messages: { unknownConstructor: "specification.{{member}} does not exist — the only constructors are specification.api(), specification.jobs(), specification.cli() and specification.website() (A2 — see docs/10-linting.md)." },
|
|
766
784
|
type: "problem"
|
|
767
785
|
}
|
|
768
786
|
};
|
|
@@ -807,7 +825,7 @@ const a3NoDestructureAlias = {
|
|
|
807
825
|
},
|
|
808
826
|
meta: {
|
|
809
827
|
docs: RULE_DOCS["a3-no-destructure-alias"],
|
|
810
|
-
messages: { aliased: "Destructure the specification result with its canonical name: `{{key}}`, not `{{key}}: {{alias}}` (
|
|
828
|
+
messages: { aliased: "Destructure the specification result with its canonical name: `{{key}}`, not `{{key}}: {{alias}}` (A3 — see docs/10-linting.md). Rename at the import site if a different local name is needed." },
|
|
811
829
|
type: "problem"
|
|
812
830
|
}
|
|
813
831
|
};
|
|
@@ -859,7 +877,7 @@ const a4CleanupAfterall = {
|
|
|
859
877
|
},
|
|
860
878
|
meta: {
|
|
861
879
|
docs: RULE_DOCS["a4-cleanup-afterall"],
|
|
862
|
-
messages: { unregistered: "`cleanup` is destructured but never passed to afterAll — add `afterAll(cleanup)` to the specification file (
|
|
880
|
+
messages: { unregistered: "`cleanup` is destructured but never passed to afterAll — add `afterAll(cleanup)` to the specification file (A4 — see docs/10-linting.md)." },
|
|
863
881
|
type: "problem"
|
|
864
882
|
}
|
|
865
883
|
};
|
|
@@ -889,7 +907,7 @@ const a5ModeWithServer = {
|
|
|
889
907
|
},
|
|
890
908
|
meta: {
|
|
891
909
|
docs: RULE_DOCS["a5-mode-with-server"],
|
|
892
|
-
messages: { hardcodedMode: "`mode` must not be hardcoded when `server` is defined — the node/compose switch lives in vitest.config.ts via `env: { TEST_MODE: \"compose\" }` (
|
|
910
|
+
messages: { hardcodedMode: "`mode` must not be hardcoded when `server` is defined — the node/compose switch lives in vitest.config.ts via `env: { TEST_MODE: \"compose\" }` (A5 — see docs/10-linting.md)." },
|
|
893
911
|
type: "problem"
|
|
894
912
|
}
|
|
895
913
|
};
|
|
@@ -930,7 +948,7 @@ const a6wRedundantComposeService = {
|
|
|
930
948
|
},
|
|
931
949
|
meta: {
|
|
932
950
|
docs: RULE_DOCS["a6w-redundant-compose-service"],
|
|
933
|
-
messages: { redundant: "composeService: \"{{name}}\" is redundant — the key \"{{key}}\" already binds to it (exact name or kebab-case derivation,
|
|
951
|
+
messages: { redundant: "composeService: \"{{name}}\" is redundant — the key \"{{key}}\" already binds to it (exact name or kebab-case derivation, A6 — see docs/10-linting.md)." },
|
|
934
952
|
type: "suggestion"
|
|
935
953
|
}
|
|
936
954
|
};
|
|
@@ -994,7 +1012,7 @@ function readFileCached(path) {
|
|
|
994
1012
|
//#endregion
|
|
995
1013
|
//#region src/lint/rules/a9w-redundant-root.ts
|
|
996
1014
|
/**
|
|
997
|
-
* The root the convention would derive (
|
|
1015
|
+
* The root the convention would derive (A9 — see docs/10-linting.md): walk up from the
|
|
998
1016
|
* specification file to the first directory containing
|
|
999
1017
|
* `docker/compose.test.yaml`, else the first containing `package.json`.
|
|
1000
1018
|
*/
|
|
@@ -1033,7 +1051,7 @@ const a9wRedundantRoot = {
|
|
|
1033
1051
|
},
|
|
1034
1052
|
meta: {
|
|
1035
1053
|
docs: RULE_DOCS["a9w-redundant-root"],
|
|
1036
|
-
messages: { redundant: "root: \"{{root}}\" is redundant — walking up from the specification file already resolves to that directory (
|
|
1054
|
+
messages: { redundant: "root: \"{{root}}\" is redundant — walking up from the specification file already resolves to that directory (A9 — see docs/10-linting.md)." },
|
|
1037
1055
|
type: "suggestion"
|
|
1038
1056
|
}
|
|
1039
1057
|
};
|
|
@@ -1097,7 +1115,7 @@ const b2KnownFixtureMarker = {
|
|
|
1097
1115
|
},
|
|
1098
1116
|
meta: {
|
|
1099
1117
|
docs: RULE_DOCS["b2-known-fixture-marker"],
|
|
1100
|
-
messages: { unknownMarker: "Unknown fixture marker \"{{marker}}\" — known markers: {{known}}. A path without a marker is feature-local (
|
|
1118
|
+
messages: { unknownMarker: "Unknown fixture marker \"{{marker}}\" — known markers: {{known}}. A path without a marker is feature-local (B2 — see docs/10-linting.md)." },
|
|
1101
1119
|
type: "problem"
|
|
1102
1120
|
}
|
|
1103
1121
|
};
|
|
@@ -1162,8 +1180,8 @@ const b4GivenThen = {
|
|
|
1162
1180
|
meta: {
|
|
1163
1181
|
docs: RULE_DOCS["b4-given-then"],
|
|
1164
1182
|
messages: {
|
|
1165
|
-
givenAfterThen: "The `// Given -` marker comes after `// Then -` — Given describes the setup and must precede Then (
|
|
1166
|
-
missing: "Test is missing a `// {{marker}} -` comment (
|
|
1183
|
+
givenAfterThen: "The `// Given -` marker comes after `// Then -` — Given describes the setup and must precede Then (B4 — see docs/10-linting.md).",
|
|
1184
|
+
missing: "Test is missing a `// {{marker}} -` comment (B4 — see docs/10-linting.md — every test needs both `// Given -` and `// Then -`)."
|
|
1167
1185
|
},
|
|
1168
1186
|
type: "problem"
|
|
1169
1187
|
}
|
|
@@ -1204,7 +1222,7 @@ const b5AwaitUsing = {
|
|
|
1204
1222
|
meta: {
|
|
1205
1223
|
docs: RULE_DOCS["b5-await-using"],
|
|
1206
1224
|
defaultOptions: [{ runners: [] }],
|
|
1207
|
-
messages: { requireAwaitUsing: "The result of docker-aware runner \"{{runner}}\" must be bound with `await using` so its containers are disposed (
|
|
1225
|
+
messages: { requireAwaitUsing: "The result of docker-aware runner \"{{runner}}\" must be bound with `await using` so its containers are disposed (B5 — see docs/10-linting.md)." },
|
|
1208
1226
|
schema: [{
|
|
1209
1227
|
additionalProperties: false,
|
|
1210
1228
|
properties: { runners: {
|
|
@@ -1247,7 +1265,7 @@ const b6wRedundantEnvUrl = {
|
|
|
1247
1265
|
},
|
|
1248
1266
|
meta: {
|
|
1249
1267
|
docs: RULE_DOCS["b6w-redundant-env-url"],
|
|
1250
|
-
messages: { redundant: ".env({ {{key}}: ….connectionString }) is redundant — the framework already injects <KEY>_URL (and the DATABASE_URL/REDIS_URL aliases) for every service (
|
|
1268
|
+
messages: { redundant: ".env({ {{key}}: ….connectionString }) is redundant — the framework already injects <KEY>_URL (and the DATABASE_URL/REDIS_URL aliases) for every service (B6 — see docs/10-linting.md)." },
|
|
1251
1269
|
type: "suggestion"
|
|
1252
1270
|
}
|
|
1253
1271
|
};
|
|
@@ -1276,7 +1294,7 @@ const b8KebabTrigger = {
|
|
|
1276
1294
|
},
|
|
1277
1295
|
meta: {
|
|
1278
1296
|
docs: RULE_DOCS["b8-kebab-trigger"],
|
|
1279
|
-
messages: { notKebab: "Job name \"{{name}}\" must be a stable kebab-case identifier (e.g. \"nightly-report\") — it is a contract between the app and its tests (
|
|
1297
|
+
messages: { notKebab: "Job name \"{{name}}\" must be a stable kebab-case identifier (e.g. \"nightly-report\") — it is a contract between the app and its tests (B8 — see docs/10-linting.md)." },
|
|
1280
1298
|
type: "problem"
|
|
1281
1299
|
}
|
|
1282
1300
|
};
|
|
@@ -1331,7 +1349,7 @@ const b9wProductCommand = {
|
|
|
1331
1349
|
},
|
|
1332
1350
|
meta: {
|
|
1333
1351
|
docs: RULE_DOCS["b9w-product-command"],
|
|
1334
|
-
messages: { thirdPartyBinary: "specification.cli() points at a third-party binary in node_modules/.bin — a spec should exercise your product command (cli.exec(\"check\"), …), not the tool underneath (
|
|
1352
|
+
messages: { thirdPartyBinary: "specification.cli() points at a third-party binary in node_modules/.bin — a spec should exercise your product command (cli.exec(\"check\"), …), not the tool underneath (B9 — see docs/10-linting.md). Suppress with a reason if the product genuinely is this binary." },
|
|
1335
1353
|
type: "suggestion"
|
|
1336
1354
|
}
|
|
1337
1355
|
};
|
|
@@ -1387,9 +1405,9 @@ const c1DomainStructure = {
|
|
|
1387
1405
|
meta: {
|
|
1388
1406
|
docs: RULE_DOCS["c1-domain-structure"],
|
|
1389
1407
|
messages: {
|
|
1390
|
-
specNotAtFacetRoot: "A `*.specification.ts` must sit at the facet root: `specs/<facet>/<name>.specification.ts` (
|
|
1391
|
-
testAtFacetRoot: "A `*.test.ts` must live in a domain folder: `specs/<facet>/<domain>/<aspect>.test.ts` — tests directly at the facet root are forbidden (
|
|
1392
|
-
testTooDeep: "A `*.test.ts` must sit at facet/domain depth: `specs/<facet>/<domain>/<aspect>.test.ts` — no deeper nesting (
|
|
1408
|
+
specNotAtFacetRoot: "A `*.specification.ts` must sit at the facet root: `specs/<facet>/<name>.specification.ts` (C1 — see docs/10-linting.md).",
|
|
1409
|
+
testAtFacetRoot: "A `*.test.ts` must live in a domain folder: `specs/<facet>/<domain>/<aspect>.test.ts` — tests directly at the facet root are forbidden (C1 — see docs/10-linting.md).",
|
|
1410
|
+
testTooDeep: "A `*.test.ts` must sit at facet/domain depth: `specs/<facet>/<domain>/<aspect>.test.ts` — no deeper nesting (C1 — see docs/10-linting.md)."
|
|
1393
1411
|
},
|
|
1394
1412
|
type: "problem"
|
|
1395
1413
|
}
|
|
@@ -1419,13 +1437,13 @@ const c2HttpOnlyRequests = {
|
|
|
1419
1437
|
},
|
|
1420
1438
|
meta: {
|
|
1421
1439
|
docs: RULE_DOCS["c2-http-only-requests"],
|
|
1422
|
-
messages: { notHttp: "requests/{{entry}} is not a .http file — requests/ contains only complete .http request files (
|
|
1440
|
+
messages: { notHttp: "requests/{{entry}} is not a .http file — requests/ contains only complete .http request files (C2 — see docs/10-linting.md)." },
|
|
1423
1441
|
type: "problem"
|
|
1424
1442
|
}
|
|
1425
1443
|
};
|
|
1426
1444
|
//#endregion
|
|
1427
1445
|
//#region src/lint/rules/c4-contract-shape.ts
|
|
1428
|
-
/** Providers a contract file may declare (
|
|
1446
|
+
/** Providers a contract file may declare (C4 — see docs/10-linting.md). */
|
|
1429
1447
|
const PROVIDERS = /* @__PURE__ */ new Set([
|
|
1430
1448
|
"anthropic",
|
|
1431
1449
|
"http",
|
|
@@ -1491,12 +1509,12 @@ const c4ContractShape = {
|
|
|
1491
1509
|
meta: {
|
|
1492
1510
|
docs: RULE_DOCS["c4-contract-shape"],
|
|
1493
1511
|
messages: {
|
|
1494
|
-
badName: "Contract file \"{{base}}\" must be named <name>.<provider>.ts with provider ∈ openai | anthropic | http (
|
|
1495
|
-
foreignImport: "Contract imports \"{{source}}\" — a contract imports only from the public entry (@jterrazz/test) (
|
|
1496
|
-
missingDefault: "Contract file has no `export default defineContract(...)` (
|
|
1497
|
-
namedExport: "Contract files have no named exports — only `export default defineContract(...)` (
|
|
1498
|
-
notDefineContract: "The default export of a contract file must be `defineContract(...)` syntactically (
|
|
1499
|
-
subfolder: "Contract file \"{{base}}\" is nested — contracts/ is flat, no subfolders (
|
|
1512
|
+
badName: "Contract file \"{{base}}\" must be named <name>.<provider>.ts with provider ∈ openai | anthropic | http (C4 — see docs/10-linting.md).",
|
|
1513
|
+
foreignImport: "Contract imports \"{{source}}\" — a contract imports only from the public entry (@jterrazz/test) (C4 — see docs/10-linting.md).",
|
|
1514
|
+
missingDefault: "Contract file has no `export default defineContract(...)` (C4 — see docs/10-linting.md).",
|
|
1515
|
+
namedExport: "Contract files have no named exports — only `export default defineContract(...)` (C4 — see docs/10-linting.md).",
|
|
1516
|
+
notDefineContract: "The default export of a contract file must be `defineContract(...)` syntactically (C4 — see docs/10-linting.md).",
|
|
1517
|
+
subfolder: "Contract file \"{{base}}\" is nested — contracts/ is flat, no subfolders (C4 — see docs/10-linting.md)."
|
|
1500
1518
|
},
|
|
1501
1519
|
type: "problem"
|
|
1502
1520
|
}
|
|
@@ -1547,7 +1565,7 @@ const c6ToMatchExtension = {
|
|
|
1547
1565
|
},
|
|
1548
1566
|
meta: {
|
|
1549
1567
|
docs: RULE_DOCS["c6-tomatch-extension"],
|
|
1550
|
-
messages: { missingExtension: "toMatch(\"{{name}}\") is missing its extension — the extension is part of the fixture name (\"help.txt\", never \"help\"); only directory-tree snapshots (expected/<name>/) omit it (
|
|
1568
|
+
messages: { missingExtension: "toMatch(\"{{name}}\") is missing its extension — the extension is part of the fixture name (\"help.txt\", never \"help\"); only directory-tree snapshots (expected/<name>/) omit it (C6 — see docs/10-linting.md)." },
|
|
1551
1569
|
type: "problem"
|
|
1552
1570
|
}
|
|
1553
1571
|
};
|
|
@@ -1575,7 +1593,7 @@ const c7SeedsSqlOnly = {
|
|
|
1575
1593
|
},
|
|
1576
1594
|
meta: {
|
|
1577
1595
|
docs: RULE_DOCS["c7-seeds-sql-only"],
|
|
1578
|
-
messages: { notSql: "seeds/{{entry}} is not a .sql file — seeds/ carries database state only; file state goes through .fixture() (
|
|
1596
|
+
messages: { notSql: "seeds/{{entry}} is not a .sql file — seeds/ carries database state only; file state goes through .fixture() (C7 — see docs/10-linting.md)." },
|
|
1579
1597
|
type: "problem"
|
|
1580
1598
|
}
|
|
1581
1599
|
};
|
|
@@ -1664,7 +1682,7 @@ const c8ReferencedFixtureExists = {
|
|
|
1664
1682
|
},
|
|
1665
1683
|
meta: {
|
|
1666
1684
|
docs: RULE_DOCS["c8-referenced-fixture-exists"],
|
|
1667
|
-
messages: { missing: "Referenced fixture \"{{path}}\" does not exist on disk under its conventional {{root}}/ root (
|
|
1685
|
+
messages: { missing: "Referenced fixture \"{{path}}\" does not exist on disk under its conventional {{root}}/ root (C8 — see docs/10-linting.md). Create it or fix the reference." },
|
|
1668
1686
|
type: "problem"
|
|
1669
1687
|
}
|
|
1670
1688
|
};
|
|
@@ -1733,7 +1751,7 @@ const d12wResponseBodyProbe = {
|
|
|
1733
1751
|
meta: {
|
|
1734
1752
|
docs: RULE_DOCS["d12w-response-body-probe"],
|
|
1735
1753
|
defaultOptions: [{ threshold: DEFAULT_THRESHOLD }],
|
|
1736
|
-
messages: { bodyProbeCluster: "{{count}} body probes in one test — this wants a full golden: expect(result.response).toMatch('x.http') (
|
|
1754
|
+
messages: { bodyProbeCluster: "{{count}} body probes in one test — this wants a full golden: expect(result.response).toMatch('x.http') (D12 — see docs/10-linting.md)." },
|
|
1737
1755
|
schema: [{
|
|
1738
1756
|
additionalProperties: false,
|
|
1739
1757
|
properties: { threshold: {
|
|
@@ -1816,7 +1834,7 @@ const d13wUnfrozenNegativeFixture = {
|
|
|
1816
1834
|
},
|
|
1817
1835
|
meta: {
|
|
1818
1836
|
docs: RULE_DOCS["d13w-unfrozen-negative-fixture"],
|
|
1819
|
-
messages: { unfrozenNegativeFixture: "This toMatch asserts a mismatch (wrapped in expect(…).toThrow/.rejects) — pass { frozen: true } so TEST_UPDATE=1 never overwrites the deliberately-wrong fixture (
|
|
1837
|
+
messages: { unfrozenNegativeFixture: "This toMatch asserts a mismatch (wrapped in expect(…).toThrow/.rejects) — pass { frozen: true } so TEST_UPDATE=1 never overwrites the deliberately-wrong fixture (D13 — see docs/10-linting.md)." },
|
|
1820
1838
|
type: "suggestion"
|
|
1821
1839
|
}
|
|
1822
1840
|
};
|
|
@@ -1892,7 +1910,7 @@ const d15wStatusOnlyProbe = {
|
|
|
1892
1910
|
},
|
|
1893
1911
|
meta: {
|
|
1894
1912
|
docs: RULE_DOCS["d15w-status-only-probe"],
|
|
1895
|
-
messages: { statusOnlyProbe: "The test's only assertion(s) are status probes — pin the full response: expect(result.response).toMatch('case.http') (
|
|
1913
|
+
messages: { statusOnlyProbe: "The test's only assertion(s) are status probes — pin the full response: expect(result.response).toMatch('case.http') (D11 — see docs/10-linting.md)." },
|
|
1896
1914
|
type: "suggestion"
|
|
1897
1915
|
}
|
|
1898
1916
|
};
|
|
@@ -1947,7 +1965,7 @@ const d2AwaitIoMatcher = {
|
|
|
1947
1965
|
},
|
|
1948
1966
|
meta: {
|
|
1949
1967
|
docs: RULE_DOCS["d2-await-io-matcher"],
|
|
1950
|
-
messages: { mustAwait: "expect(…).{{matcher}}(…) is IO-backed and returns a promise — it must be awaited or returned, else the assertion never runs (
|
|
1968
|
+
messages: { mustAwait: "expect(…).{{matcher}}(…) is IO-backed and returns a promise — it must be awaited or returned, else the assertion never runs (D2 — see docs/10-linting.md)." },
|
|
1951
1969
|
type: "problem"
|
|
1952
1970
|
}
|
|
1953
1971
|
};
|
|
@@ -2001,7 +2019,7 @@ const d2wAwaitSyncMatcher = {
|
|
|
2001
2019
|
},
|
|
2002
2020
|
meta: {
|
|
2003
2021
|
docs: RULE_DOCS["d2w-await-sync-matcher"],
|
|
2004
|
-
messages: { redundantAwait: "await on expect(…).{{matcher}}(…) is redundant — that matcher is synchronous; only IO-backed matchers (toMatchRows/toBeEmpty/toBeRunning) are awaited (
|
|
2022
|
+
messages: { redundantAwait: "await on expect(…).{{matcher}}(…) is redundant — that matcher is synchronous; only IO-backed matchers (toMatchRows/toBeEmpty/toBeRunning) are awaited (D2 — see docs/10-linting.md)." },
|
|
2005
2023
|
type: "suggestion"
|
|
2006
2024
|
}
|
|
2007
2025
|
};
|
|
@@ -2078,7 +2096,7 @@ const d6wTransformTokenEquivalent = {
|
|
|
2078
2096
|
},
|
|
2079
2097
|
meta: {
|
|
2080
2098
|
docs: RULE_DOCS["d6w-transform-token-equivalent"],
|
|
2081
|
-
messages: { tokenEquivalent: "This transform only rewrites output into known token literals — write the tokens in the expected/ fixture instead; transform is an escape hatch for uncovered noise (
|
|
2099
|
+
messages: { tokenEquivalent: "This transform only rewrites output into known token literals — write the tokens in the expected/ fixture instead; transform is an escape hatch for uncovered noise (D6 — see docs/10-linting.md)." },
|
|
2082
2100
|
type: "suggestion"
|
|
2083
2101
|
}
|
|
2084
2102
|
};
|
|
@@ -2112,7 +2130,7 @@ const d8wTextBypass = {
|
|
|
2112
2130
|
},
|
|
2113
2131
|
meta: {
|
|
2114
2132
|
docs: RULE_DOCS["d8w-text-bypass"],
|
|
2115
|
-
messages: { textBypass: "expect(x.text).{{matcher}}(…) asserts on the raw stream — prefer the typed accessor subject (expect(x)) so the token grammar and fixture resolution apply (
|
|
2133
|
+
messages: { textBypass: "expect(x.text).{{matcher}}(…) asserts on the raw stream — prefer the typed accessor subject (expect(x)) so the token grammar and fixture resolution apply (D8 — see docs/10-linting.md)." },
|
|
2116
2134
|
type: "suggestion"
|
|
2117
2135
|
}
|
|
2118
2136
|
};
|
|
@@ -2195,7 +2213,7 @@ const d9wSingleUseRef = {
|
|
|
2195
2213
|
},
|
|
2196
2214
|
meta: {
|
|
2197
2215
|
docs: RULE_DOCS["d9w-single-use-ref"],
|
|
2198
|
-
messages: { singleUse: "Capture ref \"{{ref}}\" is used only once in this spec — a ref earns its name by asserting equality across occurrences; use a plain matcher instead (
|
|
2216
|
+
messages: { singleUse: "Capture ref \"{{ref}}\" is used only once in this spec — a ref earns its name by asserting equality across occurrences; use a plain matcher instead (D9 — see docs/10-linting.md)." },
|
|
2199
2217
|
type: "suggestion"
|
|
2200
2218
|
}
|
|
2201
2219
|
};
|
|
@@ -2223,7 +2241,7 @@ const f1NoSubpathImport = {
|
|
|
2223
2241
|
},
|
|
2224
2242
|
meta: {
|
|
2225
2243
|
docs: RULE_DOCS["f1-no-subpath-import"],
|
|
2226
|
-
messages: { subpath: "Import from \"@jterrazz/test\", not \"{{source}}\" — subpaths do not exist (
|
|
2244
|
+
messages: { subpath: "Import from \"@jterrazz/test\", not \"{{source}}\" — subpaths do not exist (F1 — see docs/10-linting.md; only the tool-facing @jterrazz/test/oxlint is exempt)." },
|
|
2227
2245
|
type: "problem"
|
|
2228
2246
|
}
|
|
2229
2247
|
};
|
|
@@ -2271,7 +2289,7 @@ const f2NoTestImportsInProd = {
|
|
|
2271
2289
|
},
|
|
2272
2290
|
meta: {
|
|
2273
2291
|
docs: RULE_DOCS["f2-no-test-imports-in-prod"],
|
|
2274
|
-
messages: { testImport: "Production code must not import the test artefact \"{{source}}\" — test imports are only legal from specs/, *.test.ts, *.fixtures.ts or *.specification.ts (
|
|
2292
|
+
messages: { testImport: "Production code must not import the test artefact \"{{source}}\" — test imports are only legal from specs/, *.test.ts, *.fixtures.ts or *.specification.ts (F2 — see docs/10-linting.md)." },
|
|
2275
2293
|
type: "problem"
|
|
2276
2294
|
}
|
|
2277
2295
|
};
|
|
@@ -2335,7 +2353,7 @@ const f3SpecsPublicEntry = {
|
|
|
2335
2353
|
},
|
|
2336
2354
|
meta: {
|
|
2337
2355
|
docs: RULE_DOCS["f3-specs-public-entry"],
|
|
2338
|
-
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}}\" (
|
|
2356
|
+
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/10-linting.md; only @jterrazz/test/oxlint, and src/integrations/** from specs/integrations/, are exempt)." },
|
|
2339
2357
|
type: "problem"
|
|
2340
2358
|
}
|
|
2341
2359
|
};
|
|
@@ -2361,7 +2379,7 @@ const f4NoTestToTestImport = {
|
|
|
2361
2379
|
},
|
|
2362
2380
|
meta: {
|
|
2363
2381
|
docs: RULE_DOCS["f4-no-test-to-test-import"],
|
|
2364
|
-
messages: { testToTest: "A test file must not import another test file (\"{{source}}\") — share data via a *.fixtures.ts neighbour or a *.specification.ts runner (
|
|
2382
|
+
messages: { testToTest: "A test file must not import another test file (\"{{source}}\") — share data via a *.fixtures.ts neighbour or a *.specification.ts runner (F4 — see docs/10-linting.md)." },
|
|
2365
2383
|
type: "problem"
|
|
2366
2384
|
}
|
|
2367
2385
|
};
|
|
@@ -2387,14 +2405,14 @@ const f5FixturesOnlyFromTests = {
|
|
|
2387
2405
|
},
|
|
2388
2406
|
meta: {
|
|
2389
2407
|
docs: RULE_DOCS["f5-fixtures-only-from-tests"],
|
|
2390
|
-
messages: { fixturesImport: "A *.fixtures.ts module (\"{{source}}\") is only importable from *.test.ts files (
|
|
2408
|
+
messages: { fixturesImport: "A *.fixtures.ts module (\"{{source}}\") is only importable from *.test.ts files (F5 — see docs/10-linting.md)." },
|
|
2391
2409
|
type: "problem"
|
|
2392
2410
|
}
|
|
2393
2411
|
};
|
|
2394
2412
|
//#endregion
|
|
2395
2413
|
//#region src/lint/rules/i1-layer-boundaries.ts
|
|
2396
2414
|
/**
|
|
2397
|
-
* One folder = one external dependency (
|
|
2415
|
+
* One folder = one external dependency (I1 — see docs/10-linting.md): the packages each
|
|
2398
2416
|
* `src/integrations/<folder>/` may import.
|
|
2399
2417
|
*/
|
|
2400
2418
|
const INTEGRATION_DEPS = {
|
|
@@ -2404,6 +2422,7 @@ const INTEGRATION_DEPS = {
|
|
|
2404
2422
|
hono: ["hono", "@hono/node-server"],
|
|
2405
2423
|
msw: ["msw"],
|
|
2406
2424
|
openai: ["openai"],
|
|
2425
|
+
playwright: ["playwright"],
|
|
2407
2426
|
postgres: ["pg"],
|
|
2408
2427
|
redis: ["redis"],
|
|
2409
2428
|
sqlite: ["better-sqlite3"],
|
|
@@ -2474,6 +2493,7 @@ const i1LayerBoundaries = {
|
|
|
2474
2493
|
if (layer === "core") {
|
|
2475
2494
|
if (target.startsWith("core/") || target.startsWith("integrations/docker/") || target.startsWith("integrations/hono/") || withoutExtension(target) === "vitest/matchers") return null;
|
|
2476
2495
|
if (target.startsWith("integrations/msw/") && inside === "core/specification/shared/builder.ts") return null;
|
|
2496
|
+
if (target.startsWith("integrations/playwright/") && inside === "core/specification/website/start-website.ts") return null;
|
|
2477
2497
|
return "crossLayer";
|
|
2478
2498
|
}
|
|
2479
2499
|
if (layer === "integrations") return target.startsWith("core/") || target.startsWith(`integrations/${integrationFolder}/`) ? null : "crossLayer";
|
|
@@ -2500,10 +2520,10 @@ const i1LayerBoundaries = {
|
|
|
2500
2520
|
meta: {
|
|
2501
2521
|
docs: RULE_DOCS["i1-layer-boundaries"],
|
|
2502
2522
|
messages: {
|
|
2503
|
-
coreExternal: "core/ imports nothing external — \"{{source}}\" is not allowed (
|
|
2504
|
-
crossLayer: "Layer \"{{layer}}\" must not import \"{{source}}\" — outside the sanctioned layer edges (
|
|
2505
|
-
foreignDependency: "\"{{source}}\" is not this folder's own dependency — one integrations folder = one external dependency (
|
|
2506
|
-
lintRuntime: "The lint layer imports nothing from the framework runtime — \"{{source}}\" is outside its pure-helper whitelist (
|
|
2523
|
+
coreExternal: "core/ imports nothing external — \"{{source}}\" is not allowed (I1 — see docs/10-linting.md).",
|
|
2524
|
+
crossLayer: "Layer \"{{layer}}\" must not import \"{{source}}\" — outside the sanctioned layer edges (I1 — see docs/10-linting.md).",
|
|
2525
|
+
foreignDependency: "\"{{source}}\" is not this folder's own dependency — one integrations folder = one external dependency (I1 — see docs/10-linting.md).",
|
|
2526
|
+
lintRuntime: "The lint layer imports nothing from the framework runtime — \"{{source}}\" is outside its pure-helper whitelist (I1 — see docs/10-linting.md)."
|
|
2507
2527
|
},
|
|
2508
2528
|
type: "problem"
|
|
2509
2529
|
}
|
|
@@ -2556,9 +2576,9 @@ const i2SiblingTestNaming = {
|
|
|
2556
2576
|
meta: {
|
|
2557
2577
|
docs: RULE_DOCS["i2-sibling-test-naming"],
|
|
2558
2578
|
messages: {
|
|
2559
|
-
orphanTest: "A src/ module test must sit NEXT to the module it tests — no neighbour \"{{expected}}\" found (
|
|
2560
|
-
rootTests: "A root-level tests/ directory is banned — module tests are siblings under src/, product specifications live in specs/ (
|
|
2561
|
-
testsDir: "__tests__/ directories are banned under src/ — the test of <file>.ts is its neighbour <file>.test.ts (
|
|
2579
|
+
orphanTest: "A src/ module test must sit NEXT to the module it tests — no neighbour \"{{expected}}\" found (I2 — see docs/10-linting.md). A test needing more than its module is a specification: move it to specs/.",
|
|
2580
|
+
rootTests: "A root-level tests/ directory is banned — module tests are siblings under src/, product specifications live in specs/ (I2 — see docs/10-linting.md).",
|
|
2581
|
+
testsDir: "__tests__/ directories are banned under src/ — the test of <file>.ts is its neighbour <file>.test.ts (I2 — see docs/10-linting.md)."
|
|
2562
2582
|
},
|
|
2563
2583
|
type: "problem"
|
|
2564
2584
|
}
|
|
@@ -2617,9 +2637,9 @@ const i4NoViMockInSrc = {
|
|
|
2617
2637
|
meta: {
|
|
2618
2638
|
docs: RULE_DOCS["i4-no-vi-mock-in-src"],
|
|
2619
2639
|
messages: {
|
|
2620
|
-
assetImport: "A src/ module test must not import the data asset \"{{source}}\" — inline it as code or move the test to specs/ (
|
|
2621
|
-
bannedDir: "`{{dir}}/` directories are banned under src/ — mocks and data are code: mockOf/mockOfDate inline, payloads in a *.fixtures.ts neighbour (
|
|
2622
|
-
viMock: "`vi.mock` is banned under src/ — use mockOf/mockOfDate (
|
|
2640
|
+
assetImport: "A src/ module test must not import the data asset \"{{source}}\" — inline it as code or move the test to specs/ (I4 — see docs/10-linting.md).",
|
|
2641
|
+
bannedDir: "`{{dir}}/` directories are banned under src/ — mocks and data are code: mockOf/mockOfDate inline, payloads in a *.fixtures.ts neighbour (I4 — see docs/10-linting.md).",
|
|
2642
|
+
viMock: "`vi.mock` is banned under src/ — use mockOf/mockOfDate (I4 — see docs/10-linting.md)."
|
|
2623
2643
|
},
|
|
2624
2644
|
type: "problem"
|
|
2625
2645
|
}
|
|
@@ -2663,7 +2683,7 @@ const j1NoOnlySkip = {
|
|
|
2663
2683
|
},
|
|
2664
2684
|
meta: {
|
|
2665
2685
|
docs: RULE_DOCS["j1-no-only-skip"],
|
|
2666
|
-
messages: { forbidden: "Committed `{{runner}}.{{modifier}}` is not allowed (
|
|
2686
|
+
messages: { forbidden: "Committed `{{runner}}.{{modifier}}` is not allowed (J1 — see docs/10-linting.md). Remove it before committing." },
|
|
2667
2687
|
type: "problem"
|
|
2668
2688
|
}
|
|
2669
2689
|
};
|
|
@@ -2706,8 +2726,8 @@ const j2NoSleepInSpecs = {
|
|
|
2706
2726
|
meta: {
|
|
2707
2727
|
docs: RULE_DOCS["j2-no-sleep-in-specs"],
|
|
2708
2728
|
messages: {
|
|
2709
|
-
sleep: "No arbitrary sleeps in specs — synchronise via `waitFor` or the framework (
|
|
2710
|
-
timersImport: "No timer-based sleeps in specs — synchronise via `waitFor` or the framework (
|
|
2729
|
+
sleep: "No arbitrary sleeps in specs — synchronise via `waitFor` or the framework (J2 — see docs/10-linting.md).",
|
|
2730
|
+
timersImport: "No timer-based sleeps in specs — synchronise via `waitFor` or the framework (J2 — see docs/10-linting.md)."
|
|
2711
2731
|
},
|
|
2712
2732
|
type: "problem"
|
|
2713
2733
|
}
|
|
@@ -2747,7 +2767,7 @@ const j3NoExpectlessTest = {
|
|
|
2747
2767
|
},
|
|
2748
2768
|
meta: {
|
|
2749
2769
|
docs: RULE_DOCS["j3-no-expectless-test"],
|
|
2750
|
-
messages: { noExpect: "Test has no `expect(…)` — a test must assert something (
|
|
2770
|
+
messages: { noExpect: "Test has no `expect(…)` — a test must assert something (J3 — see docs/10-linting.md). Use `test.todo` for a pending test." },
|
|
2751
2771
|
type: "problem"
|
|
2752
2772
|
}
|
|
2753
2773
|
};
|
|
@@ -2783,7 +2803,7 @@ const j4UniqueTestNames = {
|
|
|
2783
2803
|
},
|
|
2784
2804
|
meta: {
|
|
2785
2805
|
docs: RULE_DOCS["j4-unique-test-names"],
|
|
2786
|
-
messages: { duplicate: "Duplicate test name \"{{name}}\" in this file — the test name is the sole description of behaviour, so it must be unique (
|
|
2806
|
+
messages: { duplicate: "Duplicate test name \"{{name}}\" in this file — the test name is the sole description of behaviour, so it must be unique (J4 — see docs/10-linting.md)." },
|
|
2787
2807
|
type: "problem"
|
|
2788
2808
|
}
|
|
2789
2809
|
};
|
|
@@ -2879,9 +2899,55 @@ const plugin = {
|
|
|
2879
2899
|
},
|
|
2880
2900
|
meta: {
|
|
2881
2901
|
docs: RULE_DOCS["j5-lowercase-title"],
|
|
2882
|
-
messages: { uppercase: "A {{runner}}() title must start lowercase — the test name is a prose fragment, not a sentence (
|
|
2902
|
+
messages: { uppercase: "A {{runner}}() title must start lowercase — the test name is a prose fragment, not a sentence (J5 — see docs/10-linting.md)." },
|
|
2883
2903
|
type: "problem"
|
|
2884
2904
|
}
|
|
2905
|
+
},
|
|
2906
|
+
"w1-scenario-pure": {
|
|
2907
|
+
create(context) {
|
|
2908
|
+
return { CallExpression(node) {
|
|
2909
|
+
const callee = node.callee;
|
|
2910
|
+
if (!callee || memberPropertyName(callee) !== "visit") return;
|
|
2911
|
+
const scenario = node.arguments?.[1];
|
|
2912
|
+
if (scenario?.type !== "ArrowFunctionExpression" && scenario?.type !== "FunctionExpression") return;
|
|
2913
|
+
walk(scenario, (inner) => {
|
|
2914
|
+
if (inner.type !== "CallExpression") return;
|
|
2915
|
+
const innerCallee = inner.callee;
|
|
2916
|
+
if (innerCallee?.type === "Identifier" && innerCallee.name === "expect") context.report({
|
|
2917
|
+
messageId: "expectInScenario",
|
|
2918
|
+
node: inner
|
|
2919
|
+
});
|
|
2920
|
+
});
|
|
2921
|
+
} };
|
|
2922
|
+
},
|
|
2923
|
+
meta: {
|
|
2924
|
+
docs: RULE_DOCS["w1-scenario-pure"],
|
|
2925
|
+
messages: { expectInScenario: "No expect() inside a visit scenario — the scenario is the When; assert the final state on the returned result in the Then (W1 — see docs/10-linting.md)." },
|
|
2926
|
+
type: "problem"
|
|
2927
|
+
}
|
|
2928
|
+
},
|
|
2929
|
+
"w2w-user-facing-elements": {
|
|
2930
|
+
create(context) {
|
|
2931
|
+
return { CallExpression(node) {
|
|
2932
|
+
const callee = node.callee;
|
|
2933
|
+
if (!callee || memberPropertyName(callee) !== "visit") return;
|
|
2934
|
+
const scenario = node.arguments?.[1];
|
|
2935
|
+
if (scenario?.type !== "ArrowFunctionExpression" && scenario?.type !== "FunctionExpression") return;
|
|
2936
|
+
walk(scenario, (inner) => {
|
|
2937
|
+
if (inner.type !== "CallExpression") return;
|
|
2938
|
+
const innerCallee = inner.callee;
|
|
2939
|
+
if (innerCallee?.type === "Identifier" && innerCallee.name === "testId") context.report({
|
|
2940
|
+
messageId: "testIdElement",
|
|
2941
|
+
node: inner
|
|
2942
|
+
});
|
|
2943
|
+
});
|
|
2944
|
+
} };
|
|
2945
|
+
},
|
|
2946
|
+
meta: {
|
|
2947
|
+
docs: RULE_DOCS["w2w-user-facing-elements"],
|
|
2948
|
+
messages: { testIdElement: "Prefer a user-facing element (button, link, field, heading, content) over testId() — the escape hatch hides what the user actually sees (W2 — see docs/10-linting.md)." },
|
|
2949
|
+
type: "suggestion"
|
|
2950
|
+
}
|
|
2885
2951
|
}
|
|
2886
2952
|
}
|
|
2887
2953
|
};
|