@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.cjs
CHANGED
|
@@ -166,7 +166,7 @@ const RULE_DOCS = {
|
|
|
166
166
|
},
|
|
167
167
|
"a2-known-constructors": {
|
|
168
168
|
channel: "statique",
|
|
169
|
-
convention: "
|
|
169
|
+
convention: "Quatre constructeurs et seulement quatre : `specification.api()`, `specification.jobs()`, `specification.cli(bin)`, `specification.website()` ; tout autre membre (`.app`, `.http`, `.stack`…) est une erreur.",
|
|
170
170
|
family: "A",
|
|
171
171
|
id: "A2",
|
|
172
172
|
rationale: "Une surface fermée empêche l’invention de constructeurs parallèles et garde l’API mémorisable."
|
|
@@ -436,6 +436,22 @@ const RULE_DOCS = {
|
|
|
436
436
|
family: "J",
|
|
437
437
|
id: "J5",
|
|
438
438
|
rationale: "Un titre est un fragment de prose, pas une phrase — la casse minuscule le garde fragmentaire ; minusculiser un symbole nommé le mal-orthographierait."
|
|
439
|
+
},
|
|
440
|
+
"w1-scenario-pure": {
|
|
441
|
+
channel: "statique",
|
|
442
|
+
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é.",
|
|
443
|
+
facet: "website",
|
|
444
|
+
family: "W",
|
|
445
|
+
id: "W1",
|
|
446
|
+
rationale: "Séparer l’interaction de l’assertion garde la grammaire setup → action → résultat intacte et les scénarios rejouables."
|
|
447
|
+
},
|
|
448
|
+
"w2w-user-facing-elements": {
|
|
449
|
+
channel: "statique",
|
|
450
|
+
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.",
|
|
451
|
+
facet: "website",
|
|
452
|
+
family: "W",
|
|
453
|
+
id: "W2",
|
|
454
|
+
rationale: "Tester ce que l’utilisateur voit (rôles, labels) rend les specs robustes aux refontes DOM ; un test-id contourne cette garantie."
|
|
439
455
|
}
|
|
440
456
|
};
|
|
441
457
|
/**
|
|
@@ -657,7 +673,7 @@ const a1SpecificationFile = {
|
|
|
657
673
|
},
|
|
658
674
|
meta: {
|
|
659
675
|
docs: RULE_DOCS["a1-specification-file"],
|
|
660
|
-
messages: { outsideSpecification: "specification.{{member}}() must be called from a `*.specification.ts` file under specs/ (
|
|
676
|
+
messages: { outsideSpecification: "specification.{{member}}() must be called from a `*.specification.ts` file under specs/ (A1 — see docs/10-linting.md)." },
|
|
661
677
|
type: "problem"
|
|
662
678
|
}
|
|
663
679
|
};
|
|
@@ -734,22 +750,24 @@ const a10DuplicateBinding = {
|
|
|
734
750
|
},
|
|
735
751
|
meta: {
|
|
736
752
|
docs: RULE_DOCS["a10-duplicate-binding"],
|
|
737
|
-
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 (
|
|
753
|
+
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)." },
|
|
738
754
|
type: "problem"
|
|
739
755
|
}
|
|
740
756
|
};
|
|
741
757
|
//#endregion
|
|
742
758
|
//#region src/lint/rules/a2-known-constructors.ts
|
|
743
|
-
/** The
|
|
759
|
+
/** The four constructors, and only four (A2 — see docs/10-linting.md). */
|
|
744
760
|
const KNOWN_CONSTRUCTORS = /* @__PURE__ */ new Set([
|
|
745
761
|
"api",
|
|
746
762
|
"cli",
|
|
747
|
-
"jobs"
|
|
763
|
+
"jobs",
|
|
764
|
+
"website"
|
|
748
765
|
]);
|
|
749
766
|
/**
|
|
750
|
-
* CONVENTIONS A2 — `specification.api()`, `specification.jobs()
|
|
751
|
-
* `specification.cli()` are the only members.
|
|
752
|
-
* (`specification.app`, `.http`, `.stack`, …) is flagged at
|
|
767
|
+
* CONVENTIONS A2 — `specification.api()`, `specification.jobs()`,
|
|
768
|
+
* `specification.cli()` and `specification.website()` are the only members.
|
|
769
|
+
* Any other access (`specification.app`, `.http`, `.stack`, …) is flagged at
|
|
770
|
+
* the member site.
|
|
753
771
|
*/
|
|
754
772
|
const a2KnownConstructors = {
|
|
755
773
|
create(context) {
|
|
@@ -766,7 +784,7 @@ const a2KnownConstructors = {
|
|
|
766
784
|
},
|
|
767
785
|
meta: {
|
|
768
786
|
docs: RULE_DOCS["a2-known-constructors"],
|
|
769
|
-
messages: { unknownConstructor: "specification.{{member}} does not exist — the only constructors are specification.api(), specification.jobs() and specification.
|
|
787
|
+
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)." },
|
|
770
788
|
type: "problem"
|
|
771
789
|
}
|
|
772
790
|
};
|
|
@@ -811,7 +829,7 @@ const a3NoDestructureAlias = {
|
|
|
811
829
|
},
|
|
812
830
|
meta: {
|
|
813
831
|
docs: RULE_DOCS["a3-no-destructure-alias"],
|
|
814
|
-
messages: { aliased: "Destructure the specification result with its canonical name: `{{key}}`, not `{{key}}: {{alias}}` (
|
|
832
|
+
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." },
|
|
815
833
|
type: "problem"
|
|
816
834
|
}
|
|
817
835
|
};
|
|
@@ -863,7 +881,7 @@ const a4CleanupAfterall = {
|
|
|
863
881
|
},
|
|
864
882
|
meta: {
|
|
865
883
|
docs: RULE_DOCS["a4-cleanup-afterall"],
|
|
866
|
-
messages: { unregistered: "`cleanup` is destructured but never passed to afterAll — add `afterAll(cleanup)` to the specification file (
|
|
884
|
+
messages: { unregistered: "`cleanup` is destructured but never passed to afterAll — add `afterAll(cleanup)` to the specification file (A4 — see docs/10-linting.md)." },
|
|
867
885
|
type: "problem"
|
|
868
886
|
}
|
|
869
887
|
};
|
|
@@ -893,7 +911,7 @@ const a5ModeWithServer = {
|
|
|
893
911
|
},
|
|
894
912
|
meta: {
|
|
895
913
|
docs: RULE_DOCS["a5-mode-with-server"],
|
|
896
|
-
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\" }` (
|
|
914
|
+
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)." },
|
|
897
915
|
type: "problem"
|
|
898
916
|
}
|
|
899
917
|
};
|
|
@@ -934,7 +952,7 @@ const a6wRedundantComposeService = {
|
|
|
934
952
|
},
|
|
935
953
|
meta: {
|
|
936
954
|
docs: RULE_DOCS["a6w-redundant-compose-service"],
|
|
937
|
-
messages: { redundant: "composeService: \"{{name}}\" is redundant — the key \"{{key}}\" already binds to it (exact name or kebab-case derivation,
|
|
955
|
+
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)." },
|
|
938
956
|
type: "suggestion"
|
|
939
957
|
}
|
|
940
958
|
};
|
|
@@ -998,7 +1016,7 @@ function readFileCached(path) {
|
|
|
998
1016
|
//#endregion
|
|
999
1017
|
//#region src/lint/rules/a9w-redundant-root.ts
|
|
1000
1018
|
/**
|
|
1001
|
-
* The root the convention would derive (
|
|
1019
|
+
* The root the convention would derive (A9 — see docs/10-linting.md): walk up from the
|
|
1002
1020
|
* specification file to the first directory containing
|
|
1003
1021
|
* `docker/compose.test.yaml`, else the first containing `package.json`.
|
|
1004
1022
|
*/
|
|
@@ -1037,7 +1055,7 @@ const a9wRedundantRoot = {
|
|
|
1037
1055
|
},
|
|
1038
1056
|
meta: {
|
|
1039
1057
|
docs: RULE_DOCS["a9w-redundant-root"],
|
|
1040
|
-
messages: { redundant: "root: \"{{root}}\" is redundant — walking up from the specification file already resolves to that directory (
|
|
1058
|
+
messages: { redundant: "root: \"{{root}}\" is redundant — walking up from the specification file already resolves to that directory (A9 — see docs/10-linting.md)." },
|
|
1041
1059
|
type: "suggestion"
|
|
1042
1060
|
}
|
|
1043
1061
|
};
|
|
@@ -1101,7 +1119,7 @@ const b2KnownFixtureMarker = {
|
|
|
1101
1119
|
},
|
|
1102
1120
|
meta: {
|
|
1103
1121
|
docs: RULE_DOCS["b2-known-fixture-marker"],
|
|
1104
|
-
messages: { unknownMarker: "Unknown fixture marker \"{{marker}}\" — known markers: {{known}}. A path without a marker is feature-local (
|
|
1122
|
+
messages: { unknownMarker: "Unknown fixture marker \"{{marker}}\" — known markers: {{known}}. A path without a marker is feature-local (B2 — see docs/10-linting.md)." },
|
|
1105
1123
|
type: "problem"
|
|
1106
1124
|
}
|
|
1107
1125
|
};
|
|
@@ -1166,8 +1184,8 @@ const b4GivenThen = {
|
|
|
1166
1184
|
meta: {
|
|
1167
1185
|
docs: RULE_DOCS["b4-given-then"],
|
|
1168
1186
|
messages: {
|
|
1169
|
-
givenAfterThen: "The `// Given -` marker comes after `// Then -` — Given describes the setup and must precede Then (
|
|
1170
|
-
missing: "Test is missing a `// {{marker}} -` comment (
|
|
1187
|
+
givenAfterThen: "The `// Given -` marker comes after `// Then -` — Given describes the setup and must precede Then (B4 — see docs/10-linting.md).",
|
|
1188
|
+
missing: "Test is missing a `// {{marker}} -` comment (B4 — see docs/10-linting.md — every test needs both `// Given -` and `// Then -`)."
|
|
1171
1189
|
},
|
|
1172
1190
|
type: "problem"
|
|
1173
1191
|
}
|
|
@@ -1208,7 +1226,7 @@ const b5AwaitUsing = {
|
|
|
1208
1226
|
meta: {
|
|
1209
1227
|
docs: RULE_DOCS["b5-await-using"],
|
|
1210
1228
|
defaultOptions: [{ runners: [] }],
|
|
1211
|
-
messages: { requireAwaitUsing: "The result of docker-aware runner \"{{runner}}\" must be bound with `await using` so its containers are disposed (
|
|
1229
|
+
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)." },
|
|
1212
1230
|
schema: [{
|
|
1213
1231
|
additionalProperties: false,
|
|
1214
1232
|
properties: { runners: {
|
|
@@ -1251,7 +1269,7 @@ const b6wRedundantEnvUrl = {
|
|
|
1251
1269
|
},
|
|
1252
1270
|
meta: {
|
|
1253
1271
|
docs: RULE_DOCS["b6w-redundant-env-url"],
|
|
1254
|
-
messages: { redundant: ".env({ {{key}}: ….connectionString }) is redundant — the framework already injects <KEY>_URL (and the DATABASE_URL/REDIS_URL aliases) for every service (
|
|
1272
|
+
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)." },
|
|
1255
1273
|
type: "suggestion"
|
|
1256
1274
|
}
|
|
1257
1275
|
};
|
|
@@ -1280,7 +1298,7 @@ const b8KebabTrigger = {
|
|
|
1280
1298
|
},
|
|
1281
1299
|
meta: {
|
|
1282
1300
|
docs: RULE_DOCS["b8-kebab-trigger"],
|
|
1283
|
-
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 (
|
|
1301
|
+
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)." },
|
|
1284
1302
|
type: "problem"
|
|
1285
1303
|
}
|
|
1286
1304
|
};
|
|
@@ -1335,7 +1353,7 @@ const b9wProductCommand = {
|
|
|
1335
1353
|
},
|
|
1336
1354
|
meta: {
|
|
1337
1355
|
docs: RULE_DOCS["b9w-product-command"],
|
|
1338
|
-
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 (
|
|
1356
|
+
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." },
|
|
1339
1357
|
type: "suggestion"
|
|
1340
1358
|
}
|
|
1341
1359
|
};
|
|
@@ -1391,9 +1409,9 @@ const c1DomainStructure = {
|
|
|
1391
1409
|
meta: {
|
|
1392
1410
|
docs: RULE_DOCS["c1-domain-structure"],
|
|
1393
1411
|
messages: {
|
|
1394
|
-
specNotAtFacetRoot: "A `*.specification.ts` must sit at the facet root: `specs/<facet>/<name>.specification.ts` (
|
|
1395
|
-
testAtFacetRoot: "A `*.test.ts` must live in a domain folder: `specs/<facet>/<domain>/<aspect>.test.ts` — tests directly at the facet root are forbidden (
|
|
1396
|
-
testTooDeep: "A `*.test.ts` must sit at facet/domain depth: `specs/<facet>/<domain>/<aspect>.test.ts` — no deeper nesting (
|
|
1412
|
+
specNotAtFacetRoot: "A `*.specification.ts` must sit at the facet root: `specs/<facet>/<name>.specification.ts` (C1 — see docs/10-linting.md).",
|
|
1413
|
+
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).",
|
|
1414
|
+
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)."
|
|
1397
1415
|
},
|
|
1398
1416
|
type: "problem"
|
|
1399
1417
|
}
|
|
@@ -1423,13 +1441,13 @@ const c2HttpOnlyRequests = {
|
|
|
1423
1441
|
},
|
|
1424
1442
|
meta: {
|
|
1425
1443
|
docs: RULE_DOCS["c2-http-only-requests"],
|
|
1426
|
-
messages: { notHttp: "requests/{{entry}} is not a .http file — requests/ contains only complete .http request files (
|
|
1444
|
+
messages: { notHttp: "requests/{{entry}} is not a .http file — requests/ contains only complete .http request files (C2 — see docs/10-linting.md)." },
|
|
1427
1445
|
type: "problem"
|
|
1428
1446
|
}
|
|
1429
1447
|
};
|
|
1430
1448
|
//#endregion
|
|
1431
1449
|
//#region src/lint/rules/c4-contract-shape.ts
|
|
1432
|
-
/** Providers a contract file may declare (
|
|
1450
|
+
/** Providers a contract file may declare (C4 — see docs/10-linting.md). */
|
|
1433
1451
|
const PROVIDERS = /* @__PURE__ */ new Set([
|
|
1434
1452
|
"anthropic",
|
|
1435
1453
|
"http",
|
|
@@ -1495,12 +1513,12 @@ const c4ContractShape = {
|
|
|
1495
1513
|
meta: {
|
|
1496
1514
|
docs: RULE_DOCS["c4-contract-shape"],
|
|
1497
1515
|
messages: {
|
|
1498
|
-
badName: "Contract file \"{{base}}\" must be named <name>.<provider>.ts with provider ∈ openai | anthropic | http (
|
|
1499
|
-
foreignImport: "Contract imports \"{{source}}\" — a contract imports only from the public entry (@jterrazz/test) (
|
|
1500
|
-
missingDefault: "Contract file has no `export default defineContract(...)` (
|
|
1501
|
-
namedExport: "Contract files have no named exports — only `export default defineContract(...)` (
|
|
1502
|
-
notDefineContract: "The default export of a contract file must be `defineContract(...)` syntactically (
|
|
1503
|
-
subfolder: "Contract file \"{{base}}\" is nested — contracts/ is flat, no subfolders (
|
|
1516
|
+
badName: "Contract file \"{{base}}\" must be named <name>.<provider>.ts with provider ∈ openai | anthropic | http (C4 — see docs/10-linting.md).",
|
|
1517
|
+
foreignImport: "Contract imports \"{{source}}\" — a contract imports only from the public entry (@jterrazz/test) (C4 — see docs/10-linting.md).",
|
|
1518
|
+
missingDefault: "Contract file has no `export default defineContract(...)` (C4 — see docs/10-linting.md).",
|
|
1519
|
+
namedExport: "Contract files have no named exports — only `export default defineContract(...)` (C4 — see docs/10-linting.md).",
|
|
1520
|
+
notDefineContract: "The default export of a contract file must be `defineContract(...)` syntactically (C4 — see docs/10-linting.md).",
|
|
1521
|
+
subfolder: "Contract file \"{{base}}\" is nested — contracts/ is flat, no subfolders (C4 — see docs/10-linting.md)."
|
|
1504
1522
|
},
|
|
1505
1523
|
type: "problem"
|
|
1506
1524
|
}
|
|
@@ -1551,7 +1569,7 @@ const c6ToMatchExtension = {
|
|
|
1551
1569
|
},
|
|
1552
1570
|
meta: {
|
|
1553
1571
|
docs: RULE_DOCS["c6-tomatch-extension"],
|
|
1554
|
-
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 (
|
|
1572
|
+
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)." },
|
|
1555
1573
|
type: "problem"
|
|
1556
1574
|
}
|
|
1557
1575
|
};
|
|
@@ -1579,7 +1597,7 @@ const c7SeedsSqlOnly = {
|
|
|
1579
1597
|
},
|
|
1580
1598
|
meta: {
|
|
1581
1599
|
docs: RULE_DOCS["c7-seeds-sql-only"],
|
|
1582
|
-
messages: { notSql: "seeds/{{entry}} is not a .sql file — seeds/ carries database state only; file state goes through .fixture() (
|
|
1600
|
+
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)." },
|
|
1583
1601
|
type: "problem"
|
|
1584
1602
|
}
|
|
1585
1603
|
};
|
|
@@ -1668,7 +1686,7 @@ const c8ReferencedFixtureExists = {
|
|
|
1668
1686
|
},
|
|
1669
1687
|
meta: {
|
|
1670
1688
|
docs: RULE_DOCS["c8-referenced-fixture-exists"],
|
|
1671
|
-
messages: { missing: "Referenced fixture \"{{path}}\" does not exist on disk under its conventional {{root}}/ root (
|
|
1689
|
+
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." },
|
|
1672
1690
|
type: "problem"
|
|
1673
1691
|
}
|
|
1674
1692
|
};
|
|
@@ -1737,7 +1755,7 @@ const d12wResponseBodyProbe = {
|
|
|
1737
1755
|
meta: {
|
|
1738
1756
|
docs: RULE_DOCS["d12w-response-body-probe"],
|
|
1739
1757
|
defaultOptions: [{ threshold: DEFAULT_THRESHOLD }],
|
|
1740
|
-
messages: { bodyProbeCluster: "{{count}} body probes in one test — this wants a full golden: expect(result.response).toMatch('x.http') (
|
|
1758
|
+
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)." },
|
|
1741
1759
|
schema: [{
|
|
1742
1760
|
additionalProperties: false,
|
|
1743
1761
|
properties: { threshold: {
|
|
@@ -1820,7 +1838,7 @@ const d13wUnfrozenNegativeFixture = {
|
|
|
1820
1838
|
},
|
|
1821
1839
|
meta: {
|
|
1822
1840
|
docs: RULE_DOCS["d13w-unfrozen-negative-fixture"],
|
|
1823
|
-
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 (
|
|
1841
|
+
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)." },
|
|
1824
1842
|
type: "suggestion"
|
|
1825
1843
|
}
|
|
1826
1844
|
};
|
|
@@ -1896,7 +1914,7 @@ const d15wStatusOnlyProbe = {
|
|
|
1896
1914
|
},
|
|
1897
1915
|
meta: {
|
|
1898
1916
|
docs: RULE_DOCS["d15w-status-only-probe"],
|
|
1899
|
-
messages: { statusOnlyProbe: "The test's only assertion(s) are status probes — pin the full response: expect(result.response).toMatch('case.http') (
|
|
1917
|
+
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)." },
|
|
1900
1918
|
type: "suggestion"
|
|
1901
1919
|
}
|
|
1902
1920
|
};
|
|
@@ -1951,7 +1969,7 @@ const d2AwaitIoMatcher = {
|
|
|
1951
1969
|
},
|
|
1952
1970
|
meta: {
|
|
1953
1971
|
docs: RULE_DOCS["d2-await-io-matcher"],
|
|
1954
|
-
messages: { mustAwait: "expect(…).{{matcher}}(…) is IO-backed and returns a promise — it must be awaited or returned, else the assertion never runs (
|
|
1972
|
+
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)." },
|
|
1955
1973
|
type: "problem"
|
|
1956
1974
|
}
|
|
1957
1975
|
};
|
|
@@ -2005,7 +2023,7 @@ const d2wAwaitSyncMatcher = {
|
|
|
2005
2023
|
},
|
|
2006
2024
|
meta: {
|
|
2007
2025
|
docs: RULE_DOCS["d2w-await-sync-matcher"],
|
|
2008
|
-
messages: { redundantAwait: "await on expect(…).{{matcher}}(…) is redundant — that matcher is synchronous; only IO-backed matchers (toMatchRows/toBeEmpty/toBeRunning) are awaited (
|
|
2026
|
+
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)." },
|
|
2009
2027
|
type: "suggestion"
|
|
2010
2028
|
}
|
|
2011
2029
|
};
|
|
@@ -2082,7 +2100,7 @@ const d6wTransformTokenEquivalent = {
|
|
|
2082
2100
|
},
|
|
2083
2101
|
meta: {
|
|
2084
2102
|
docs: RULE_DOCS["d6w-transform-token-equivalent"],
|
|
2085
|
-
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 (
|
|
2103
|
+
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)." },
|
|
2086
2104
|
type: "suggestion"
|
|
2087
2105
|
}
|
|
2088
2106
|
};
|
|
@@ -2116,7 +2134,7 @@ const d8wTextBypass = {
|
|
|
2116
2134
|
},
|
|
2117
2135
|
meta: {
|
|
2118
2136
|
docs: RULE_DOCS["d8w-text-bypass"],
|
|
2119
|
-
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 (
|
|
2137
|
+
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)." },
|
|
2120
2138
|
type: "suggestion"
|
|
2121
2139
|
}
|
|
2122
2140
|
};
|
|
@@ -2199,7 +2217,7 @@ const d9wSingleUseRef = {
|
|
|
2199
2217
|
},
|
|
2200
2218
|
meta: {
|
|
2201
2219
|
docs: RULE_DOCS["d9w-single-use-ref"],
|
|
2202
|
-
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 (
|
|
2220
|
+
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)." },
|
|
2203
2221
|
type: "suggestion"
|
|
2204
2222
|
}
|
|
2205
2223
|
};
|
|
@@ -2227,7 +2245,7 @@ const f1NoSubpathImport = {
|
|
|
2227
2245
|
},
|
|
2228
2246
|
meta: {
|
|
2229
2247
|
docs: RULE_DOCS["f1-no-subpath-import"],
|
|
2230
|
-
messages: { subpath: "Import from \"@jterrazz/test\", not \"{{source}}\" — subpaths do not exist (
|
|
2248
|
+
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)." },
|
|
2231
2249
|
type: "problem"
|
|
2232
2250
|
}
|
|
2233
2251
|
};
|
|
@@ -2275,7 +2293,7 @@ const f2NoTestImportsInProd = {
|
|
|
2275
2293
|
},
|
|
2276
2294
|
meta: {
|
|
2277
2295
|
docs: RULE_DOCS["f2-no-test-imports-in-prod"],
|
|
2278
|
-
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 (
|
|
2296
|
+
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)." },
|
|
2279
2297
|
type: "problem"
|
|
2280
2298
|
}
|
|
2281
2299
|
};
|
|
@@ -2339,7 +2357,7 @@ const f3SpecsPublicEntry = {
|
|
|
2339
2357
|
},
|
|
2340
2358
|
meta: {
|
|
2341
2359
|
docs: RULE_DOCS["f3-specs-public-entry"],
|
|
2342
|
-
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}}\" (
|
|
2360
|
+
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)." },
|
|
2343
2361
|
type: "problem"
|
|
2344
2362
|
}
|
|
2345
2363
|
};
|
|
@@ -2365,7 +2383,7 @@ const f4NoTestToTestImport = {
|
|
|
2365
2383
|
},
|
|
2366
2384
|
meta: {
|
|
2367
2385
|
docs: RULE_DOCS["f4-no-test-to-test-import"],
|
|
2368
|
-
messages: { testToTest: "A test file must not import another test file (\"{{source}}\") — share data via a *.fixtures.ts neighbour or a *.specification.ts runner (
|
|
2386
|
+
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)." },
|
|
2369
2387
|
type: "problem"
|
|
2370
2388
|
}
|
|
2371
2389
|
};
|
|
@@ -2391,14 +2409,14 @@ const f5FixturesOnlyFromTests = {
|
|
|
2391
2409
|
},
|
|
2392
2410
|
meta: {
|
|
2393
2411
|
docs: RULE_DOCS["f5-fixtures-only-from-tests"],
|
|
2394
|
-
messages: { fixturesImport: "A *.fixtures.ts module (\"{{source}}\") is only importable from *.test.ts files (
|
|
2412
|
+
messages: { fixturesImport: "A *.fixtures.ts module (\"{{source}}\") is only importable from *.test.ts files (F5 — see docs/10-linting.md)." },
|
|
2395
2413
|
type: "problem"
|
|
2396
2414
|
}
|
|
2397
2415
|
};
|
|
2398
2416
|
//#endregion
|
|
2399
2417
|
//#region src/lint/rules/i1-layer-boundaries.ts
|
|
2400
2418
|
/**
|
|
2401
|
-
* One folder = one external dependency (
|
|
2419
|
+
* One folder = one external dependency (I1 — see docs/10-linting.md): the packages each
|
|
2402
2420
|
* `src/integrations/<folder>/` may import.
|
|
2403
2421
|
*/
|
|
2404
2422
|
const INTEGRATION_DEPS = {
|
|
@@ -2408,6 +2426,7 @@ const INTEGRATION_DEPS = {
|
|
|
2408
2426
|
hono: ["hono", "@hono/node-server"],
|
|
2409
2427
|
msw: ["msw"],
|
|
2410
2428
|
openai: ["openai"],
|
|
2429
|
+
playwright: ["playwright"],
|
|
2411
2430
|
postgres: ["pg"],
|
|
2412
2431
|
redis: ["redis"],
|
|
2413
2432
|
sqlite: ["better-sqlite3"],
|
|
@@ -2478,6 +2497,7 @@ const i1LayerBoundaries = {
|
|
|
2478
2497
|
if (layer === "core") {
|
|
2479
2498
|
if (target.startsWith("core/") || target.startsWith("integrations/docker/") || target.startsWith("integrations/hono/") || withoutExtension(target) === "vitest/matchers") return null;
|
|
2480
2499
|
if (target.startsWith("integrations/msw/") && inside === "core/specification/shared/builder.ts") return null;
|
|
2500
|
+
if (target.startsWith("integrations/playwright/") && inside === "core/specification/website/start-website.ts") return null;
|
|
2481
2501
|
return "crossLayer";
|
|
2482
2502
|
}
|
|
2483
2503
|
if (layer === "integrations") return target.startsWith("core/") || target.startsWith(`integrations/${integrationFolder}/`) ? null : "crossLayer";
|
|
@@ -2504,10 +2524,10 @@ const i1LayerBoundaries = {
|
|
|
2504
2524
|
meta: {
|
|
2505
2525
|
docs: RULE_DOCS["i1-layer-boundaries"],
|
|
2506
2526
|
messages: {
|
|
2507
|
-
coreExternal: "core/ imports nothing external — \"{{source}}\" is not allowed (
|
|
2508
|
-
crossLayer: "Layer \"{{layer}}\" must not import \"{{source}}\" — outside the sanctioned layer edges (
|
|
2509
|
-
foreignDependency: "\"{{source}}\" is not this folder's own dependency — one integrations folder = one external dependency (
|
|
2510
|
-
lintRuntime: "The lint layer imports nothing from the framework runtime — \"{{source}}\" is outside its pure-helper whitelist (
|
|
2527
|
+
coreExternal: "core/ imports nothing external — \"{{source}}\" is not allowed (I1 — see docs/10-linting.md).",
|
|
2528
|
+
crossLayer: "Layer \"{{layer}}\" must not import \"{{source}}\" — outside the sanctioned layer edges (I1 — see docs/10-linting.md).",
|
|
2529
|
+
foreignDependency: "\"{{source}}\" is not this folder's own dependency — one integrations folder = one external dependency (I1 — see docs/10-linting.md).",
|
|
2530
|
+
lintRuntime: "The lint layer imports nothing from the framework runtime — \"{{source}}\" is outside its pure-helper whitelist (I1 — see docs/10-linting.md)."
|
|
2511
2531
|
},
|
|
2512
2532
|
type: "problem"
|
|
2513
2533
|
}
|
|
@@ -2560,9 +2580,9 @@ const i2SiblingTestNaming = {
|
|
|
2560
2580
|
meta: {
|
|
2561
2581
|
docs: RULE_DOCS["i2-sibling-test-naming"],
|
|
2562
2582
|
messages: {
|
|
2563
|
-
orphanTest: "A src/ module test must sit NEXT to the module it tests — no neighbour \"{{expected}}\" found (
|
|
2564
|
-
rootTests: "A root-level tests/ directory is banned — module tests are siblings under src/, product specifications live in specs/ (
|
|
2565
|
-
testsDir: "__tests__/ directories are banned under src/ — the test of <file>.ts is its neighbour <file>.test.ts (
|
|
2583
|
+
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/.",
|
|
2584
|
+
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).",
|
|
2585
|
+
testsDir: "__tests__/ directories are banned under src/ — the test of <file>.ts is its neighbour <file>.test.ts (I2 — see docs/10-linting.md)."
|
|
2566
2586
|
},
|
|
2567
2587
|
type: "problem"
|
|
2568
2588
|
}
|
|
@@ -2621,9 +2641,9 @@ const i4NoViMockInSrc = {
|
|
|
2621
2641
|
meta: {
|
|
2622
2642
|
docs: RULE_DOCS["i4-no-vi-mock-in-src"],
|
|
2623
2643
|
messages: {
|
|
2624
|
-
assetImport: "A src/ module test must not import the data asset \"{{source}}\" — inline it as code or move the test to specs/ (
|
|
2625
|
-
bannedDir: "`{{dir}}/` directories are banned under src/ — mocks and data are code: mockOf/mockOfDate inline, payloads in a *.fixtures.ts neighbour (
|
|
2626
|
-
viMock: "`vi.mock` is banned under src/ — use mockOf/mockOfDate (
|
|
2644
|
+
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).",
|
|
2645
|
+
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).",
|
|
2646
|
+
viMock: "`vi.mock` is banned under src/ — use mockOf/mockOfDate (I4 — see docs/10-linting.md)."
|
|
2627
2647
|
},
|
|
2628
2648
|
type: "problem"
|
|
2629
2649
|
}
|
|
@@ -2667,7 +2687,7 @@ const j1NoOnlySkip = {
|
|
|
2667
2687
|
},
|
|
2668
2688
|
meta: {
|
|
2669
2689
|
docs: RULE_DOCS["j1-no-only-skip"],
|
|
2670
|
-
messages: { forbidden: "Committed `{{runner}}.{{modifier}}` is not allowed (
|
|
2690
|
+
messages: { forbidden: "Committed `{{runner}}.{{modifier}}` is not allowed (J1 — see docs/10-linting.md). Remove it before committing." },
|
|
2671
2691
|
type: "problem"
|
|
2672
2692
|
}
|
|
2673
2693
|
};
|
|
@@ -2710,8 +2730,8 @@ const j2NoSleepInSpecs = {
|
|
|
2710
2730
|
meta: {
|
|
2711
2731
|
docs: RULE_DOCS["j2-no-sleep-in-specs"],
|
|
2712
2732
|
messages: {
|
|
2713
|
-
sleep: "No arbitrary sleeps in specs — synchronise via `waitFor` or the framework (
|
|
2714
|
-
timersImport: "No timer-based sleeps in specs — synchronise via `waitFor` or the framework (
|
|
2733
|
+
sleep: "No arbitrary sleeps in specs — synchronise via `waitFor` or the framework (J2 — see docs/10-linting.md).",
|
|
2734
|
+
timersImport: "No timer-based sleeps in specs — synchronise via `waitFor` or the framework (J2 — see docs/10-linting.md)."
|
|
2715
2735
|
},
|
|
2716
2736
|
type: "problem"
|
|
2717
2737
|
}
|
|
@@ -2751,7 +2771,7 @@ const j3NoExpectlessTest = {
|
|
|
2751
2771
|
},
|
|
2752
2772
|
meta: {
|
|
2753
2773
|
docs: RULE_DOCS["j3-no-expectless-test"],
|
|
2754
|
-
messages: { noExpect: "Test has no `expect(…)` — a test must assert something (
|
|
2774
|
+
messages: { noExpect: "Test has no `expect(…)` — a test must assert something (J3 — see docs/10-linting.md). Use `test.todo` for a pending test." },
|
|
2755
2775
|
type: "problem"
|
|
2756
2776
|
}
|
|
2757
2777
|
};
|
|
@@ -2787,7 +2807,7 @@ const j4UniqueTestNames = {
|
|
|
2787
2807
|
},
|
|
2788
2808
|
meta: {
|
|
2789
2809
|
docs: RULE_DOCS["j4-unique-test-names"],
|
|
2790
|
-
messages: { duplicate: "Duplicate test name \"{{name}}\" in this file — the test name is the sole description of behaviour, so it must be unique (
|
|
2810
|
+
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)." },
|
|
2791
2811
|
type: "problem"
|
|
2792
2812
|
}
|
|
2793
2813
|
};
|
|
@@ -2883,9 +2903,55 @@ const plugin = {
|
|
|
2883
2903
|
},
|
|
2884
2904
|
meta: {
|
|
2885
2905
|
docs: RULE_DOCS["j5-lowercase-title"],
|
|
2886
|
-
messages: { uppercase: "A {{runner}}() title must start lowercase — the test name is a prose fragment, not a sentence (
|
|
2906
|
+
messages: { uppercase: "A {{runner}}() title must start lowercase — the test name is a prose fragment, not a sentence (J5 — see docs/10-linting.md)." },
|
|
2887
2907
|
type: "problem"
|
|
2888
2908
|
}
|
|
2909
|
+
},
|
|
2910
|
+
"w1-scenario-pure": {
|
|
2911
|
+
create(context) {
|
|
2912
|
+
return { CallExpression(node) {
|
|
2913
|
+
const callee = node.callee;
|
|
2914
|
+
if (!callee || memberPropertyName(callee) !== "visit") return;
|
|
2915
|
+
const scenario = node.arguments?.[1];
|
|
2916
|
+
if (scenario?.type !== "ArrowFunctionExpression" && scenario?.type !== "FunctionExpression") return;
|
|
2917
|
+
walk(scenario, (inner) => {
|
|
2918
|
+
if (inner.type !== "CallExpression") return;
|
|
2919
|
+
const innerCallee = inner.callee;
|
|
2920
|
+
if (innerCallee?.type === "Identifier" && innerCallee.name === "expect") context.report({
|
|
2921
|
+
messageId: "expectInScenario",
|
|
2922
|
+
node: inner
|
|
2923
|
+
});
|
|
2924
|
+
});
|
|
2925
|
+
} };
|
|
2926
|
+
},
|
|
2927
|
+
meta: {
|
|
2928
|
+
docs: RULE_DOCS["w1-scenario-pure"],
|
|
2929
|
+
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)." },
|
|
2930
|
+
type: "problem"
|
|
2931
|
+
}
|
|
2932
|
+
},
|
|
2933
|
+
"w2w-user-facing-elements": {
|
|
2934
|
+
create(context) {
|
|
2935
|
+
return { CallExpression(node) {
|
|
2936
|
+
const callee = node.callee;
|
|
2937
|
+
if (!callee || memberPropertyName(callee) !== "visit") return;
|
|
2938
|
+
const scenario = node.arguments?.[1];
|
|
2939
|
+
if (scenario?.type !== "ArrowFunctionExpression" && scenario?.type !== "FunctionExpression") return;
|
|
2940
|
+
walk(scenario, (inner) => {
|
|
2941
|
+
if (inner.type !== "CallExpression") return;
|
|
2942
|
+
const innerCallee = inner.callee;
|
|
2943
|
+
if (innerCallee?.type === "Identifier" && innerCallee.name === "testId") context.report({
|
|
2944
|
+
messageId: "testIdElement",
|
|
2945
|
+
node: inner
|
|
2946
|
+
});
|
|
2947
|
+
});
|
|
2948
|
+
} };
|
|
2949
|
+
},
|
|
2950
|
+
meta: {
|
|
2951
|
+
docs: RULE_DOCS["w2w-user-facing-elements"],
|
|
2952
|
+
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)." },
|
|
2953
|
+
type: "suggestion"
|
|
2954
|
+
}
|
|
2889
2955
|
}
|
|
2890
2956
|
}
|
|
2891
2957
|
};
|
package/dist/oxlint.d.cts
CHANGED
|
@@ -61,6 +61,11 @@ type RuleDoc = {
|
|
|
61
61
|
channel: 'checker' | 'process' | 'runtime' | 'statique';
|
|
62
62
|
/** The French normative sentence (the constitution's per-rule text, moved here). */
|
|
63
63
|
convention: string;
|
|
64
|
+
/**
|
|
65
|
+
* The specification facet the rule guards — segments the catalogue like
|
|
66
|
+
* the constructors segment the API. `'shared'` for cross-facet rules.
|
|
67
|
+
*/
|
|
68
|
+
facet?: 'api' | 'cli' | 'jobs' | 'shared' | 'website';
|
|
64
69
|
/** Convention family letter, e.g. `'A'`. */
|
|
65
70
|
family: string;
|
|
66
71
|
/** Convention code, e.g. `'A1'`. */
|
package/dist/oxlint.d.ts
CHANGED
|
@@ -61,6 +61,11 @@ type RuleDoc = {
|
|
|
61
61
|
channel: 'checker' | 'process' | 'runtime' | 'statique';
|
|
62
62
|
/** The French normative sentence (the constitution's per-rule text, moved here). */
|
|
63
63
|
convention: string;
|
|
64
|
+
/**
|
|
65
|
+
* The specification facet the rule guards — segments the catalogue like
|
|
66
|
+
* the constructors segment the API. `'shared'` for cross-facet rules.
|
|
67
|
+
*/
|
|
68
|
+
facet?: 'api' | 'cli' | 'jobs' | 'shared' | 'website';
|
|
64
69
|
/** Convention family letter, e.g. `'A'`. */
|
|
65
70
|
family: string;
|
|
66
71
|
/** Convention code, e.g. `'A1'`. */
|