@jterrazz/test 14.3.0 → 15.0.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 +10 -10
- package/dist/ambiguity.js +1 -1
- package/dist/appium.adapter.js +1 -1
- package/dist/checker.js +14 -14
- package/dist/oxlint.cjs +90 -281
- package/dist/oxlint.d.cts +2 -2
- package/dist/oxlint.d.ts +2 -2
- package/dist/oxlint.js +90 -281
- package/package.json +3 -2
package/dist/oxlint.js
CHANGED
|
@@ -194,21 +194,10 @@ function isTestCallee(callee) {
|
|
|
194
194
|
if (callee.type === "CallExpression") return isTestCallee(callee.callee);
|
|
195
195
|
return false;
|
|
196
196
|
}
|
|
197
|
-
/** Does the test callee chain carry a modifier member with the given name? */
|
|
198
|
-
function testCalleeHasModifier(callee, modifier) {
|
|
199
|
-
if (callee === void 0) return false;
|
|
200
|
-
if (callee.type === "MemberExpression") return memberPropertyName(callee) === modifier || testCalleeHasModifier(callee.object, modifier);
|
|
201
|
-
if (callee.type === "CallExpression") return testCalleeHasModifier(callee.callee, modifier);
|
|
202
|
-
return false;
|
|
203
|
-
}
|
|
204
197
|
/** The callback argument of a test call (arrow or function expression), if any. */
|
|
205
198
|
function findTestCallback(args) {
|
|
206
199
|
return args.find((arg) => arg.type === "ArrowFunctionExpression" || arg.type === "FunctionExpression");
|
|
207
200
|
}
|
|
208
|
-
/** The static string name of a test call (`test('name', …)`), if a literal. */
|
|
209
|
-
function testName(callNode) {
|
|
210
|
-
return stringValue((callNode.arguments ?? [])[0]);
|
|
211
|
-
}
|
|
212
201
|
/**
|
|
213
202
|
* Visitor fragment collecting every import-like source: static imports,
|
|
214
203
|
* dynamic `import()`, and re-exports. Rules spread this into their visitor.
|
|
@@ -235,6 +224,26 @@ function importSourceVisitor(onSource) {
|
|
|
235
224
|
* The **statique** channel — one entry per shipped `jterrazz/*` rule. Each rule
|
|
236
225
|
* file attaches its entry as `meta.docs`, so a rule and its normative text can
|
|
237
226
|
* never drift (the completeness meta-test asserts every rule carries one).
|
|
227
|
+
*
|
|
228
|
+
* **What this channel does NOT own.** A convention oxlint's own `vitest` plugin
|
|
229
|
+
* already enforces belongs to that plugin, not here — `@jterrazz/typescript` v10
|
|
230
|
+
* turns the plugin on over the test globs in every profile, so a consumer of the
|
|
231
|
+
* preset gets it without wiring. Four hygiene rules left the channel on that
|
|
232
|
+
* ground (J1, J3, J4, J5 → `vitest/no-focused-tests` + `vitest/no-disabled-tests`,
|
|
233
|
+
* `vitest/expect-expect`, `vitest/no-identical-title`, `vitest/prefer-lowercase-title`).
|
|
234
|
+
* `prefer-lowercase-title` is stricter than J5 was: a title opening on an all-caps
|
|
235
|
+
* identifier (`HTTP 404 …`, `DI …`) now fails, and the rulebook carries no
|
|
236
|
+
* `allowedPrefixes` — a title starts lowercase, whatever its first word. The
|
|
237
|
+
* rest are behaviour-identical on this package's own fixtures. Only J2 (the
|
|
238
|
+
* arbitrary-sleep ban, which has no upstream counterpart) stays statique.
|
|
239
|
+
*
|
|
240
|
+
* **Severity.** The estate's rulebook has no warn tier: a rule is on at `error`
|
|
241
|
+
* or off with a recorded reason, and that law binds `@jterrazz/typescript`'s
|
|
242
|
+
* rulebook. This channel is its one documented exception, and it is narrow — an
|
|
243
|
+
* id shaped `<family><n>w-…` is a REDUNDANCY heuristic (a `root` the walk would
|
|
244
|
+
* have found, a probe cluster that wants a golden), a reading of taste that
|
|
245
|
+
* would block a legitimate shape if it failed the build. Every hard convention
|
|
246
|
+
* here is an `error`; `plugin.test.ts` holds the split.
|
|
238
247
|
*/
|
|
239
248
|
const RULE_DOCS = {
|
|
240
249
|
"a1-specification-file": {
|
|
@@ -510,13 +519,6 @@ const RULE_DOCS = {
|
|
|
510
519
|
id: "I4",
|
|
511
520
|
rationale: "Dans les tests de module, mocks et données sont du CODE (`mockOf`, `*.fixtures.ts`) ; un vrai fichier appelle une spec."
|
|
512
521
|
},
|
|
513
|
-
"j1-no-only-skip": {
|
|
514
|
-
channel: "statique",
|
|
515
|
-
convention: "Aucun `.only` / `.skip` committé (`describe.only`, `test.only`, `test.skip`).",
|
|
516
|
-
family: "J",
|
|
517
|
-
id: "J1",
|
|
518
|
-
rationale: "Un `.only`/`.skip` oublié désactive silencieusement une partie de la suite."
|
|
519
|
-
},
|
|
520
522
|
"j2-no-sleep-in-specs": {
|
|
521
523
|
channel: "statique",
|
|
522
524
|
convention: "Aucun sleep arbitraire (`setTimeout`/`setInterval`/`Atomics.wait`) sous `specs/**` — la synchronisation passe par `waitFor`.",
|
|
@@ -524,27 +526,6 @@ const RULE_DOCS = {
|
|
|
524
526
|
id: "J2",
|
|
525
527
|
rationale: "Un sleep fixe rend les tests lents et instables ; attendre une condition est déterministe."
|
|
526
528
|
},
|
|
527
|
-
"j3-no-expectless-test": {
|
|
528
|
-
channel: "statique",
|
|
529
|
-
convention: "Un `test(...)` avec callback contient au moins un `expect(…)` ; `test.todo` (sans callback) est ignoré.",
|
|
530
|
-
family: "J",
|
|
531
|
-
id: "J3",
|
|
532
|
-
rationale: "Un test sans assertion est mort ou muet — il passe toujours sans rien vérifier."
|
|
533
|
-
},
|
|
534
|
-
"j4-unique-test-names": {
|
|
535
|
-
channel: "statique",
|
|
536
|
-
convention: "Deux tests d’un même fichier ne partagent pas un nom littéral (`.each` ignoré).",
|
|
537
|
-
family: "J",
|
|
538
|
-
id: "J4",
|
|
539
|
-
rationale: "Le nom du test est son unique description — deux noms identiques rendent un échec ambigu."
|
|
540
|
-
},
|
|
541
|
-
"j5-lowercase-title": {
|
|
542
|
-
channel: "statique",
|
|
543
|
-
convention: "La première lettre d’un titre `test()`/`describe()`/`it()` littéral est en minuscule. Exemptés : les titres dont le premier MOT est un identifiant tout en majuscules/underscores (`VALID_CATEGORIES`, `HTTP`, `DI`) et ceux démarrant sur un non-lettre — seuls les premiers mots de prose minusculisables sont contraints.",
|
|
544
|
-
family: "J",
|
|
545
|
-
id: "J5",
|
|
546
|
-
rationale: "Un titre est un fragment de prose, pas une phrase — la casse minuscule le garde fragmentaire ; minusculiser un symbole nommé le mal-orthographierait."
|
|
547
|
-
},
|
|
548
529
|
"w1-scenario-pure": {
|
|
549
530
|
channel: "statique",
|
|
550
531
|
convention: "Un scénario (`.visit()` website, `.open()` mobile) 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é.",
|
|
@@ -903,7 +884,7 @@ const a1SpecificationFile = {
|
|
|
903
884
|
},
|
|
904
885
|
meta: {
|
|
905
886
|
docs: RULE_DOCS["a1-specification-file"],
|
|
906
|
-
messages: { outsideSpecification: "specification.{{member}}() must be called from a `*.specification.ts` file under specs/ (A1 — see docs/
|
|
887
|
+
messages: { outsideSpecification: "specification.{{member}}() must be called from a `*.specification.ts` file under specs/ (A1 — see docs/13-linting.md)." },
|
|
907
888
|
type: "problem"
|
|
908
889
|
}
|
|
909
890
|
};
|
|
@@ -980,13 +961,13 @@ const a10DuplicateBinding = {
|
|
|
980
961
|
},
|
|
981
962
|
meta: {
|
|
982
963
|
docs: RULE_DOCS["a10-duplicate-binding"],
|
|
983
|
-
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/
|
|
964
|
+
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/13-linting.md)." },
|
|
984
965
|
type: "problem"
|
|
985
966
|
}
|
|
986
967
|
};
|
|
987
968
|
//#endregion
|
|
988
969
|
//#region src/lint/rules/a2-known-constructors.ts
|
|
989
|
-
/** The five constructors, and only five (A2 — see docs/
|
|
970
|
+
/** The five constructors, and only five (A2 — see docs/13-linting.md). */
|
|
990
971
|
const KNOWN_CONSTRUCTORS = /* @__PURE__ */ new Set([
|
|
991
972
|
"api",
|
|
992
973
|
"cli",
|
|
@@ -1015,7 +996,7 @@ const a2KnownConstructors = {
|
|
|
1015
996
|
},
|
|
1016
997
|
meta: {
|
|
1017
998
|
docs: RULE_DOCS["a2-known-constructors"],
|
|
1018
|
-
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/
|
|
999
|
+
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)." },
|
|
1019
1000
|
type: "problem"
|
|
1020
1001
|
}
|
|
1021
1002
|
};
|
|
@@ -1060,7 +1041,7 @@ const a3NoDestructureAlias = {
|
|
|
1060
1041
|
},
|
|
1061
1042
|
meta: {
|
|
1062
1043
|
docs: RULE_DOCS["a3-no-destructure-alias"],
|
|
1063
|
-
messages: { aliased: "Destructure the specification result with its canonical name: `{{key}}`, not `{{key}}: {{alias}}` (A3 — see docs/
|
|
1044
|
+
messages: { aliased: "Destructure the specification result with its canonical name: `{{key}}`, not `{{key}}: {{alias}}` (A3 — see docs/13-linting.md). Rename at the import site if a different local name is needed." },
|
|
1064
1045
|
type: "problem"
|
|
1065
1046
|
}
|
|
1066
1047
|
};
|
|
@@ -1112,7 +1093,7 @@ const a4CleanupAfterall = {
|
|
|
1112
1093
|
},
|
|
1113
1094
|
meta: {
|
|
1114
1095
|
docs: RULE_DOCS["a4-cleanup-afterall"],
|
|
1115
|
-
messages: { unregistered: "`cleanup` is destructured but never passed to afterAll — add `afterAll(cleanup)` to the specification file (A4 — see docs/
|
|
1096
|
+
messages: { unregistered: "`cleanup` is destructured but never passed to afterAll — add `afterAll(cleanup)` to the specification file (A4 — see docs/13-linting.md)." },
|
|
1116
1097
|
type: "problem"
|
|
1117
1098
|
}
|
|
1118
1099
|
};
|
|
@@ -1142,7 +1123,7 @@ const a5ModeWithServer = {
|
|
|
1142
1123
|
},
|
|
1143
1124
|
meta: {
|
|
1144
1125
|
docs: RULE_DOCS["a5-mode-with-server"],
|
|
1145
|
-
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/
|
|
1126
|
+
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/13-linting.md)." },
|
|
1146
1127
|
type: "problem"
|
|
1147
1128
|
}
|
|
1148
1129
|
};
|
|
@@ -1183,7 +1164,7 @@ const a6wRedundantComposeService = {
|
|
|
1183
1164
|
},
|
|
1184
1165
|
meta: {
|
|
1185
1166
|
docs: RULE_DOCS["a6w-redundant-compose-service"],
|
|
1186
|
-
messages: { redundant: "composeService: \"{{name}}\" is redundant — the key \"{{key}}\" already binds to it (exact name or kebab-case derivation, A6 — see docs/
|
|
1167
|
+
messages: { redundant: "composeService: \"{{name}}\" is redundant — the key \"{{key}}\" already binds to it (exact name or kebab-case derivation, A6 — see docs/13-linting.md)." },
|
|
1187
1168
|
type: "suggestion"
|
|
1188
1169
|
}
|
|
1189
1170
|
};
|
|
@@ -1218,7 +1199,7 @@ function findRoot(startDir, exists) {
|
|
|
1218
1199
|
//#endregion
|
|
1219
1200
|
//#region src/lint/rules/a9w-redundant-root.ts
|
|
1220
1201
|
/**
|
|
1221
|
-
* The root the convention would derive (A9 — see docs/
|
|
1202
|
+
* The root the convention would derive (A9 — see docs/13-linting.md): the
|
|
1222
1203
|
* framework's own walk-up, run against this layer's cached fs probe. The rule
|
|
1223
1204
|
* flags a `root` that the runner would have found by itself, so the two MUST
|
|
1224
1205
|
* be the same function — a private copy of the walk drifted from it before.
|
|
@@ -1250,7 +1231,7 @@ const a9wRedundantRoot = {
|
|
|
1250
1231
|
},
|
|
1251
1232
|
meta: {
|
|
1252
1233
|
docs: RULE_DOCS["a9w-redundant-root"],
|
|
1253
|
-
messages: { redundant: "root: \"{{root}}\" is redundant — walking up from the specification file already resolves to that directory (A9 — see docs/
|
|
1234
|
+
messages: { redundant: "root: \"{{root}}\" is redundant — walking up from the specification file already resolves to that directory (A9 — see docs/13-linting.md)." },
|
|
1254
1235
|
type: "suggestion"
|
|
1255
1236
|
}
|
|
1256
1237
|
};
|
|
@@ -1356,7 +1337,7 @@ const b2KnownFixtureMarker = {
|
|
|
1356
1337
|
},
|
|
1357
1338
|
meta: {
|
|
1358
1339
|
docs: RULE_DOCS["b2-known-fixture-marker"],
|
|
1359
|
-
messages: { unknownMarker: "Unknown fixture marker \"{{marker}}\" — known markers: {{known}}. A path without a marker is feature-local (B2 — see docs/
|
|
1340
|
+
messages: { unknownMarker: "Unknown fixture marker \"{{marker}}\" — known markers: {{known}}. A path without a marker is feature-local (B2 — see docs/13-linting.md)." },
|
|
1360
1341
|
type: "problem"
|
|
1361
1342
|
}
|
|
1362
1343
|
};
|
|
@@ -1421,8 +1402,8 @@ const b4GivenThen = {
|
|
|
1421
1402
|
meta: {
|
|
1422
1403
|
docs: RULE_DOCS["b4-given-then"],
|
|
1423
1404
|
messages: {
|
|
1424
|
-
givenAfterThen: "The `// Given -` marker comes after `// Then -` — Given describes the setup and must precede Then (B4 — see docs/
|
|
1425
|
-
missing: "Test is missing a `// {{marker}} -` comment (B4 — see docs/
|
|
1405
|
+
givenAfterThen: "The `// Given -` marker comes after `// Then -` — Given describes the setup and must precede Then (B4 — see docs/13-linting.md).",
|
|
1406
|
+
missing: "Test is missing a `// {{marker}} -` comment (B4 — see docs/13-linting.md — every test needs both `// Given -` and `// Then -`)."
|
|
1426
1407
|
},
|
|
1427
1408
|
type: "problem"
|
|
1428
1409
|
}
|
|
@@ -1463,7 +1444,7 @@ const b5AwaitUsing = {
|
|
|
1463
1444
|
meta: {
|
|
1464
1445
|
docs: RULE_DOCS["b5-await-using"],
|
|
1465
1446
|
defaultOptions: [{ runners: [] }],
|
|
1466
|
-
messages: { requireAwaitUsing: "The result of docker-aware runner \"{{runner}}\" must be bound with `await using` so its containers are disposed (B5 — see docs/
|
|
1447
|
+
messages: { requireAwaitUsing: "The result of docker-aware runner \"{{runner}}\" must be bound with `await using` so its containers are disposed (B5 — see docs/13-linting.md)." },
|
|
1467
1448
|
schema: [{
|
|
1468
1449
|
additionalProperties: false,
|
|
1469
1450
|
properties: { runners: {
|
|
@@ -1506,7 +1487,7 @@ const b6wRedundantEnvUrl = {
|
|
|
1506
1487
|
},
|
|
1507
1488
|
meta: {
|
|
1508
1489
|
docs: RULE_DOCS["b6w-redundant-env-url"],
|
|
1509
|
-
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/
|
|
1490
|
+
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/13-linting.md)." },
|
|
1510
1491
|
type: "suggestion"
|
|
1511
1492
|
}
|
|
1512
1493
|
};
|
|
@@ -1535,7 +1516,7 @@ const b8KebabTrigger = {
|
|
|
1535
1516
|
},
|
|
1536
1517
|
meta: {
|
|
1537
1518
|
docs: RULE_DOCS["b8-kebab-trigger"],
|
|
1538
|
-
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/
|
|
1519
|
+
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/13-linting.md)." },
|
|
1539
1520
|
type: "problem"
|
|
1540
1521
|
}
|
|
1541
1522
|
};
|
|
@@ -1590,7 +1571,7 @@ const b9wProductCommand = {
|
|
|
1590
1571
|
},
|
|
1591
1572
|
meta: {
|
|
1592
1573
|
docs: RULE_DOCS["b9w-product-command"],
|
|
1593
|
-
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/
|
|
1574
|
+
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/13-linting.md). Suppress with a reason if the product genuinely is this binary." },
|
|
1594
1575
|
type: "suggestion"
|
|
1595
1576
|
}
|
|
1596
1577
|
};
|
|
@@ -1743,14 +1724,14 @@ const c1DomainStructure = {
|
|
|
1743
1724
|
defaultOptions: [{ depth: DEFAULT_DEPTH }],
|
|
1744
1725
|
docs: RULE_DOCS["c1-domain-structure"],
|
|
1745
1726
|
messages: {
|
|
1746
|
-
specInGround: "A spec must not live under \"{{ground}}/\" — a leading underscore marks GROUND (what the specs of a row stand on: _fixtures/, _expected/, _requests/, _seeds/), never a domain (C1 — see docs/
|
|
1747
|
-
specNotAtFacetRoot: "A `*.specification.ts` must sit at the facet root: `specs/<facet>/<name>.specification.ts` (C1 — see docs/
|
|
1748
|
-
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/
|
|
1749
|
-
testAtSpecsRoot: "A `*.test.ts` must live in a directory under specs/ — with `depth: \"mirror\"` the tree mirrors a structure, and the specs root mirrors nothing (C1 — see docs/
|
|
1750
|
-
testNotMirroringDirectory: "A `*.test.ts` must be named after the directory holding it — `{{directory}}/{{directory}}.test.ts` — with `depth: \"mirror\"` (C1 — see docs/
|
|
1751
|
-
testOutsideFacet: "A `*.test.ts` must live in a facet folder — `specs/<facet>/<aspect>.test.ts`, or one domain deeper: with `depth: \"facet\"` the specs root holds facets, not tests (C1 — see docs/
|
|
1752
|
-
testTooDeep: "A `*.test.ts` must sit at facet/domain depth: `specs/<facet>/<domain>/<aspect>.test.ts` — no deeper nesting (C1 — see docs/
|
|
1753
|
-
testTooDeepForFacet: "A `*.test.ts` must sit at the facet root or one domain folder down — `specs/<facet>/<aspect>.test.ts` or `specs/<facet>/<domain>/<aspect>.test.ts`: with `depth: \"facet\"` nothing nests deeper (C1 — see docs/
|
|
1727
|
+
specInGround: "A spec must not live under \"{{ground}}/\" — a leading underscore marks GROUND (what the specs of a row stand on: _fixtures/, _expected/, _requests/, _seeds/), never a domain (C1 — see docs/13-linting.md). Ground that is CODE keeps its own unit test: `<module>.test.ts` NEXT to the `<module>.ts` it tests (I2), and nothing else.",
|
|
1728
|
+
specNotAtFacetRoot: "A `*.specification.ts` must sit at the facet root: `specs/<facet>/<name>.specification.ts` (C1 — see docs/13-linting.md).",
|
|
1729
|
+
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/13-linting.md).",
|
|
1730
|
+
testAtSpecsRoot: "A `*.test.ts` must live in a directory under specs/ — with `depth: \"mirror\"` the tree mirrors a structure, and the specs root mirrors nothing (C1 — see docs/13-linting.md).",
|
|
1731
|
+
testNotMirroringDirectory: "A `*.test.ts` must be named after the directory holding it — `{{directory}}/{{directory}}.test.ts` — with `depth: \"mirror\"` (C1 — see docs/13-linting.md).",
|
|
1732
|
+
testOutsideFacet: "A `*.test.ts` must live in a facet folder — `specs/<facet>/<aspect>.test.ts`, or one domain deeper: with `depth: \"facet\"` the specs root holds facets, not tests (C1 — see docs/13-linting.md).",
|
|
1733
|
+
testTooDeep: "A `*.test.ts` must sit at facet/domain depth: `specs/<facet>/<domain>/<aspect>.test.ts` — no deeper nesting (C1 — see docs/13-linting.md).",
|
|
1734
|
+
testTooDeepForFacet: "A `*.test.ts` must sit at the facet root or one domain folder down — `specs/<facet>/<aspect>.test.ts` or `specs/<facet>/<domain>/<aspect>.test.ts`: with `depth: \"facet\"` nothing nests deeper (C1 — see docs/13-linting.md)."
|
|
1754
1735
|
},
|
|
1755
1736
|
schema: [{
|
|
1756
1737
|
additionalProperties: false,
|
|
@@ -1795,7 +1776,7 @@ const c10ContractsBoundary = {
|
|
|
1795
1776
|
},
|
|
1796
1777
|
meta: {
|
|
1797
1778
|
docs: RULE_DOCS["c10-contracts-boundary"],
|
|
1798
|
-
messages: { internal: "Import \"{{source}}\" reaches into a provider folder — only contracts/*.contracts.ts is importable from a test; add a named scenario export there instead (C10 — see docs/
|
|
1779
|
+
messages: { internal: "Import \"{{source}}\" reaches into a provider folder — only contracts/*.contracts.ts is importable from a test; add a named scenario export there instead (C10 — see docs/13-linting.md)." },
|
|
1799
1780
|
type: "problem"
|
|
1800
1781
|
}
|
|
1801
1782
|
};
|
|
@@ -1850,7 +1831,7 @@ const c11ContractDataPairing = {
|
|
|
1850
1831
|
},
|
|
1851
1832
|
meta: {
|
|
1852
1833
|
docs: RULE_DOCS["c11-contract-data-pairing"],
|
|
1853
|
-
messages: { orphan: "contracts/{{provider}}/{{entry}} has no owning contract — data pairs with the sibling {{owner}} (stem = name up to the first dot) (C11 — see docs/
|
|
1834
|
+
messages: { orphan: "contracts/{{provider}}/{{entry}} has no owning contract — data pairs with the sibling {{owner}} (stem = name up to the first dot) (C11 — see docs/13-linting.md)." },
|
|
1854
1835
|
type: "problem"
|
|
1855
1836
|
}
|
|
1856
1837
|
};
|
|
@@ -1903,7 +1884,7 @@ const c13UnderscoredGround = {
|
|
|
1903
1884
|
},
|
|
1904
1885
|
meta: {
|
|
1905
1886
|
docs: RULE_DOCS["c13-underscored-ground"],
|
|
1906
|
-
messages: { notUnderscored: "The directory \"{{legacy}}/\" is a pre-14 ground name — rename it to \"{{ground}}/\" (`git mv {{legacy}} {{ground}}`). What a spec stands on carries the underscore; a spec's own folder never does, and no resolver reads the un-underscored name (C13 — see docs/
|
|
1887
|
+
messages: { notUnderscored: "The directory \"{{legacy}}/\" is a pre-14 ground name — rename it to \"{{ground}}/\" (`git mv {{legacy}} {{ground}}`). What a spec stands on carries the underscore; a spec's own folder never does, and no resolver reads the un-underscored name (C13 — see docs/13-linting.md)." },
|
|
1907
1888
|
type: "problem"
|
|
1908
1889
|
}
|
|
1909
1890
|
};
|
|
@@ -1932,7 +1913,7 @@ const c2HttpOnlyRequests = {
|
|
|
1932
1913
|
},
|
|
1933
1914
|
meta: {
|
|
1934
1915
|
docs: RULE_DOCS["c2-http-only-requests"],
|
|
1935
|
-
messages: { notHttp: "_requests/{{entry}} is not a .http file — _requests/ contains only complete .http request files (C2 — see docs/
|
|
1916
|
+
messages: { notHttp: "_requests/{{entry}} is not a .http file — _requests/ contains only complete .http request files (C2 — see docs/13-linting.md)." },
|
|
1936
1917
|
type: "problem"
|
|
1937
1918
|
}
|
|
1938
1919
|
};
|
|
@@ -2156,18 +2137,18 @@ const c4ContractShape = {
|
|
|
2156
2137
|
meta: {
|
|
2157
2138
|
docs: RULE_DOCS["c4-contract-shape"],
|
|
2158
2139
|
messages: {
|
|
2159
|
-
badName: "Contract unit \"{{base}}\" must be named <kebab-name>.ts — the folder already carries the provider (C4 — see docs/
|
|
2160
|
-
badProviderDir: "contracts/{{entry}}/ is not a provider directory — a contracts/ root holds only http/, openai/, anthropic/ and *.contracts.ts files (C4 — see docs/
|
|
2161
|
-
missingComposite: "Contract facade \"{{base}}\" has no default export — it must default-export the composed world (C4 — see docs/
|
|
2162
|
-
missingDefault: "Contract unit \"{{base}}\" has no default export — a unit default-exports `defineContract(...)` or a factory returning one (C4 — see docs/
|
|
2163
|
-
notDefineContract: "The default export of a contract unit must be `defineContract(...)` or a factory returning a contract (C4 — see docs/
|
|
2164
|
-
notDefineContracts: "The default export of \"{{base}}\" must be built from `defineContracts(...)` (or a composition re-export) (C4 — see docs/
|
|
2165
|
-
providerEntry: "contracts/{{provider}}/{{child}} is neither a *.ts contract nor a *.response.json payload (C4 — see docs/
|
|
2166
|
-
providerMismatch: "This contract lives in contracts/{{provider}}/ but its request is a `{{declared}}.*` builder — the folder carries the provider (C4 — see docs/
|
|
2167
|
-
providerSubfolder: "contracts/{{provider}}/{{child}}/ is nested — a provider folder is flat (C4 — see docs/
|
|
2168
|
-
rootEntry: "contracts/{{entry}} is not a *.contracts.ts facade — data files live in a provider folder next to their contract (C4 — see docs/
|
|
2169
|
-
rootFile: "Contract file \"{{base}}\" sits at the contracts/ root, which holds only *.contracts.ts facades — a unit contract belongs in http/, openai/ or anthropic/ (C4 — see docs/
|
|
2170
|
-
tooDeep: "Contract file \"{{base}}\" is nested deeper than contracts/<provider>/ (C4 — see docs/
|
|
2140
|
+
badName: "Contract unit \"{{base}}\" must be named <kebab-name>.ts — the folder already carries the provider (C4 — see docs/13-linting.md).",
|
|
2141
|
+
badProviderDir: "contracts/{{entry}}/ is not a provider directory — a contracts/ root holds only http/, openai/, anthropic/ and *.contracts.ts files (C4 — see docs/13-linting.md).",
|
|
2142
|
+
missingComposite: "Contract facade \"{{base}}\" has no default export — it must default-export the composed world (C4 — see docs/13-linting.md).",
|
|
2143
|
+
missingDefault: "Contract unit \"{{base}}\" has no default export — a unit default-exports `defineContract(...)` or a factory returning one (C4 — see docs/13-linting.md).",
|
|
2144
|
+
notDefineContract: "The default export of a contract unit must be `defineContract(...)` or a factory returning a contract (C4 — see docs/13-linting.md).",
|
|
2145
|
+
notDefineContracts: "The default export of \"{{base}}\" must be built from `defineContracts(...)` (or a composition re-export) (C4 — see docs/13-linting.md).",
|
|
2146
|
+
providerEntry: "contracts/{{provider}}/{{child}} is neither a *.ts contract nor a *.response.json payload (C4 — see docs/13-linting.md).",
|
|
2147
|
+
providerMismatch: "This contract lives in contracts/{{provider}}/ but its request is a `{{declared}}.*` builder — the folder carries the provider (C4 — see docs/13-linting.md).",
|
|
2148
|
+
providerSubfolder: "contracts/{{provider}}/{{child}}/ is nested — a provider folder is flat (C4 — see docs/13-linting.md).",
|
|
2149
|
+
rootEntry: "contracts/{{entry}} is not a *.contracts.ts facade — data files live in a provider folder next to their contract (C4 — see docs/13-linting.md).",
|
|
2150
|
+
rootFile: "Contract file \"{{base}}\" sits at the contracts/ root, which holds only *.contracts.ts facades — a unit contract belongs in http/, openai/ or anthropic/ (C4 — see docs/13-linting.md).",
|
|
2151
|
+
tooDeep: "Contract file \"{{base}}\" is nested deeper than contracts/<provider>/ (C4 — see docs/13-linting.md)."
|
|
2171
2152
|
},
|
|
2172
2153
|
type: "problem"
|
|
2173
2154
|
}
|
|
@@ -2218,7 +2199,7 @@ const c6ToMatchExtension = {
|
|
|
2218
2199
|
},
|
|
2219
2200
|
meta: {
|
|
2220
2201
|
docs: RULE_DOCS["c6-tomatch-extension"],
|
|
2221
|
-
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/
|
|
2202
|
+
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/13-linting.md)." },
|
|
2222
2203
|
type: "problem"
|
|
2223
2204
|
}
|
|
2224
2205
|
};
|
|
@@ -2246,7 +2227,7 @@ const c7SeedsSqlOnly = {
|
|
|
2246
2227
|
},
|
|
2247
2228
|
meta: {
|
|
2248
2229
|
docs: RULE_DOCS["c7-seeds-sql-only"],
|
|
2249
|
-
messages: { notSql: "_seeds/{{entry}} is not a .sql file — _seeds/ carries database state only; file state goes through .fixture() (C7 — see docs/
|
|
2230
|
+
messages: { notSql: "_seeds/{{entry}} is not a .sql file — _seeds/ carries database state only; file state goes through .fixture() (C7 — see docs/13-linting.md)." },
|
|
2250
2231
|
type: "problem"
|
|
2251
2232
|
}
|
|
2252
2233
|
};
|
|
@@ -2323,7 +2304,7 @@ const c8ReferencedFixtureExists = {
|
|
|
2323
2304
|
},
|
|
2324
2305
|
meta: {
|
|
2325
2306
|
docs: RULE_DOCS["c8-referenced-fixture-exists"],
|
|
2326
|
-
messages: { missing: "Referenced fixture \"{{path}}\" does not exist on disk under its ground {{root}}/ root (C8 — see docs/
|
|
2307
|
+
messages: { missing: "Referenced fixture \"{{path}}\" does not exist on disk under its ground {{root}}/ root (C8 — see docs/13-linting.md). Create it or fix the reference." },
|
|
2327
2308
|
type: "problem"
|
|
2328
2309
|
}
|
|
2329
2310
|
};
|
|
@@ -2392,7 +2373,7 @@ const d12wResponseBodyProbe = {
|
|
|
2392
2373
|
meta: {
|
|
2393
2374
|
docs: RULE_DOCS["d12w-response-body-probe"],
|
|
2394
2375
|
defaultOptions: [{ threshold: DEFAULT_THRESHOLD }],
|
|
2395
|
-
messages: { bodyProbeCluster: "{{count}} body probes in one test — this wants a full golden: expect(result.response).toMatch('x.http') (D12 — see docs/
|
|
2376
|
+
messages: { bodyProbeCluster: "{{count}} body probes in one test — this wants a full golden: expect(result.response).toMatch('x.http') (D12 — see docs/13-linting.md)." },
|
|
2396
2377
|
schema: [{
|
|
2397
2378
|
additionalProperties: false,
|
|
2398
2379
|
properties: { threshold: {
|
|
@@ -2475,7 +2456,7 @@ const d13wUnfrozenNegativeFixture = {
|
|
|
2475
2456
|
},
|
|
2476
2457
|
meta: {
|
|
2477
2458
|
docs: RULE_DOCS["d13w-unfrozen-negative-fixture"],
|
|
2478
|
-
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/
|
|
2459
|
+
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/13-linting.md)." },
|
|
2479
2460
|
type: "suggestion"
|
|
2480
2461
|
}
|
|
2481
2462
|
};
|
|
@@ -2551,7 +2532,7 @@ const d15wStatusOnlyProbe = {
|
|
|
2551
2532
|
},
|
|
2552
2533
|
meta: {
|
|
2553
2534
|
docs: RULE_DOCS["d15w-status-only-probe"],
|
|
2554
|
-
messages: { statusOnlyProbe: "The test's only assertion(s) are status probes — pin the full response: expect(result.response).toMatch('case.http') (D11 — see docs/
|
|
2535
|
+
messages: { statusOnlyProbe: "The test's only assertion(s) are status probes — pin the full response: expect(result.response).toMatch('case.http') (D11 — see docs/13-linting.md)." },
|
|
2555
2536
|
type: "suggestion"
|
|
2556
2537
|
}
|
|
2557
2538
|
};
|
|
@@ -2606,7 +2587,7 @@ const d2AwaitIoMatcher = {
|
|
|
2606
2587
|
},
|
|
2607
2588
|
meta: {
|
|
2608
2589
|
docs: RULE_DOCS["d2-await-io-matcher"],
|
|
2609
|
-
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/
|
|
2590
|
+
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/13-linting.md)." },
|
|
2610
2591
|
type: "problem"
|
|
2611
2592
|
}
|
|
2612
2593
|
};
|
|
@@ -2660,7 +2641,7 @@ const d2wAwaitSyncMatcher = {
|
|
|
2660
2641
|
},
|
|
2661
2642
|
meta: {
|
|
2662
2643
|
docs: RULE_DOCS["d2w-await-sync-matcher"],
|
|
2663
|
-
messages: { redundantAwait: "await on expect(…).{{matcher}}(…) is redundant — that matcher is synchronous; only IO-backed matchers (toMatchRows/toBeEmpty/toBeRunning) are awaited (D2 — see docs/
|
|
2644
|
+
messages: { redundantAwait: "await on expect(…).{{matcher}}(…) is redundant — that matcher is synchronous; only IO-backed matchers (toMatchRows/toBeEmpty/toBeRunning) are awaited (D2 — see docs/13-linting.md)." },
|
|
2664
2645
|
type: "suggestion"
|
|
2665
2646
|
}
|
|
2666
2647
|
};
|
|
@@ -2737,7 +2718,7 @@ const d6wTransformTokenEquivalent = {
|
|
|
2737
2718
|
},
|
|
2738
2719
|
meta: {
|
|
2739
2720
|
docs: RULE_DOCS["d6w-transform-token-equivalent"],
|
|
2740
|
-
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/
|
|
2721
|
+
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/13-linting.md)." },
|
|
2741
2722
|
type: "suggestion"
|
|
2742
2723
|
}
|
|
2743
2724
|
};
|
|
@@ -2771,7 +2752,7 @@ const d8wTextBypass = {
|
|
|
2771
2752
|
},
|
|
2772
2753
|
meta: {
|
|
2773
2754
|
docs: RULE_DOCS["d8w-text-bypass"],
|
|
2774
|
-
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/
|
|
2755
|
+
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/13-linting.md)." },
|
|
2775
2756
|
type: "suggestion"
|
|
2776
2757
|
}
|
|
2777
2758
|
};
|
|
@@ -2854,7 +2835,7 @@ const d9wSingleUseRef = {
|
|
|
2854
2835
|
},
|
|
2855
2836
|
meta: {
|
|
2856
2837
|
docs: RULE_DOCS["d9w-single-use-ref"],
|
|
2857
|
-
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/
|
|
2838
|
+
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/13-linting.md)." },
|
|
2858
2839
|
type: "suggestion"
|
|
2859
2840
|
}
|
|
2860
2841
|
};
|
|
@@ -2948,7 +2929,7 @@ const f1NoSubpathImport = {
|
|
|
2948
2929
|
},
|
|
2949
2930
|
meta: {
|
|
2950
2931
|
docs: RULE_DOCS["f1-no-subpath-import"],
|
|
2951
|
-
messages: { subpath: "Import from \"@jterrazz/test\", not \"{{source}}\" — internal subpaths do not exist (F1 — see docs/
|
|
2932
|
+
messages: { subpath: "Import from \"@jterrazz/test\", not \"{{source}}\" — internal subpaths do not exist (F1 — see docs/13-linting.md). Published subpaths, exempt everywhere: {{published}}." },
|
|
2952
2933
|
type: "problem"
|
|
2953
2934
|
}
|
|
2954
2935
|
};
|
|
@@ -2996,7 +2977,7 @@ const f2NoTestImportsInProd = {
|
|
|
2996
2977
|
},
|
|
2997
2978
|
meta: {
|
|
2998
2979
|
docs: RULE_DOCS["f2-no-test-imports-in-prod"],
|
|
2999
|
-
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/
|
|
2980
|
+
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/13-linting.md)." },
|
|
3000
2981
|
type: "problem"
|
|
3001
2982
|
}
|
|
3002
2983
|
};
|
|
@@ -3059,7 +3040,7 @@ const f3SpecsPublicEntry = {
|
|
|
3059
3040
|
},
|
|
3060
3041
|
meta: {
|
|
3061
3042
|
docs: RULE_DOCS["f3-specs-public-entry"],
|
|
3062
|
-
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/
|
|
3043
|
+
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; only @jterrazz/test/oxlint, and src/integrations/** from specs/integrations/, are exempt)." },
|
|
3063
3044
|
type: "problem"
|
|
3064
3045
|
}
|
|
3065
3046
|
};
|
|
@@ -3085,7 +3066,7 @@ const f4NoTestToTestImport = {
|
|
|
3085
3066
|
},
|
|
3086
3067
|
meta: {
|
|
3087
3068
|
docs: RULE_DOCS["f4-no-test-to-test-import"],
|
|
3088
|
-
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/
|
|
3069
|
+
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/13-linting.md)." },
|
|
3089
3070
|
type: "problem"
|
|
3090
3071
|
}
|
|
3091
3072
|
};
|
|
@@ -3111,7 +3092,7 @@ const f5FixturesOnlyFromTests = {
|
|
|
3111
3092
|
},
|
|
3112
3093
|
meta: {
|
|
3113
3094
|
docs: RULE_DOCS["f5-fixtures-only-from-tests"],
|
|
3114
|
-
messages: { fixturesImport: "A *.fixtures.ts module (\"{{source}}\") is only importable from *.test.ts files (F5 — see docs/
|
|
3095
|
+
messages: { fixturesImport: "A *.fixtures.ts module (\"{{source}}\") is only importable from *.test.ts files (F5 — see docs/13-linting.md)." },
|
|
3115
3096
|
type: "problem"
|
|
3116
3097
|
}
|
|
3117
3098
|
};
|
|
@@ -3203,8 +3184,8 @@ const i1LayerBoundaries = {
|
|
|
3203
3184
|
defaultOptions: [{ layers: {} }],
|
|
3204
3185
|
docs: RULE_DOCS["i1-layer-boundaries"],
|
|
3205
3186
|
messages: {
|
|
3206
|
-
crossLayer: "Layer \"{{layer}}\" must not import \"{{source}}\" — outside the edges its layer map declares (I1 — see docs/
|
|
3207
|
-
foreignDependency: "\"{{source}}\" is not a dependency layer \"{{layer}}\" declares — one layer states the packages it may import (I1 — see docs/
|
|
3187
|
+
crossLayer: "Layer \"{{layer}}\" must not import \"{{source}}\" — outside the edges its layer map declares (I1 — see docs/13-linting.md).",
|
|
3188
|
+
foreignDependency: "\"{{source}}\" is not a dependency layer \"{{layer}}\" declares — one layer states the packages it may import (I1 — see docs/13-linting.md)."
|
|
3208
3189
|
},
|
|
3209
3190
|
schema: [{
|
|
3210
3191
|
additionalProperties: false,
|
|
@@ -3292,9 +3273,9 @@ const i2SiblingTestNaming = {
|
|
|
3292
3273
|
meta: {
|
|
3293
3274
|
docs: RULE_DOCS["i2-sibling-test-naming"],
|
|
3294
3275
|
messages: {
|
|
3295
|
-
orphanTest: "A src/ module test must sit NEXT to the module it tests — no neighbour \"{{expected}}\" found (I2 — see docs/
|
|
3296
|
-
rootTests: "A root-level tests/ directory is banned — module tests are siblings under src/, product specifications live in specs/ (I2 — see docs/
|
|
3297
|
-
testsDir: "__tests__/ directories are banned under src/ — the test of <file>.ts is its neighbour <file>.test.ts (I2 — see docs/
|
|
3276
|
+
orphanTest: "A src/ module test must sit NEXT to the module it tests — no neighbour \"{{expected}}\" found (I2 — see docs/13-linting.md). A test needing more than its module is a specification: move it to specs/.",
|
|
3277
|
+
rootTests: "A root-level tests/ directory is banned — module tests are siblings under src/, product specifications live in specs/ (I2 — see docs/13-linting.md).",
|
|
3278
|
+
testsDir: "__tests__/ directories are banned under src/ — the test of <file>.ts is its neighbour <file>.test.ts (I2 — see docs/13-linting.md)."
|
|
3298
3279
|
},
|
|
3299
3280
|
type: "problem"
|
|
3300
3281
|
}
|
|
@@ -3394,57 +3375,14 @@ const i4NoViMockInSrc = {
|
|
|
3394
3375
|
meta: {
|
|
3395
3376
|
docs: RULE_DOCS["i4-no-vi-mock-in-src"],
|
|
3396
3377
|
messages: {
|
|
3397
|
-
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/
|
|
3398
|
-
bannedDir: "`{{dir}}/` directories are banned under src/ — mocks and data are code: mockOf/mockOfDate inline, payloads in a *.fixtures.ts neighbour (I4 — see docs/
|
|
3399
|
-
viMock: "`vi.mock` is banned under src/ — use mockOf/mockOfDate (I4 — see docs/
|
|
3378
|
+
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/13-linting.md).",
|
|
3379
|
+
bannedDir: "`{{dir}}/` directories are banned under src/ — mocks and data are code: mockOf/mockOfDate inline, payloads in a *.fixtures.ts neighbour (I4 — see docs/13-linting.md).",
|
|
3380
|
+
viMock: "`vi.mock` is banned under src/ — use mockOf/mockOfDate (I4 — see docs/13-linting.md)."
|
|
3400
3381
|
},
|
|
3401
3382
|
type: "problem"
|
|
3402
3383
|
}
|
|
3403
3384
|
};
|
|
3404
3385
|
//#endregion
|
|
3405
|
-
//#region src/lint/rules/j1-no-only-skip.ts
|
|
3406
|
-
/** Test runners whose `.only` / `.skip` modifiers must never be committed. */
|
|
3407
|
-
const RUNNERS = /* @__PURE__ */ new Set([
|
|
3408
|
-
"describe",
|
|
3409
|
-
"it",
|
|
3410
|
-
"test"
|
|
3411
|
-
]);
|
|
3412
|
-
/** The focus/skip modifiers CONVENTIONS J1 forbids. */
|
|
3413
|
-
const FORBIDDEN = /* @__PURE__ */ new Set(["only", "skip"]);
|
|
3414
|
-
/**
|
|
3415
|
-
* CONVENTIONS J1 — no committed `.only` / `.skip`.
|
|
3416
|
-
*
|
|
3417
|
-
* Flags the member form used across the specs: `describe.only`, `test.only`,
|
|
3418
|
-
* `it.only` and their `.skip` counterparts. Modifiers that legitimately gate a
|
|
3419
|
-
* test (`test.skipIf`, `test.runIf`) are untouched — only the bare `only`/`skip`
|
|
3420
|
-
* that pin or disable a test in a commit are reported.
|
|
3421
|
-
*/
|
|
3422
|
-
const j1NoOnlySkip = {
|
|
3423
|
-
create(context) {
|
|
3424
|
-
return { MemberExpression(node) {
|
|
3425
|
-
if (node.computed === true) return;
|
|
3426
|
-
const object = node.object;
|
|
3427
|
-
const property = node.property;
|
|
3428
|
-
if (object?.type !== "Identifier" || property?.type !== "Identifier") return;
|
|
3429
|
-
const runner = object.name;
|
|
3430
|
-
const modifier = property.name;
|
|
3431
|
-
if (RUNNERS.has(runner) && FORBIDDEN.has(modifier)) context.report({
|
|
3432
|
-
data: {
|
|
3433
|
-
modifier,
|
|
3434
|
-
runner
|
|
3435
|
-
},
|
|
3436
|
-
messageId: "forbidden",
|
|
3437
|
-
node
|
|
3438
|
-
});
|
|
3439
|
-
} };
|
|
3440
|
-
},
|
|
3441
|
-
meta: {
|
|
3442
|
-
docs: RULE_DOCS["j1-no-only-skip"],
|
|
3443
|
-
messages: { forbidden: "Committed `{{runner}}.{{modifier}}` is not allowed (J1 — see docs/10-linting.md). Remove it before committing." },
|
|
3444
|
-
type: "problem"
|
|
3445
|
-
}
|
|
3446
|
-
};
|
|
3447
|
-
//#endregion
|
|
3448
3386
|
//#region src/lint/rules/j2-no-sleep-in-specs.ts
|
|
3449
3387
|
/**
|
|
3450
3388
|
* CONVENTIONS J2 — spec files (`specs/**`) contain no arbitrary sleeps:
|
|
@@ -3483,138 +3421,13 @@ const j2NoSleepInSpecs = {
|
|
|
3483
3421
|
meta: {
|
|
3484
3422
|
docs: RULE_DOCS["j2-no-sleep-in-specs"],
|
|
3485
3423
|
messages: {
|
|
3486
|
-
sleep: "No arbitrary sleeps in specs — synchronise via `waitFor` or the framework (J2 — see docs/
|
|
3487
|
-
timersImport: "No timer-based sleeps in specs — synchronise via `waitFor` or the framework (J2 — see docs/
|
|
3424
|
+
sleep: "No arbitrary sleeps in specs — synchronise via `waitFor` or the framework (J2 — see docs/13-linting.md).",
|
|
3425
|
+
timersImport: "No timer-based sleeps in specs — synchronise via `waitFor` or the framework (J2 — see docs/13-linting.md)."
|
|
3488
3426
|
},
|
|
3489
3427
|
type: "problem"
|
|
3490
3428
|
}
|
|
3491
3429
|
};
|
|
3492
3430
|
//#endregion
|
|
3493
|
-
//#region src/lint/rules/j3-no-expectless-test.ts
|
|
3494
|
-
/** Does `callback` contain at least one `expect(…)` call? */
|
|
3495
|
-
function hasExpect(callback) {
|
|
3496
|
-
let found = false;
|
|
3497
|
-
walk(callback, (node) => {
|
|
3498
|
-
if (found || node.type !== "CallExpression") return;
|
|
3499
|
-
const callee = node.callee;
|
|
3500
|
-
if (callee?.type === "Identifier" && callee.name === "expect") found = true;
|
|
3501
|
-
});
|
|
3502
|
-
return found;
|
|
3503
|
-
}
|
|
3504
|
-
/**
|
|
3505
|
-
* CONVENTIONS J3 — a test asserts something. A `test('…', () => { … })` whose
|
|
3506
|
-
* body contains no `expect(…)` is either dead or a silent no-op.
|
|
3507
|
-
*
|
|
3508
|
-
* Reuses the shared B4 test-shape helpers. `test.todo(...)` (and any other
|
|
3509
|
-
* callback-less form) is skipped — there is nothing to assert yet.
|
|
3510
|
-
*/
|
|
3511
|
-
const j3NoExpectlessTest = {
|
|
3512
|
-
create(context) {
|
|
3513
|
-
return { CallExpression(node) {
|
|
3514
|
-
const callee = node.callee;
|
|
3515
|
-
if (!isTestCallee(callee)) return;
|
|
3516
|
-
if (testCalleeHasModifier(callee, "todo")) return;
|
|
3517
|
-
const callback = findTestCallback(node.arguments ?? []);
|
|
3518
|
-
if (callback === void 0) return;
|
|
3519
|
-
if (!hasExpect(callback)) context.report({
|
|
3520
|
-
messageId: "noExpect",
|
|
3521
|
-
node
|
|
3522
|
-
});
|
|
3523
|
-
} };
|
|
3524
|
-
},
|
|
3525
|
-
meta: {
|
|
3526
|
-
docs: RULE_DOCS["j3-no-expectless-test"],
|
|
3527
|
-
messages: { noExpect: "Test has no `expect(…)` — a test must assert something (J3 — see docs/10-linting.md). Use `test.todo` for a pending test." },
|
|
3528
|
-
type: "problem"
|
|
3529
|
-
}
|
|
3530
|
-
};
|
|
3531
|
-
//#endregion
|
|
3532
|
-
//#region src/lint/rules/j4-unique-test-names.ts
|
|
3533
|
-
/**
|
|
3534
|
-
* CONVENTIONS J4 — test names are the sole description of behaviour (B3), so two
|
|
3535
|
-
* tests sharing a literal name in one file are a copy-paste smell: one shadows
|
|
3536
|
-
* the other in the reporter. Flags the second (and later) occurrence.
|
|
3537
|
-
*
|
|
3538
|
-
* `.each` / parametrized tests are skipped — their name is a template expanded
|
|
3539
|
-
* per row, collisions there are by construction.
|
|
3540
|
-
*/
|
|
3541
|
-
const j4UniqueTestNames = {
|
|
3542
|
-
create(context) {
|
|
3543
|
-
const seen = /* @__PURE__ */ new Set();
|
|
3544
|
-
return { CallExpression(node) {
|
|
3545
|
-
const callee = node.callee;
|
|
3546
|
-
if (!isTestCallee(callee)) return;
|
|
3547
|
-
if (testCalleeHasModifier(callee, "each")) return;
|
|
3548
|
-
const name = testName(node);
|
|
3549
|
-
if (name === void 0) return;
|
|
3550
|
-
if (seen.has(name)) {
|
|
3551
|
-
context.report({
|
|
3552
|
-
data: { name },
|
|
3553
|
-
messageId: "duplicate",
|
|
3554
|
-
node
|
|
3555
|
-
});
|
|
3556
|
-
return;
|
|
3557
|
-
}
|
|
3558
|
-
seen.add(name);
|
|
3559
|
-
} };
|
|
3560
|
-
},
|
|
3561
|
-
meta: {
|
|
3562
|
-
docs: RULE_DOCS["j4-unique-test-names"],
|
|
3563
|
-
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)." },
|
|
3564
|
-
type: "problem"
|
|
3565
|
-
}
|
|
3566
|
-
};
|
|
3567
|
-
//#endregion
|
|
3568
|
-
//#region src/lint/rules/j5-lowercase-title.ts
|
|
3569
|
-
/** The block/test introducers whose title literal C1' constrains. */
|
|
3570
|
-
const TITLED = /* @__PURE__ */ new Set([
|
|
3571
|
-
"describe",
|
|
3572
|
-
"it",
|
|
3573
|
-
"test"
|
|
3574
|
-
]);
|
|
3575
|
-
/** An identifier-shaped, all-caps/underscored first word: `VALID_CATEGORIES`,
|
|
3576
|
-
* `HTTP`, `DI`. Such a word names a symbol verbatim — lowercasing it would
|
|
3577
|
-
* misspell it — so it is exempt. A prose word (`Rejects`, `Builds`) is not. */
|
|
3578
|
-
const ALL_CAPS_IDENTIFIER = /^[A-Z][A-Z0-9_]*$/u;
|
|
3579
|
-
/**
|
|
3580
|
-
* CONVENTIONS J5 — the first character of a `test('…')` / `describe('…')` title
|
|
3581
|
-
* literal must be lowercase.
|
|
3582
|
-
*
|
|
3583
|
-
* The test name is the sole description of behaviour (B3): a sentence fragment,
|
|
3584
|
-
* lowercase-led like prose (`'rejects an unknown job'`). Exempt are titles
|
|
3585
|
-
* whose first WORD is an identifier-shaped all-caps/underscored token
|
|
3586
|
-
* (`'VALID_CATEGORIES is rejected'`, `'HTTP is upgraded'`, `'DI wires …'`) —
|
|
3587
|
-
* lowercasing a named symbol would misspell it — and titles opening on a
|
|
3588
|
-
* non-letter (a `$`, a `{`, a `%s` placeholder). Only lowercase-able prose
|
|
3589
|
-
* first words are enforced. Covers the modifier and parametrized forms
|
|
3590
|
-
* (`test.only`, `describe.each(...)(...)`) via the chain root.
|
|
3591
|
-
*/
|
|
3592
|
-
const j5LowercaseTitle = {
|
|
3593
|
-
create(context) {
|
|
3594
|
-
return { CallExpression(node) {
|
|
3595
|
-
const callee = node.callee;
|
|
3596
|
-
const root = chainRootName(callee);
|
|
3597
|
-
if (root === void 0 || !TITLED.has(root)) return;
|
|
3598
|
-
const title = stringValue(node.arguments?.[0]);
|
|
3599
|
-
if (title === void 0) return;
|
|
3600
|
-
const first = [...title][0];
|
|
3601
|
-
if (first === void 0 || !/\p{L}/u.test(first)) return;
|
|
3602
|
-
const firstWord = title.split(/\s+/u)[0];
|
|
3603
|
-
if (ALL_CAPS_IDENTIFIER.test(firstWord)) return;
|
|
3604
|
-
if (first !== first.toLocaleLowerCase()) context.report({
|
|
3605
|
-
data: { runner: root },
|
|
3606
|
-
messageId: "uppercase",
|
|
3607
|
-
node
|
|
3608
|
-
});
|
|
3609
|
-
} };
|
|
3610
|
-
},
|
|
3611
|
-
meta: {
|
|
3612
|
-
docs: RULE_DOCS["j5-lowercase-title"],
|
|
3613
|
-
messages: { uppercase: "A {{runner}}() title must start lowercase — the test name is a prose fragment, not a sentence (J5 — see docs/10-linting.md)." },
|
|
3614
|
-
type: "problem"
|
|
3615
|
-
}
|
|
3616
|
-
};
|
|
3617
|
-
//#endregion
|
|
3618
3431
|
//#region src/lint/rules/w1-scenario-pure.ts
|
|
3619
3432
|
/** The scenario-carrying terminal actions: `.visit()` (website) and `.open()` (mobile). */
|
|
3620
3433
|
const SCENARIO_ACTIONS$1 = /* @__PURE__ */ new Set(["open", "visit"]);
|
|
@@ -3644,7 +3457,7 @@ const w1ScenarioPure = {
|
|
|
3644
3457
|
},
|
|
3645
3458
|
meta: {
|
|
3646
3459
|
docs: RULE_DOCS["w1-scenario-pure"],
|
|
3647
|
-
messages: { expectInScenario: "No expect() inside a scenario — the scenario is the When; assert the final state on the returned result in the Then (W1 — see docs/
|
|
3460
|
+
messages: { expectInScenario: "No expect() inside a scenario — the scenario is the When; assert the final state on the returned result in the Then (W1 — see docs/13-linting.md)." },
|
|
3648
3461
|
type: "problem"
|
|
3649
3462
|
}
|
|
3650
3463
|
};
|
|
@@ -3660,7 +3473,7 @@ const SCENARIO_ACTIONS = /* @__PURE__ */ new Set(["open", "visit"]);
|
|
|
3660
3473
|
*
|
|
3661
3474
|
* Registered in a consumer's (or this repo's own) `oxlint.config.ts` via
|
|
3662
3475
|
* `jsPlugins: ['@jterrazz/test/oxlint']` and referenced as `jterrazz/<rule>` in
|
|
3663
|
-
* the `rules` map, e.g. `'jterrazz/
|
|
3476
|
+
* the `rules` map, e.g. `'jterrazz/j2-no-sleep-in-specs': 'error'` — or enabled
|
|
3664
3477
|
* wholesale by spreading {@link recommendedRules}.
|
|
3665
3478
|
*
|
|
3666
3479
|
* This entry is bundled by tsdown (`dist/oxlint.js`); rules import nothing from
|
|
@@ -3710,11 +3523,7 @@ const plugin = {
|
|
|
3710
3523
|
"i1-layer-boundaries": i1LayerBoundaries,
|
|
3711
3524
|
"i2-sibling-test-naming": i2SiblingTestNaming,
|
|
3712
3525
|
"i4-no-vi-mock-in-src": i4NoViMockInSrc,
|
|
3713
|
-
"j1-no-only-skip": j1NoOnlySkip,
|
|
3714
3526
|
"j2-no-sleep-in-specs": j2NoSleepInSpecs,
|
|
3715
|
-
"j3-no-expectless-test": j3NoExpectlessTest,
|
|
3716
|
-
"j4-unique-test-names": j4UniqueTestNames,
|
|
3717
|
-
"j5-lowercase-title": j5LowercaseTitle,
|
|
3718
3527
|
"w1-scenario-pure": w1ScenarioPure,
|
|
3719
3528
|
"w2w-user-facing-elements": {
|
|
3720
3529
|
create(context) {
|
|
@@ -3736,7 +3545,7 @@ const plugin = {
|
|
|
3736
3545
|
},
|
|
3737
3546
|
meta: {
|
|
3738
3547
|
docs: RULE_DOCS["w2w-user-facing-elements"],
|
|
3739
|
-
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/
|
|
3548
|
+
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/13-linting.md)." },
|
|
3740
3549
|
type: "suggestion"
|
|
3741
3550
|
}
|
|
3742
3551
|
}
|