@jterrazz/test 16.1.0 → 16.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/appium.adapter.js +2 -2
- package/dist/browser/index.js +126 -13
- package/dist/checker.js +36 -3
- package/dist/index.d.ts +2 -0
- package/dist/oxlint.cjs +25 -3
- package/dist/oxlint.js +25 -3
- package/dist/playwright.adapter.js +94 -5
- package/dist/substring-warning.js +46 -8
- package/dist/vitest.js +20 -1
- package/docs/03-testing.md +17 -17
- package/docs/13-elements.md +6 -0
- package/docs/18-conventions.md +32 -5
- package/docs/19-linting.md +28 -26
- package/package.json +2 -2
package/dist/appium.adapter.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { i as AmbiguousElementError, n as warnSubstringOnly, o as formatElement } from "./substring-warning.js";
|
|
2
2
|
import { mkdtempSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { resolve } from "node:path";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
@@ -430,7 +430,7 @@ var AppiumAdapter = class {
|
|
|
430
430
|
const first = matches[0];
|
|
431
431
|
if (first === void 0 || await matches.length !== 1) return null;
|
|
432
432
|
const evidence = await captureMatch(first);
|
|
433
|
-
warnSubstringOnly(level, this.options.udid, evidence.label);
|
|
433
|
+
await warnSubstringOnly(level, this.options.udid, evidence.label);
|
|
434
434
|
return loose;
|
|
435
435
|
}
|
|
436
436
|
/** Enumerate the candidates' evidence, truncated. */
|
package/dist/browser/index.js
CHANGED
|
@@ -1358,6 +1358,45 @@ function describeAmbiguity(options) {
|
|
|
1358
1358
|
].join("\n");
|
|
1359
1359
|
}
|
|
1360
1360
|
//#endregion
|
|
1361
|
+
//#region src/model/elements/aria-name.ts
|
|
1362
|
+
/**
|
|
1363
|
+
* The accessible name an ARIA snapshot states.
|
|
1364
|
+
*
|
|
1365
|
+
* A descriptor matches the name the BROWSER computes, and that name is not the
|
|
1366
|
+
* element's text: `<button><span>Experiments</span><span>9</span></button>`
|
|
1367
|
+
* reads as `Experiments9` through `textContent` and is named "Experiments 9",
|
|
1368
|
+
* a field's name comes from its label and is nowhere in its text at all, and a
|
|
1369
|
+
* name cut at eighty characters resolves to nothing. Reading the name off the
|
|
1370
|
+
* snapshot is what makes the spelling the window prints a spelling that can be
|
|
1371
|
+
* written back into a spec.
|
|
1372
|
+
*
|
|
1373
|
+
* The dialect is the one both surfaces already speak — playwright's, on the
|
|
1374
|
+
* page directly and through `server.commands` for a mounted component — so the
|
|
1375
|
+
* projection is stated once, here, and neither adapter computes a name itself.
|
|
1376
|
+
*/
|
|
1377
|
+
/**
|
|
1378
|
+
* The name of the FIRST node a snapshot describes: `- button "Experiments 9"`,
|
|
1379
|
+
* optionally followed by `:` and its children.
|
|
1380
|
+
*
|
|
1381
|
+
* A quote must follow the role across a SPACE. `- paragraph: "…"` is a node
|
|
1382
|
+
* with text and no name, and reading that text as a name would hand the author
|
|
1383
|
+
* a spelling no role descriptor matches on.
|
|
1384
|
+
*/
|
|
1385
|
+
const NAMED = /^-\s+[\w-]+\s+"(?<name>(?:[^"\\]|\\.)*)"/u;
|
|
1386
|
+
/** Undo the escaping the snapshot applies to a quote or a backslash inside a name. */
|
|
1387
|
+
function unescaped(name) {
|
|
1388
|
+
return name.replaceAll(/\\(?<char>.)/gu, "$<char>");
|
|
1389
|
+
}
|
|
1390
|
+
/**
|
|
1391
|
+
* The accessible name the snapshot gives the node it describes, or `undefined`
|
|
1392
|
+
* when that node has none — an unnamed landmark, a paragraph of text.
|
|
1393
|
+
*/
|
|
1394
|
+
function accessibleNameIn(snapshot) {
|
|
1395
|
+
const [first] = snapshot.split("\n");
|
|
1396
|
+
const name = (first === void 0 ? null : NAMED.exec(first.trim()))?.groups?.name;
|
|
1397
|
+
return name === void 0 ? void 0 : unescaped(name);
|
|
1398
|
+
}
|
|
1399
|
+
//#endregion
|
|
1361
1400
|
//#region src/model/elements/substring-warning.ts
|
|
1362
1401
|
/**
|
|
1363
1402
|
* The transitional warning for a name that matches only as a SUBSTRING.
|
|
@@ -1379,6 +1418,14 @@ function describeAmbiguity(options) {
|
|
|
1379
1418
|
*/
|
|
1380
1419
|
/** How long the window lasts, stated in the message itself. */
|
|
1381
1420
|
const WINDOW = "through 16.x";
|
|
1421
|
+
/** The node side's printer — the default every adapter but the page's uses. */
|
|
1422
|
+
function toStandardError(line) {
|
|
1423
|
+
if (typeof process === "undefined") {
|
|
1424
|
+
console.warn(line);
|
|
1425
|
+
return;
|
|
1426
|
+
}
|
|
1427
|
+
process.stderr.write(`${line}\n`);
|
|
1428
|
+
}
|
|
1382
1429
|
/** Descriptors already reported in this process. */
|
|
1383
1430
|
const reported = /* @__PURE__ */ new Set();
|
|
1384
1431
|
/** The identity a descriptor is deduplicated by — kind, name, and its scope chain. */
|
|
@@ -1391,15 +1438,26 @@ function identityOf(element) {
|
|
|
1391
1438
|
* designated something as a substring. Returns whether a line was printed,
|
|
1392
1439
|
* which is what the adapters' own tests assert on.
|
|
1393
1440
|
*/
|
|
1394
|
-
function warnSubstringOnly(element, where, found) {
|
|
1441
|
+
async function warnSubstringOnly(element, where, found, print = toStandardError) {
|
|
1395
1442
|
const identity = identityOf(element);
|
|
1396
1443
|
if (reported.has(identity)) return false;
|
|
1397
1444
|
reported.add(identity);
|
|
1398
1445
|
const spelling = found === void 0 ? "" : ` The name to write is '${found.replaceAll(/\s+/gu, " ").trim()}'.`;
|
|
1399
|
-
|
|
1446
|
+
await print(`@jterrazz/test: ${element.kind}('${element.name ?? ""}') matched nothing as a whole accessible name, and matched only as a SUBSTRING of a longer one (${where}).${spelling} Names are exact from 16.0: write the name in full, scope it with \`within(…)\`, or state \`{ exact: false }\` to keep the old behaviour. The substring is still resolved, with this warning, ${WINDOW}; gone in 17.0, where the same descriptor is simply not found.`);
|
|
1400
1447
|
return true;
|
|
1401
1448
|
}
|
|
1402
1449
|
/**
|
|
1450
|
+
* How long a descriptor is given to ARRIVE before the window concludes it is
|
|
1451
|
+
* absent — the budget `settles` spends and no more.
|
|
1452
|
+
*
|
|
1453
|
+
* A count is one observation, and a verb called right after a navigating
|
|
1454
|
+
* `click()` takes it on the page being LEFT. The window may not decide
|
|
1455
|
+
* "nothing matches this whole name" on that reading: the destination has not
|
|
1456
|
+
* been parsed yet. Long enough for a navigation to commit, far short of the
|
|
1457
|
+
* verb's own 30 s, and paid only when there is a substring to widen to.
|
|
1458
|
+
*/
|
|
1459
|
+
const WINDOW_SETTLE_MS = 5e3;
|
|
1460
|
+
/**
|
|
1403
1461
|
* The descriptor to retry with, LEVEL by level, or `undefined` when nothing
|
|
1404
1462
|
* would change.
|
|
1405
1463
|
*
|
|
@@ -1416,8 +1474,8 @@ function warnSubstringOnly(element, where, found) {
|
|
|
1416
1474
|
*
|
|
1417
1475
|
* Asked BEFORE the action, not after it fails: a verb waits its whole
|
|
1418
1476
|
* actionability budget, which is the test's own budget, so a retry afterwards
|
|
1419
|
-
* never runs — the test is already over. The
|
|
1420
|
-
*
|
|
1477
|
+
* never runs — the test is already over. The common case (the name matches
|
|
1478
|
+
* whole) costs one count per level, which waits for nothing.
|
|
1421
1479
|
*/
|
|
1422
1480
|
async function widenedForWindow(element, where, probe) {
|
|
1423
1481
|
const chain = [];
|
|
@@ -1437,8 +1495,16 @@ async function widenedForWindow(element, where, probe) {
|
|
|
1437
1495
|
}
|
|
1438
1496
|
/**
|
|
1439
1497
|
* The one level, widened and warned about, or `undefined` when it designates
|
|
1440
|
-
* something already, states its own `exact`, carries no name,
|
|
1441
|
-
*
|
|
1498
|
+
* something already, states its own `exact`, carries no name, would designate
|
|
1499
|
+
* nothing either way, or turns out to be exact on the surface that ARRIVES.
|
|
1500
|
+
*
|
|
1501
|
+
* The last is the navigation case, and it is the reason the verdict is not two
|
|
1502
|
+
* counts: a `see()` fired right after a navigating `click()` counts the page
|
|
1503
|
+
* being left, so a descriptor that is perfectly exact on the DESTINATION read
|
|
1504
|
+
* as substring-only — non-deterministically, since which page each count
|
|
1505
|
+
* landed on was a race. A widening is therefore only decided once the exact
|
|
1506
|
+
* descriptor has been given the settle budget and still designates nothing.
|
|
1507
|
+
* The tell of the old fault was the printed name equalling the name written.
|
|
1442
1508
|
*/
|
|
1443
1509
|
async function widenedLevel(candidate, where, probe) {
|
|
1444
1510
|
if (candidate.exact !== void 0 || candidate.name === void 0) return;
|
|
@@ -1449,12 +1515,23 @@ async function widenedLevel(candidate, where, probe) {
|
|
|
1449
1515
|
exact: false
|
|
1450
1516
|
};
|
|
1451
1517
|
if (await probe.count(loose) !== 1) return;
|
|
1452
|
-
|
|
1518
|
+
if (!alreadyJudged(candidate) && await probe.settles(candidate)) return;
|
|
1519
|
+
await warnSubstringOnly(candidate, where, await probe.nameOf(loose), probe.print);
|
|
1453
1520
|
return loose;
|
|
1454
1521
|
} catch {
|
|
1455
1522
|
return;
|
|
1456
1523
|
}
|
|
1457
1524
|
}
|
|
1525
|
+
/**
|
|
1526
|
+
* Has this descriptor already been judged substring-only in this run?
|
|
1527
|
+
*
|
|
1528
|
+
* The verdict is made once: a scenario that clicks the same link in ten tests
|
|
1529
|
+
* pays the settle budget on the first of them and is widened straight away on
|
|
1530
|
+
* the nine after it.
|
|
1531
|
+
*/
|
|
1532
|
+
function alreadyJudged(element) {
|
|
1533
|
+
return reported.has(identityOf(element));
|
|
1534
|
+
}
|
|
1458
1535
|
//#endregion
|
|
1459
1536
|
//#region src/seams/vitest-browser/vitest-browser.adapter.ts
|
|
1460
1537
|
/**
|
|
@@ -1549,13 +1626,49 @@ function ambiguousLevel(element) {
|
|
|
1549
1626
|
for (const level of chain.slice(0, -1)) if (locate(page, level).elements().length > 1) return level;
|
|
1550
1627
|
return element;
|
|
1551
1628
|
}
|
|
1629
|
+
/**
|
|
1630
|
+
* The name to WRITE for this descriptor — the whole one, never truncated.
|
|
1631
|
+
*
|
|
1632
|
+
* A role descriptor matches the accessible name the browser computed, and a
|
|
1633
|
+
* page cannot compute one: the snapshot comes back from the node side, of the
|
|
1634
|
+
* one node the locator's own selector names. Text content would be the wrong
|
|
1635
|
+
* answer — it glues inline children with no separator and carries nothing of a
|
|
1636
|
+
* field's label — except for a `text` descriptor, which matches text.
|
|
1637
|
+
*/
|
|
1638
|
+
async function wholeNameOf(element) {
|
|
1639
|
+
const locator = locate(page, element);
|
|
1640
|
+
if (element.kind !== "text") try {
|
|
1641
|
+
const named = accessibleNameIn(await commands.ariaNode(locator.selector));
|
|
1642
|
+
if (named !== void 0) return named;
|
|
1643
|
+
} catch {}
|
|
1644
|
+
const collapsed = (locator.query()?.textContent ?? "").replaceAll(/\s+/gu, " ").trim();
|
|
1645
|
+
return collapsed === "" ? void 0 : collapsed;
|
|
1646
|
+
}
|
|
1647
|
+
/**
|
|
1648
|
+
* Does the descriptor arrive if the subject is given the settle budget?
|
|
1649
|
+
*
|
|
1650
|
+
* A mounted component never navigates, but it does re-render: a subject whose
|
|
1651
|
+
* name arrives with the state after a click is the same question a page asks
|
|
1652
|
+
* across a navigation, and one window answers both. The poll is vitest's own,
|
|
1653
|
+
* with the window's budget rather than the test's — what is being waited for
|
|
1654
|
+
* here is a surface settling, not an assertion coming true.
|
|
1655
|
+
*/
|
|
1656
|
+
async function settles(element) {
|
|
1657
|
+
try {
|
|
1658
|
+
await expect.poll(() => locate(page, element).elements().length, { timeout: WINDOW_SETTLE_MS }).toBeGreaterThan(0);
|
|
1659
|
+
return true;
|
|
1660
|
+
} catch {
|
|
1661
|
+
return false;
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1552
1664
|
/** What the transitional window may ask of the mounted page. */
|
|
1553
1665
|
const PROBE = {
|
|
1554
1666
|
count: async (element) => await Promise.resolve(locate(page, element).elements().length),
|
|
1555
|
-
nameOf: async (element) =>
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
}
|
|
1667
|
+
nameOf: async (element) => await wholeNameOf(element),
|
|
1668
|
+
print: async (line) => {
|
|
1669
|
+
await commands.notify(line);
|
|
1670
|
+
},
|
|
1671
|
+
settles: async (element) => await settles(element)
|
|
1559
1672
|
};
|
|
1560
1673
|
/**
|
|
1561
1674
|
* Run one visitor action, turning "more than one match" into the W3 refusal.
|
|
@@ -2849,7 +2962,7 @@ const specification = {
|
|
|
2849
2962
|
mobile: () => refuse("specification.mobile()"),
|
|
2850
2963
|
website: () => refuse("specification.website()")
|
|
2851
2964
|
};
|
|
2852
|
-
const process = () => refuse("process()");
|
|
2965
|
+
const process$1 = () => refuse("process()");
|
|
2853
2966
|
const ProcessHandle = nodeOnlyClass("ProcessHandle");
|
|
2854
2967
|
const postgres = () => refuse("postgres()");
|
|
2855
2968
|
const redis = () => refuse("redis()");
|
|
@@ -2864,4 +2977,4 @@ function text(value) {
|
|
|
2864
2977
|
return new TextAccessor(value, "text", "");
|
|
2865
2978
|
}
|
|
2866
2979
|
//#endregion
|
|
2867
|
-
export { Matcher, ProcessHandle, anthropic, banner, button, clock, complementary, component, content, contentinfo, defineContract, defineContracts, dialog, disabled, enabled, field, focused, form, heading, http, intercept, link, listitem, main, match, mockOf, navigation, openai, option, postgres, process, redis, region, required, row, search, selected, specification, sqlite, status, table, testId, text, valued, waitUntil, within };
|
|
2980
|
+
export { Matcher, ProcessHandle, anthropic, banner, button, clock, complementary, component, content, contentinfo, defineContract, defineContracts, dialog, disabled, enabled, field, focused, form, heading, http, intercept, link, listitem, main, match, mockOf, navigation, openai, option, postgres, process$1 as process, redis, region, required, row, search, selected, specification, sqlite, status, table, testId, text, valued, waitUntil, within };
|
package/dist/checker.js
CHANGED
|
@@ -1370,6 +1370,40 @@ const JEST_CONFIG = /^jest\.config\.[cm]?[jt]s$/u;
|
|
|
1370
1370
|
const JEST_REACH = /\b(?:roots|testMatch|testPathDirs|testPathIgnorePatterns)\s*:\s*\[(?<paths>[^\]]*)\]/gu;
|
|
1371
1371
|
/** One quoted entry of such a list. */
|
|
1372
1372
|
const JEST_PATH = /['"`](?<path>[^'"`\n]*)['"`]/gu;
|
|
1373
|
+
/** The file a pnpm workspace is declared in, whichever spelling the repository uses. */
|
|
1374
|
+
const PNPM_WORKSPACE = ["pnpm-workspace.yaml", "pnpm-workspace.yml"];
|
|
1375
|
+
/** Does this directory DECLARE a workspace — npm/bun `workspaces`, or pnpm's own file? */
|
|
1376
|
+
function declaresWorkspace(dir) {
|
|
1377
|
+
if (PNPM_WORKSPACE.some((name) => existsSync(join(dir, name)))) return true;
|
|
1378
|
+
const manifest = readManifest(dir);
|
|
1379
|
+
return manifest !== null && workspacePatterns(manifest).length > 0;
|
|
1380
|
+
}
|
|
1381
|
+
/**
|
|
1382
|
+
* The workspace a member belongs to, found by walking UP from the member.
|
|
1383
|
+
*
|
|
1384
|
+
* The jest config that collects a member's tests is not always below it: in the
|
|
1385
|
+
* layout the allowance was written for, the app owning the only working React
|
|
1386
|
+
* Native toolchain roots into the library BESIDE it. Read from the member's own
|
|
1387
|
+
* anchor, the walk below it found nothing, so `cd packages/native &&
|
|
1388
|
+
* jterrazz-test-check` — which is what that member's own gate runs — reported
|
|
1389
|
+
* F8 while the root-anchored run cleared it. Two directions, two verdicts, on
|
|
1390
|
+
* one member: the chapter's contract is that a path-less run reports what the
|
|
1391
|
+
* per-root and per-member runs report.
|
|
1392
|
+
*
|
|
1393
|
+
* The walk stops at the first directory that declares a workspace, and never
|
|
1394
|
+
* leaves the checkout: a `.git` is where this repository ends and somebody
|
|
1395
|
+
* else's tree begins — a directory in a clone, a FILE in a worktree, so its
|
|
1396
|
+
* kind is never asked.
|
|
1397
|
+
*/
|
|
1398
|
+
function workspaceRootOf(memberDir) {
|
|
1399
|
+
let current = memberDir;
|
|
1400
|
+
for (;;) {
|
|
1401
|
+
if (declaresWorkspace(current) || existsSync(join(current, ".git"))) return current;
|
|
1402
|
+
const parent = dirname(current);
|
|
1403
|
+
if (parent === current) return memberDir;
|
|
1404
|
+
current = parent;
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1373
1407
|
/** Every `jest.config.*` under `rootDir`, by absolute path — read once per root. */
|
|
1374
1408
|
const jestConfigs = /* @__PURE__ */ new Map();
|
|
1375
1409
|
/** Find them, bounded by the same skips and depth the member walk uses. */
|
|
@@ -1550,8 +1584,8 @@ function simulatedDom(config, rootDir) {
|
|
|
1550
1584
|
* test can see, while dropping a retired seam means writing its facet.
|
|
1551
1585
|
*/
|
|
1552
1586
|
function seamDependencies(member) {
|
|
1553
|
-
const { label, manifest, memberDir,
|
|
1554
|
-
return declaredSeams(manifest, declaresJest(manifest) || jestReachesMember(memberDir,
|
|
1587
|
+
const { label, manifest, memberDir, subject } = member;
|
|
1588
|
+
return declaredSeams(manifest, declaresJest(manifest) || jestReachesMember(memberDir, workspaceRootOf(memberDir))).map((seam) => ({
|
|
1555
1589
|
file: join(label, "package.json"),
|
|
1556
1590
|
line: lineOfKey(memberDir, seam.name),
|
|
1557
1591
|
message: seam.kind === "carried" ? `${subject}: \`${seam.name}\` is a transitive of \`@jterrazz/test\` — remove the declaration (F8 — see docs/19-linting.md#f8-no-seam-dependency)` : `${subject}: \`${seam.name}\` is a seam \`@jterrazz/test\` replaced — the facet is \`component()\`, \`website()\`, \`clock\` or \`intercept()\`; remove the declaration (F8 — see docs/19-linting.md#f8-no-seam-dependency)`,
|
|
@@ -1590,7 +1624,6 @@ function checkMember(memberDir, rootDir) {
|
|
|
1590
1624
|
label,
|
|
1591
1625
|
manifest,
|
|
1592
1626
|
memberDir,
|
|
1593
|
-
rootDir,
|
|
1594
1627
|
subject
|
|
1595
1628
|
}),
|
|
1596
1629
|
...anchoredToRoot(checkSpecOutsideSpecs(memberDir), label)
|
package/dist/index.d.ts
CHANGED
|
@@ -1730,9 +1730,11 @@ type DomMount = (container: HTMLElement) => (() => void) | void;
|
|
|
1730
1730
|
*/
|
|
1731
1731
|
declare module 'vitest/browser' {
|
|
1732
1732
|
interface BrowserCommands {
|
|
1733
|
+
ariaNode: (selector: string) => Promise<string>;
|
|
1733
1734
|
ariaTree: () => Promise<string>;
|
|
1734
1735
|
goldenRead: (name: string) => Promise<null | string>;
|
|
1735
1736
|
goldenWrite: (name: string, content: string) => Promise<string>;
|
|
1737
|
+
notify: (line: string) => Promise<void>;
|
|
1736
1738
|
}
|
|
1737
1739
|
}
|
|
1738
1740
|
//#endregion
|
package/dist/oxlint.cjs
CHANGED
|
@@ -271,7 +271,7 @@ const RULE_DOCS = {
|
|
|
271
271
|
},
|
|
272
272
|
"c1-domain-structure": {
|
|
273
273
|
channel: "statique",
|
|
274
|
-
convention: "A specs tree DECLARES its shape through the `depth` option: `facet-domain` (the default — a test at facet/domain depth, a `*.specification.ts` at the facet root), `facet` (a test at the facet root OR one domain folder down, never deeper), `mirror` (a test at any depth ≥ 1, named after its directory), `off`. In every mode, `off` included: a directory with a leading underscore is GROUND, never a domain — no spec lives inside one. One exception, and only one: ground may be CODE, and a `<module>.test.ts` sitting BESIDE the `<module>.ts` it tests is legal there (that is I2). A test with no neighbouring module, or a `*.specification.ts`, is still a violation. A `.test.tsx` is out of reach: a rendered unit is specified beside its component, never in a facet/domain tree — I2 holds its neighbourhood.",
|
|
274
|
+
convention: "A specs tree DECLARES its shape through the `depth` option: `facet-domain` (the default — a test at facet/domain depth, a `*.specification.ts` at the facet root), `facet` (a test at the facet root OR one domain folder down, never deeper), `mirror` (a test at any depth ≥ 1, named after its directory), `off`. A first-level folder that is no facet — a repository suite, `surface/` or `lint/` — is judged by the same depth as a facet is. In every mode, `off` included: a directory with a leading underscore is GROUND, never a domain — no spec lives inside one. One exception, and only one: ground may be CODE, and a `<module>.test.ts` sitting BESIDE the `<module>.ts` it tests is legal there (that is I2). A test with no neighbouring module, or a `*.specification.ts`, is still a violation. A `.test.tsx` is out of reach: a rendered unit is specified beside its component, never in a facet/domain tree — I2 holds its neighbourhood.",
|
|
275
275
|
facet: "model",
|
|
276
276
|
family: "C",
|
|
277
277
|
fix: "Move the file to the depth the tree declares, or declare the depth the tree actually has.",
|
|
@@ -661,7 +661,7 @@ const RULE_DOCS = {
|
|
|
661
661
|
},
|
|
662
662
|
"j2-no-sleep": {
|
|
663
663
|
channel: "statique",
|
|
664
|
-
convention: "No arbitrary sleep (`setTimeout`/`setInterval`/`Atomics.wait`, or a `node:timers/promises` import) in any test file — synchronisation is `see()`/`gone()` inside a scenario and `waitUntil(predicate)` everywhere else. A timer inside a test DOUBLE's implementation (`vi.fn(…)`, `mockImplementation(…)`, a `mockOf` handler)
|
|
664
|
+
convention: "No arbitrary sleep (`setTimeout`/`setInterval`/`Atomics.wait`, or a `node:timers/promises` import) in any test file — synchronisation is `see()`/`gone()` inside a scenario and `waitUntil(predicate)` everywhere else. A timer inside a test DOUBLE's implementation is out of reach: the double is staging the world, not the test waiting on it. A double is a factory call (`vi.fn(…)`, `mockImplementation(…)`, a `mockOf` handler) or a function-valued PROPERTY of an object literal, which is the plainest double there is — `const git: GitGateway = { cloneRepository: async () => … }`, where the object itself is the double.",
|
|
665
665
|
facet: "model",
|
|
666
666
|
family: "J",
|
|
667
667
|
fix: "Wait for the condition, not for a duration.",
|
|
@@ -5639,6 +5639,20 @@ function isDoubleImplementation(node) {
|
|
|
5639
5639
|
return name !== void 0 && DOUBLE_FACTORIES.has(name);
|
|
5640
5640
|
}
|
|
5641
5641
|
/**
|
|
5642
|
+
* The FUNCTION a property of an object literal holds, or `undefined`.
|
|
5643
|
+
*
|
|
5644
|
+
* A double is not always built by a factory: the plainest one there is, in a
|
|
5645
|
+
* test file, is an object literal whose properties are the port's methods —
|
|
5646
|
+
* `const git: GitGateway = { cloneRepository: async () => … }`. The object IS
|
|
5647
|
+
* the double, so a timer in one of its methods stages the world exactly as
|
|
5648
|
+
* `vi.fn(() => setTimeout(…))` does, and judging it as the test sleeping asked
|
|
5649
|
+
* an author to remove the very thing under test.
|
|
5650
|
+
*/
|
|
5651
|
+
function implementationOf(node) {
|
|
5652
|
+
const value = child(node, "value");
|
|
5653
|
+
return value?.type === "ArrowFunctionExpression" || value?.type === "FunctionExpression" ? value : void 0;
|
|
5654
|
+
}
|
|
5655
|
+
/**
|
|
5642
5656
|
* CONVENTIONS J2 — a test contains no arbitrary sleep, wherever it sits:
|
|
5643
5657
|
* synchronisation is `see()`/`gone()` inside a scenario and `waitUntil()`
|
|
5644
5658
|
* everywhere else. Flags `setTimeout(…)` calls (bare or as a member, e.g.
|
|
@@ -5654,7 +5668,8 @@ function isDoubleImplementation(node) {
|
|
|
5654
5668
|
* about the ORDER results come back in, which no predicate can state: a
|
|
5655
5669
|
* `waitUntil` waits for something to become true, while what this test needs is
|
|
5656
5670
|
* for something to happen late. The test is not the one waiting, so the rule is
|
|
5657
|
-
* not about it.
|
|
5671
|
+
* not about it. A double written as a plain object literal is the same thing
|
|
5672
|
+
* said without a factory, and it counts too.
|
|
5658
5673
|
*/
|
|
5659
5674
|
const j2NoSleep = {
|
|
5660
5675
|
create(context) {
|
|
@@ -5662,6 +5677,13 @@ const j2NoSleep = {
|
|
|
5662
5677
|
const doubles = [];
|
|
5663
5678
|
const insideADouble = (node) => doubles.some((span) => nodeStart(node) >= span.start && nodeEnd(node) <= span.end);
|
|
5664
5679
|
return {
|
|
5680
|
+
Property(node) {
|
|
5681
|
+
const implementation = implementationOf(node);
|
|
5682
|
+
if (implementation !== void 0) doubles.push({
|
|
5683
|
+
end: nodeEnd(implementation),
|
|
5684
|
+
start: nodeStart(implementation)
|
|
5685
|
+
});
|
|
5686
|
+
},
|
|
5665
5687
|
CallExpression(node) {
|
|
5666
5688
|
if (isDoubleImplementation(node)) {
|
|
5667
5689
|
doubles.push({
|
package/dist/oxlint.js
CHANGED
|
@@ -267,7 +267,7 @@ const RULE_DOCS = {
|
|
|
267
267
|
},
|
|
268
268
|
"c1-domain-structure": {
|
|
269
269
|
channel: "statique",
|
|
270
|
-
convention: "A specs tree DECLARES its shape through the `depth` option: `facet-domain` (the default — a test at facet/domain depth, a `*.specification.ts` at the facet root), `facet` (a test at the facet root OR one domain folder down, never deeper), `mirror` (a test at any depth ≥ 1, named after its directory), `off`. In every mode, `off` included: a directory with a leading underscore is GROUND, never a domain — no spec lives inside one. One exception, and only one: ground may be CODE, and a `<module>.test.ts` sitting BESIDE the `<module>.ts` it tests is legal there (that is I2). A test with no neighbouring module, or a `*.specification.ts`, is still a violation. A `.test.tsx` is out of reach: a rendered unit is specified beside its component, never in a facet/domain tree — I2 holds its neighbourhood.",
|
|
270
|
+
convention: "A specs tree DECLARES its shape through the `depth` option: `facet-domain` (the default — a test at facet/domain depth, a `*.specification.ts` at the facet root), `facet` (a test at the facet root OR one domain folder down, never deeper), `mirror` (a test at any depth ≥ 1, named after its directory), `off`. A first-level folder that is no facet — a repository suite, `surface/` or `lint/` — is judged by the same depth as a facet is. In every mode, `off` included: a directory with a leading underscore is GROUND, never a domain — no spec lives inside one. One exception, and only one: ground may be CODE, and a `<module>.test.ts` sitting BESIDE the `<module>.ts` it tests is legal there (that is I2). A test with no neighbouring module, or a `*.specification.ts`, is still a violation. A `.test.tsx` is out of reach: a rendered unit is specified beside its component, never in a facet/domain tree — I2 holds its neighbourhood.",
|
|
271
271
|
facet: "model",
|
|
272
272
|
family: "C",
|
|
273
273
|
fix: "Move the file to the depth the tree declares, or declare the depth the tree actually has.",
|
|
@@ -657,7 +657,7 @@ const RULE_DOCS = {
|
|
|
657
657
|
},
|
|
658
658
|
"j2-no-sleep": {
|
|
659
659
|
channel: "statique",
|
|
660
|
-
convention: "No arbitrary sleep (`setTimeout`/`setInterval`/`Atomics.wait`, or a `node:timers/promises` import) in any test file — synchronisation is `see()`/`gone()` inside a scenario and `waitUntil(predicate)` everywhere else. A timer inside a test DOUBLE's implementation (`vi.fn(…)`, `mockImplementation(…)`, a `mockOf` handler)
|
|
660
|
+
convention: "No arbitrary sleep (`setTimeout`/`setInterval`/`Atomics.wait`, or a `node:timers/promises` import) in any test file — synchronisation is `see()`/`gone()` inside a scenario and `waitUntil(predicate)` everywhere else. A timer inside a test DOUBLE's implementation is out of reach: the double is staging the world, not the test waiting on it. A double is a factory call (`vi.fn(…)`, `mockImplementation(…)`, a `mockOf` handler) or a function-valued PROPERTY of an object literal, which is the plainest double there is — `const git: GitGateway = { cloneRepository: async () => … }`, where the object itself is the double.",
|
|
661
661
|
facet: "model",
|
|
662
662
|
family: "J",
|
|
663
663
|
fix: "Wait for the condition, not for a duration.",
|
|
@@ -5635,6 +5635,20 @@ function isDoubleImplementation(node) {
|
|
|
5635
5635
|
return name !== void 0 && DOUBLE_FACTORIES.has(name);
|
|
5636
5636
|
}
|
|
5637
5637
|
/**
|
|
5638
|
+
* The FUNCTION a property of an object literal holds, or `undefined`.
|
|
5639
|
+
*
|
|
5640
|
+
* A double is not always built by a factory: the plainest one there is, in a
|
|
5641
|
+
* test file, is an object literal whose properties are the port's methods —
|
|
5642
|
+
* `const git: GitGateway = { cloneRepository: async () => … }`. The object IS
|
|
5643
|
+
* the double, so a timer in one of its methods stages the world exactly as
|
|
5644
|
+
* `vi.fn(() => setTimeout(…))` does, and judging it as the test sleeping asked
|
|
5645
|
+
* an author to remove the very thing under test.
|
|
5646
|
+
*/
|
|
5647
|
+
function implementationOf(node) {
|
|
5648
|
+
const value = child(node, "value");
|
|
5649
|
+
return value?.type === "ArrowFunctionExpression" || value?.type === "FunctionExpression" ? value : void 0;
|
|
5650
|
+
}
|
|
5651
|
+
/**
|
|
5638
5652
|
* CONVENTIONS J2 — a test contains no arbitrary sleep, wherever it sits:
|
|
5639
5653
|
* synchronisation is `see()`/`gone()` inside a scenario and `waitUntil()`
|
|
5640
5654
|
* everywhere else. Flags `setTimeout(…)` calls (bare or as a member, e.g.
|
|
@@ -5650,7 +5664,8 @@ function isDoubleImplementation(node) {
|
|
|
5650
5664
|
* about the ORDER results come back in, which no predicate can state: a
|
|
5651
5665
|
* `waitUntil` waits for something to become true, while what this test needs is
|
|
5652
5666
|
* for something to happen late. The test is not the one waiting, so the rule is
|
|
5653
|
-
* not about it.
|
|
5667
|
+
* not about it. A double written as a plain object literal is the same thing
|
|
5668
|
+
* said without a factory, and it counts too.
|
|
5654
5669
|
*/
|
|
5655
5670
|
const j2NoSleep = {
|
|
5656
5671
|
create(context) {
|
|
@@ -5658,6 +5673,13 @@ const j2NoSleep = {
|
|
|
5658
5673
|
const doubles = [];
|
|
5659
5674
|
const insideADouble = (node) => doubles.some((span) => nodeStart(node) >= span.start && nodeEnd(node) <= span.end);
|
|
5660
5675
|
return {
|
|
5676
|
+
Property(node) {
|
|
5677
|
+
const implementation = implementationOf(node);
|
|
5678
|
+
if (implementation !== void 0) doubles.push({
|
|
5679
|
+
end: nodeEnd(implementation),
|
|
5680
|
+
start: nodeStart(implementation)
|
|
5681
|
+
});
|
|
5682
|
+
},
|
|
5661
5683
|
CallExpression(node) {
|
|
5662
5684
|
if (isDoubleImplementation(node)) {
|
|
5663
5685
|
doubles.push({
|
|
@@ -1,8 +1,47 @@
|
|
|
1
|
-
import { a as
|
|
1
|
+
import { a as describeAmbiguity, i as AmbiguousElementError, o as formatElement, r as widenedForWindow, t as WINDOW_SETTLE_MS } from "./substring-warning.js";
|
|
2
2
|
import { mkdtempSync } from "node:fs";
|
|
3
3
|
import { resolve } from "node:path";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { setTimeout } from "node:timers/promises";
|
|
6
|
+
//#region src/model/elements/aria-name.ts
|
|
7
|
+
/**
|
|
8
|
+
* The accessible name an ARIA snapshot states.
|
|
9
|
+
*
|
|
10
|
+
* A descriptor matches the name the BROWSER computes, and that name is not the
|
|
11
|
+
* element's text: `<button><span>Experiments</span><span>9</span></button>`
|
|
12
|
+
* reads as `Experiments9` through `textContent` and is named "Experiments 9",
|
|
13
|
+
* a field's name comes from its label and is nowhere in its text at all, and a
|
|
14
|
+
* name cut at eighty characters resolves to nothing. Reading the name off the
|
|
15
|
+
* snapshot is what makes the spelling the window prints a spelling that can be
|
|
16
|
+
* written back into a spec.
|
|
17
|
+
*
|
|
18
|
+
* The dialect is the one both surfaces already speak — playwright's, on the
|
|
19
|
+
* page directly and through `server.commands` for a mounted component — so the
|
|
20
|
+
* projection is stated once, here, and neither adapter computes a name itself.
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* The name of the FIRST node a snapshot describes: `- button "Experiments 9"`,
|
|
24
|
+
* optionally followed by `:` and its children.
|
|
25
|
+
*
|
|
26
|
+
* A quote must follow the role across a SPACE. `- paragraph: "…"` is a node
|
|
27
|
+
* with text and no name, and reading that text as a name would hand the author
|
|
28
|
+
* a spelling no role descriptor matches on.
|
|
29
|
+
*/
|
|
30
|
+
const NAMED = /^-\s+[\w-]+\s+"(?<name>(?:[^"\\]|\\.)*)"/u;
|
|
31
|
+
/** Undo the escaping the snapshot applies to a quote or a backslash inside a name. */
|
|
32
|
+
function unescaped(name) {
|
|
33
|
+
return name.replaceAll(/\\(?<char>.)/gu, "$<char>");
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The accessible name the snapshot gives the node it describes, or `undefined`
|
|
37
|
+
* when that node has none — an unnamed landmark, a paragraph of text.
|
|
38
|
+
*/
|
|
39
|
+
function accessibleNameIn(snapshot) {
|
|
40
|
+
const [first] = snapshot.split("\n");
|
|
41
|
+
const name = (first === void 0 ? null : NAMED.exec(first.trim()))?.groups?.name;
|
|
42
|
+
return name === void 0 ? void 0 : unescaped(name);
|
|
43
|
+
}
|
|
44
|
+
//#endregion
|
|
6
45
|
//#region src/seams/playwright/playwright.adapter.ts
|
|
7
46
|
/** How many candidates the ambiguity error enumerates before truncating. */
|
|
8
47
|
const MAX_REPORTED_MATCHES = 10;
|
|
@@ -107,12 +146,62 @@ async function act(page, element, action) {
|
|
|
107
146
|
function probeOf(page) {
|
|
108
147
|
return {
|
|
109
148
|
count: async (element) => await locate(page, element).count(),
|
|
110
|
-
nameOf: async (element) =>
|
|
111
|
-
|
|
112
|
-
return match?.accessibleName ?? match?.text;
|
|
113
|
-
}
|
|
149
|
+
nameOf: async (element) => await wholeNameOf(page, element),
|
|
150
|
+
settles: async (element) => await settles(page, element)
|
|
114
151
|
};
|
|
115
152
|
}
|
|
153
|
+
/**
|
|
154
|
+
* Does the descriptor arrive if the page is given the settle budget?
|
|
155
|
+
*
|
|
156
|
+
* A poll rather than `waitFor`, for the reason the rest of this adapter has no
|
|
157
|
+
* `.first()`: waiting on the locator itself would have to narrow a strict
|
|
158
|
+
* locator to one element, and a count answers the same question without
|
|
159
|
+
* touching the mechanism W3 rests on. The locator is rebuilt every round, so a
|
|
160
|
+
* navigation that replaced the document is read on the new one.
|
|
161
|
+
*/
|
|
162
|
+
async function settles(page, element) {
|
|
163
|
+
const deadline = Date.now() + WINDOW_SETTLE_MS;
|
|
164
|
+
for (;;) {
|
|
165
|
+
if (await countOrZero(page, element) > 0) return true;
|
|
166
|
+
if (Date.now() >= deadline) return false;
|
|
167
|
+
await setTimeout(VALUE_POLL_MS);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/** A count taken mid-navigation throws; the page is simply not answering yet. */
|
|
171
|
+
async function countOrZero(page, element) {
|
|
172
|
+
try {
|
|
173
|
+
return await locate(page, element).count();
|
|
174
|
+
} catch {
|
|
175
|
+
return 0;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* The name to WRITE for this descriptor — the whole one, never truncated.
|
|
180
|
+
*
|
|
181
|
+
* A role descriptor matches the accessible name the browser computed, so the
|
|
182
|
+
* name is read off the ARIA snapshot, which is that same computation: the text
|
|
183
|
+
* content glues inline children with no separator (`Experiments9` for a button
|
|
184
|
+
* named "Experiments 9") and a field's name is its label, which its text never
|
|
185
|
+
* carries at all. A `text` descriptor matches text, so text is what it is told
|
|
186
|
+
* to write.
|
|
187
|
+
*/
|
|
188
|
+
async function wholeNameOf(page, element) {
|
|
189
|
+
const locator = locate(page, element);
|
|
190
|
+
if (element.kind !== "text") try {
|
|
191
|
+
const named = accessibleNameIn(await locator.ariaSnapshot());
|
|
192
|
+
if (named !== void 0) return named;
|
|
193
|
+
} catch {}
|
|
194
|
+
return await wholeTextOf(locator);
|
|
195
|
+
}
|
|
196
|
+
/** The element's text, whitespace-collapsed and WHOLE — a name cut mid-word resolves to nothing. */
|
|
197
|
+
async function wholeTextOf(locator) {
|
|
198
|
+
try {
|
|
199
|
+
const collapsed = (await locator.evaluate((node) => node.textContent ?? "")).replaceAll(/\s+/gu, " ").trim();
|
|
200
|
+
return collapsed === "" ? void 0 : collapsed;
|
|
201
|
+
} catch {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
116
205
|
/** Walk the scope chain outside-in; the outermost ambiguous level is the fault. */
|
|
117
206
|
async function findAmbiguousLevel(page, element) {
|
|
118
207
|
const chain = [];
|
|
@@ -164,6 +164,14 @@ function describeAmbiguity(options) {
|
|
|
164
164
|
*/
|
|
165
165
|
/** How long the window lasts, stated in the message itself. */
|
|
166
166
|
const WINDOW = "through 16.x";
|
|
167
|
+
/** The node side's printer — the default every adapter but the page's uses. */
|
|
168
|
+
function toStandardError(line) {
|
|
169
|
+
if (typeof process === "undefined") {
|
|
170
|
+
console.warn(line);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
process.stderr.write(`${line}\n`);
|
|
174
|
+
}
|
|
167
175
|
/** Descriptors already reported in this process. */
|
|
168
176
|
const reported = /* @__PURE__ */ new Set();
|
|
169
177
|
/** The identity a descriptor is deduplicated by — kind, name, and its scope chain. */
|
|
@@ -176,15 +184,26 @@ function identityOf(element) {
|
|
|
176
184
|
* designated something as a substring. Returns whether a line was printed,
|
|
177
185
|
* which is what the adapters' own tests assert on.
|
|
178
186
|
*/
|
|
179
|
-
function warnSubstringOnly(element, where, found) {
|
|
187
|
+
async function warnSubstringOnly(element, where, found, print = toStandardError) {
|
|
180
188
|
const identity = identityOf(element);
|
|
181
189
|
if (reported.has(identity)) return false;
|
|
182
190
|
reported.add(identity);
|
|
183
191
|
const spelling = found === void 0 ? "" : ` The name to write is '${found.replaceAll(/\s+/gu, " ").trim()}'.`;
|
|
184
|
-
|
|
192
|
+
await print(`@jterrazz/test: ${element.kind}('${element.name ?? ""}') matched nothing as a whole accessible name, and matched only as a SUBSTRING of a longer one (${where}).${spelling} Names are exact from 16.0: write the name in full, scope it with \`within(…)\`, or state \`{ exact: false }\` to keep the old behaviour. The substring is still resolved, with this warning, ${WINDOW}; gone in 17.0, where the same descriptor is simply not found.`);
|
|
185
193
|
return true;
|
|
186
194
|
}
|
|
187
195
|
/**
|
|
196
|
+
* How long a descriptor is given to ARRIVE before the window concludes it is
|
|
197
|
+
* absent — the budget `settles` spends and no more.
|
|
198
|
+
*
|
|
199
|
+
* A count is one observation, and a verb called right after a navigating
|
|
200
|
+
* `click()` takes it on the page being LEFT. The window may not decide
|
|
201
|
+
* "nothing matches this whole name" on that reading: the destination has not
|
|
202
|
+
* been parsed yet. Long enough for a navigation to commit, far short of the
|
|
203
|
+
* verb's own 30 s, and paid only when there is a substring to widen to.
|
|
204
|
+
*/
|
|
205
|
+
const WINDOW_SETTLE_MS = 5e3;
|
|
206
|
+
/**
|
|
188
207
|
* The descriptor to retry with, LEVEL by level, or `undefined` when nothing
|
|
189
208
|
* would change.
|
|
190
209
|
*
|
|
@@ -201,8 +220,8 @@ function warnSubstringOnly(element, where, found) {
|
|
|
201
220
|
*
|
|
202
221
|
* Asked BEFORE the action, not after it fails: a verb waits its whole
|
|
203
222
|
* actionability budget, which is the test's own budget, so a retry afterwards
|
|
204
|
-
* never runs — the test is already over. The
|
|
205
|
-
*
|
|
223
|
+
* never runs — the test is already over. The common case (the name matches
|
|
224
|
+
* whole) costs one count per level, which waits for nothing.
|
|
206
225
|
*/
|
|
207
226
|
async function widenedForWindow(element, where, probe) {
|
|
208
227
|
const chain = [];
|
|
@@ -222,8 +241,16 @@ async function widenedForWindow(element, where, probe) {
|
|
|
222
241
|
}
|
|
223
242
|
/**
|
|
224
243
|
* The one level, widened and warned about, or `undefined` when it designates
|
|
225
|
-
* something already, states its own `exact`, carries no name,
|
|
226
|
-
*
|
|
244
|
+
* something already, states its own `exact`, carries no name, would designate
|
|
245
|
+
* nothing either way, or turns out to be exact on the surface that ARRIVES.
|
|
246
|
+
*
|
|
247
|
+
* The last is the navigation case, and it is the reason the verdict is not two
|
|
248
|
+
* counts: a `see()` fired right after a navigating `click()` counts the page
|
|
249
|
+
* being left, so a descriptor that is perfectly exact on the DESTINATION read
|
|
250
|
+
* as substring-only — non-deterministically, since which page each count
|
|
251
|
+
* landed on was a race. A widening is therefore only decided once the exact
|
|
252
|
+
* descriptor has been given the settle budget and still designates nothing.
|
|
253
|
+
* The tell of the old fault was the printed name equalling the name written.
|
|
227
254
|
*/
|
|
228
255
|
async function widenedLevel(candidate, where, probe) {
|
|
229
256
|
if (candidate.exact !== void 0 || candidate.name === void 0) return;
|
|
@@ -234,11 +261,22 @@ async function widenedLevel(candidate, where, probe) {
|
|
|
234
261
|
exact: false
|
|
235
262
|
};
|
|
236
263
|
if (await probe.count(loose) !== 1) return;
|
|
237
|
-
|
|
264
|
+
if (!alreadyJudged(candidate) && await probe.settles(candidate)) return;
|
|
265
|
+
await warnSubstringOnly(candidate, where, await probe.nameOf(loose), probe.print);
|
|
238
266
|
return loose;
|
|
239
267
|
} catch {
|
|
240
268
|
return;
|
|
241
269
|
}
|
|
242
270
|
}
|
|
271
|
+
/**
|
|
272
|
+
* Has this descriptor already been judged substring-only in this run?
|
|
273
|
+
*
|
|
274
|
+
* The verdict is made once: a scenario that clicks the same link in ten tests
|
|
275
|
+
* pays the settle budget on the first of them and is widened straight away on
|
|
276
|
+
* the nine after it.
|
|
277
|
+
*/
|
|
278
|
+
function alreadyJudged(element) {
|
|
279
|
+
return reported.has(identityOf(element));
|
|
280
|
+
}
|
|
243
281
|
//#endregion
|
|
244
|
-
export {
|
|
282
|
+
export { describeAmbiguity as a, AmbiguousElementError as i, warnSubstringOnly as n, formatElement as o, widenedForWindow as r, WINDOW_SETTLE_MS as t };
|
package/dist/vitest.js
CHANGED
|
@@ -350,6 +350,23 @@ function goldenPath(testPath, name) {
|
|
|
350
350
|
const ariaTree = async (context) => {
|
|
351
351
|
return await (await context.frame()).locator("body").ariaSnapshot();
|
|
352
352
|
};
|
|
353
|
+
/**
|
|
354
|
+
* The ARIA snapshot of ONE node, named by the selector the page's locator
|
|
355
|
+
* carries — the accessible name, as the browser computed it, for a page that
|
|
356
|
+
* cannot compute one itself.
|
|
357
|
+
*/
|
|
358
|
+
const ariaNode = async (context, selector) => {
|
|
359
|
+
return await (await context.frame()).locator(selector).ariaSnapshot();
|
|
360
|
+
};
|
|
361
|
+
/**
|
|
362
|
+
* A line the page has for whoever is running the suite, written on the node
|
|
363
|
+
* side's stderr — the one channel vitest's default reporter passes through.
|
|
364
|
+
* The console is captured and dropped, so a page that printed to it printed to
|
|
365
|
+
* nobody.
|
|
366
|
+
*/
|
|
367
|
+
const notify = (_context, line) => {
|
|
368
|
+
process.stderr.write(`${line}\n`);
|
|
369
|
+
};
|
|
353
370
|
/** The golden's current content, or `null` when it does not exist yet. */
|
|
354
371
|
const goldenRead = (context, name) => {
|
|
355
372
|
const path = goldenPath(context.testPath, name);
|
|
@@ -367,9 +384,11 @@ const goldenWrite = (context, name, content) => {
|
|
|
367
384
|
* page calls and the names the project registers cannot drift.
|
|
368
385
|
*/
|
|
369
386
|
const COMPONENT_COMMANDS = {
|
|
387
|
+
ariaNode,
|
|
370
388
|
ariaTree,
|
|
371
389
|
goldenRead,
|
|
372
|
-
goldenWrite
|
|
390
|
+
goldenWrite,
|
|
391
|
+
notify
|
|
373
392
|
};
|
|
374
393
|
//#endregion
|
|
375
394
|
//#region src/facets/component/component.project.ts
|
package/docs/03-testing.md
CHANGED
|
@@ -108,9 +108,9 @@ The package proves itself in five layers, and they are meant to be read from the
|
|
|
108
108
|
|
|
109
109
|
| Layer | Where | What it judges | Files |
|
|
110
110
|
| ----------------------- | ------------------------------------------------------- | ------------------------------------------------------------------------ | ----- |
|
|
111
|
-
| Module tests | `src/**/*.test.ts` beside the module | one module, through its own exports, with nothing started |
|
|
111
|
+
| Module tests | `src/**/*.test.ts` beside the module | one module, through its own exports, with nothing started | 54 |
|
|
112
112
|
| Rule tests | `src/lint/rules/<facet>/<rule>.test.ts` beside the rule | one rule: what it flags, what it leaves alone, and the message it prints | 56 |
|
|
113
|
-
| The package’s own specs | `specs/<facet>/` | the framework's own facets, each met through its constructor |
|
|
113
|
+
| The package’s own specs | `specs/<facet>/` | the framework's own facets, each met through its constructor | 67 |
|
|
114
114
|
| The lint suite | `specs/lint/**` | the built binary end to end, over fixture projects | 88 |
|
|
115
115
|
| Meta-tests | `src/lint/*.test.ts` | the corpus itself: the catalogue, the matrix, the cards, the floor | 18 |
|
|
116
116
|
|
|
@@ -185,7 +185,7 @@ suite on the day it lands.
|
|
|
185
185
|
|
|
186
186
|
<!-- GENERATED:siblings — do not edit by hand; run `npm run docs`. Source: src/lint/siblings.ts -->
|
|
187
187
|
|
|
188
|
-
Today **
|
|
188
|
+
Today **80 modules** have no sibling test, and every one of them is claimed:
|
|
189
189
|
|
|
190
190
|
| Kind | Proven instead by |
|
|
191
191
|
| --------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
@@ -197,7 +197,7 @@ Today **81 modules** have no sibling test, and every one of them is claimed:
|
|
|
197
197
|
| a mobile module that needs a device (2) | the mobile facet has no tree under `specs/` and cannot have one here (M1); what a device is NOT needed for — resolving the simulator, projecting the page source, wording the ambiguity — has its own sibling test, and these two are the rest |
|
|
198
198
|
| a facet module (8) | the facet is proven end to end by its own tree under `specs/<facet>/`, which is what the constructor exists to make possible |
|
|
199
199
|
| a result accessor (7) | an accessor is what a terminal action hands back: it is exercised by every spec that asserts on a result, and `result.test.ts` holds the base |
|
|
200
|
-
| a seam adapter (
|
|
200
|
+
| a seam adapter (12) | a seam is proven through the facet that drives it — a probe that could only reach it directly is an integration spec under `specs/integration/<seam>/` (chapter 03) |
|
|
201
201
|
| a bundled CLI entry (3) | it is argument parsing over a module that has its own tests, and `specs/lint/` runs the built binary end to end |
|
|
202
202
|
| a lint-layer module (4) | the lint layer is proven by the rule tests beside each rule and by `specs/lint/`, which runs the real oxlint binary and the real checker over fixture projects |
|
|
203
203
|
| a chain or runner internal (7) | it is reached through the chain, so its proof is the facet specs that drive the chain — a direct test would assert on a seam nothing else speaks to |
|
|
@@ -275,18 +275,18 @@ What the framework can do, and how many of this package’s own test FILES carry
|
|
|
275
275
|
| `.call()` | — | — | — | 6 | — | — | — | — |
|
|
276
276
|
| `.exec()` | — | — | 7 | — | — | — | — | — |
|
|
277
277
|
| `.run()` | — | — | 2 | — | — | — | — | — |
|
|
278
|
-
| `.visit()` | — | — | — | — |
|
|
278
|
+
| `.visit()` | — | — | — | — | 11 | — | — | — |
|
|
279
279
|
| `.fetch()` | — | — | — | — | 2 | — | — | — |
|
|
280
280
|
| `.open()` | — | — | — | — | — | 0 | — | — |
|
|
281
|
-
| `.render()` | — | — | — | — | — | — |
|
|
281
|
+
| `.render()` | — | — | — | — | — | — | 15 | — |
|
|
282
282
|
|
|
283
283
|
### Verb
|
|
284
284
|
|
|
285
285
|
| Capability | api | jobs | cli | integration | website | mobile | component | module |
|
|
286
286
|
| ---------- | --- | ---- | --- | ----------- | ------- | ------ | --------- | ------ |
|
|
287
|
-
| `see` | — | — | — | — |
|
|
287
|
+
| `see` | — | — | — | — | 6 | 0 | 15 | — |
|
|
288
288
|
| `fill` | — | — | — | — | 3 | 0 | 2 | — |
|
|
289
|
-
| `click` | — | — | — | — |
|
|
289
|
+
| `click` | — | — | — | — | 4 | — | 5 | — |
|
|
290
290
|
| `gone` | — | — | — | — | 2 | — | 4 | — |
|
|
291
291
|
| `press` | — | — | — | — | 0 | — | 2 | — |
|
|
292
292
|
| `hover` | — | — | — | — | 0 | — | 1 | — |
|
|
@@ -301,14 +301,14 @@ What the framework can do, and how many of this package’s own test FILES carry
|
|
|
301
301
|
|
|
302
302
|
| Capability | api | jobs | cli | integration | website | mobile | component | module |
|
|
303
303
|
| --------------- | --- | ---- | --- | ----------- | ------- | ------ | --------- | ------ |
|
|
304
|
-
| `button` | — | — | — | — |
|
|
305
|
-
| `field` | — | — | — | — |
|
|
304
|
+
| `button` | — | — | — | — | 4 | 0 | 5 | — |
|
|
305
|
+
| `field` | — | — | — | — | 4 | 0 | 3 | — |
|
|
306
306
|
| `content` | — | — | — | — | 3 | 0 | 10 | — |
|
|
307
307
|
| `testId` | — | — | — | — | 0 | 0 | 1 | — |
|
|
308
|
-
| `heading` | — | — | — | — |
|
|
309
|
-
| `link` | — | — | — | — |
|
|
308
|
+
| `heading` | — | — | — | — | 1 | — | 1 | — |
|
|
309
|
+
| `link` | — | — | — | — | 3 | — | 2 | — |
|
|
310
310
|
| `dialog` | — | — | — | — | 0 | — | 1 | — |
|
|
311
|
-
| `status` | — | — | — | — | 0 | — |
|
|
311
|
+
| `status` | — | — | — | — | 0 | — | 3 | — |
|
|
312
312
|
| `table` | — | — | — | — | 0 | — | 1 | — |
|
|
313
313
|
| `row` | — | — | — | — | 0 | — | 1 | — |
|
|
314
314
|
| `listitem` | — | — | — | — | 0 | — | 1 | — |
|
|
@@ -345,8 +345,8 @@ What the framework can do, and how many of this package’s own test FILES carry
|
|
|
345
345
|
| `.containerIds` | — | — | 1 | — | — | — | — | — |
|
|
346
346
|
| `.value` | — | — | — | 6 | — | — | — | — |
|
|
347
347
|
| `.error` | — | — | — | 5 | — | — | — | — |
|
|
348
|
-
| `.tree` | — | — | — | — | 2 | — |
|
|
349
|
-
| `.content` | — | — | — | — | 4 | 0 |
|
|
348
|
+
| `.tree` | — | — | — | — | 2 | — | 4 | — |
|
|
349
|
+
| `.content` | — | — | — | — | 4 | 0 | 12 | — |
|
|
350
350
|
| `.console` | — | — | — | — | 1 | — | 3 | — |
|
|
351
351
|
| `.errors` | — | — | — | — | 4 | — | 5 | — |
|
|
352
352
|
| `.html` | — | — | — | — | 0 | — | 5 | — |
|
|
@@ -414,7 +414,7 @@ What the framework can do, and how many of this package’s own test FILES carry
|
|
|
414
414
|
### Exemptions
|
|
415
415
|
|
|
416
416
|
- no mobile tree: an iOS simulator is not something CI provisions (M1) — `services`·mobile, `root`·mobile, `backend`·mobile, `device`·mobile, `app`·mobile, `timeouts`·mobile, `.intercept()`·mobile, `.open()`·mobile, `see`·mobile, `fill`·mobile, `tap`·mobile, `button`·mobile, `field`·mobile, `content`·mobile, `testId`·mobile, `.content`·mobile, `.screen`·mobile
|
|
417
|
-
- the surface ships and this package does not specify it here yet — `root`·cli, `root`·website, `root`·component, `backend`·website, `external`·website, `clock`·component, `locale`·component, `timezone`·component, `viewport`·component, `.clock()`·jobs, `.intercept()`·website, `press`·website, `hover`·website, `check`·website, `goto`·website, `testId`·website, `
|
|
417
|
+
- the surface ships and this package does not specify it here yet — `root`·cli, `root`·website, `root`·component, `backend`·website, `external`·website, `clock`·component, `locale`·component, `timezone`·component, `viewport`·component, `.clock()`·jobs, `.intercept()`·website, `press`·website, `hover`·website, `check`·website, `goto`·website, `testId`·website, `dialog`·website, `status`·website, `table`·website, `row`·website, `listitem`·website, `banner`·website, `banner`·component, `complementary`·website, `complementary`·component, `contentinfo`·component, `form`·website, `form`·component, `search`·website, `search`·component, `.file()`·api, `.file()`·integration, `.directory()`·api, `.directory()`·integration, `.html`·website, `.title`·website, `.links`·website, `toMatchRows()`·component, `.json body`·api
|
|
418
418
|
- the package serves its own fixture site, so `server` is what it proves; `url` targets an already-running deployment, which is a consumer shape (A11 states the XOR) — `url`·website
|
|
419
419
|
- a job's oracle is the table it wrote (`toMatchRows`): its result carries no file and no directory — `toMatch('<name>')`·jobs
|
|
420
420
|
- the engine owns the family (proven in the module column); a facet uses the ones its goldens need — `{{any}}`·api, `{{any}}`·integration, `{{any}}`·website, `{{base64}}`·api, `{{base64}}`·cli, `{{base64}}`·integration, `{{base64}}`·website, `{{date}}`·api, `{{date}}`·cli, `{{date}}`·integration, `{{date}}`·website, `{{duration}}`·api, `{{duration}}`·integration, `{{duration}}`·website, `{{email}}`·api, `{{email}}`·cli, `{{email}}`·integration, `{{email}}`·website, `{{float}}`·api, `{{float}}`·cli, `{{float}}`·integration, `{{float}}`·website, `{{hex}}`·api, `{{hex}}`·cli, `{{hex}}`·integration, `{{hex}}`·website, `{{int}}`·api, `{{int}}`·cli, `{{int}}`·integration, `{{int}}`·website, `{{ip}}`·api, `{{ip}}`·cli, `{{ip}}`·integration, `{{ip}}`·website, `{{iso8601}}`·api, `{{iso8601}}`·cli, `{{iso8601}}`·integration, `{{iso8601}}`·website, `{{number}}`·api, `{{number}}`·cli, `{{number}}`·integration, `{{number}}`·website, `{{path}}`·api, `{{path}}`·cli, `{{path}}`·integration, `{{path}}`·website, `{{port}}`·api, `{{port}}`·cli, `{{port}}`·integration, `{{port}}`·website, `{{semver}}`·api, `{{semver}}`·integration, `{{semver}}`·website, `{{sha}}`·api, `{{sha}}`·cli, `{{sha}}`·integration, `{{sha}}`·website, `{{string}}`·api, `{{string}}`·integration, `{{string}}`·website, `{{time}}`·api, `{{time}}`·cli, `{{time}}`·integration, `{{time}}`·website, `{{ulid}}`·api, `{{ulid}}`·cli, `{{ulid}}`·integration, `{{ulid}}`·website, `{{url}}`·api, `{{url}}`·integration, `{{url}}`·website, `{{uuid}}`·api, `{{uuid}}`·cli, `{{uuid}}`·integration, `{{uuid}}`·website, `{{workdir}}`·api, `{{workdir}}`·integration, `{{workdir}}`·website
|
|
@@ -436,7 +436,7 @@ The table is GENERATED from the files, because the channel grows faster than a h
|
|
|
436
436
|
| `src/lint/ast.test.ts` | specsAnchor | 7 |
|
|
437
437
|
| `src/lint/cards.test.ts` | signature cards — generation freshness (meta-test) · signature cards — completeness against the facet declaration (meta-test) · the skill routes to the cards (meta-test) · signature cards — what a card says | 15 |
|
|
438
438
|
| `src/lint/checker-crossfile.test.ts` | checker suppression — suppressedLines (checker-disable comments) | 9 |
|
|
439
|
-
| `src/lint/checker-member.test.ts` | the member pass — what a workspace member owes |
|
|
439
|
+
| `src/lint/checker-member.test.ts` | the member pass — what a workspace member owes | 18 |
|
|
440
440
|
| `src/lint/checker-spec.test.ts` | fixSpecDocument — key order and block scalars | 5 |
|
|
441
441
|
| `src/lint/checker.test.ts` | conventions checker — findUnknownTokens (D4) · conventions checker — findKnownTokens (D10) · conventions checker — checkConventionFiles (D4) | 8 |
|
|
442
442
|
| `src/lint/coverage.test.ts` | the coverage ratchet | 8 |
|
package/docs/13-elements.md
CHANGED
|
@@ -214,8 +214,14 @@ The window reaches every LEVEL of a chain, not the target alone: `within(button(
|
|
|
214
214
|
|
|
215
215
|
The window is asked BEFORE the verb acts, not after it fails: a verb waits its whole actionability budget, which is the test's own budget, so a retry afterwards never runs — the test is already over. What it costs on the ordinary path is one count per level of the chain, which waits for nothing.
|
|
216
216
|
|
|
217
|
+
**A widening is decided on a SETTLED surface.** A count is one observation, and a `see()` fired right after a navigating `click()` takes it on the page being left: a descriptor that is perfectly exact on the destination read as substring-only, non-deterministically, and the tell was the printed name coming back equal to the name already written. So a level that designates nothing and has exactly one substring match is given five seconds to arrive before anything is printed, and the verdict is reached once per descriptor per run — a scenario that clicks the same link in ten tests waits on the first of them. Nothing else waits: a descriptor with no substring match at all, which is every ordinary `gone()`, is still two counts and no pause.
|
|
218
|
+
|
|
217
219
|
It widens on exactly one match, never on several: a name two elements carry in part is the ambiguity the old default hid, and the window may not resolve it by guessing which one the author meant. That descriptor designates nothing, as W3 says, and the refusal is the one to read.
|
|
218
220
|
|
|
221
|
+
**The name it prints is the ACCESSIBLE name, and the whole of it.** It is read off the element's ARIA snapshot — the browser's own name computation, which is what the descriptor matched on — and never off the text content: a button holding two inline children reads as `Experiments9` through its text and is named "Experiments 9", and a field's name is its label, which its text carries nothing of. Nor is it truncated: a name cut at eighty characters, mid-word, designates nothing when it is written back. The one descriptor whose printed name is TEXT is `content()`, because text is what it matches. On a device the name is what the device reports as the element's label.
|
|
222
|
+
|
|
223
|
+
**The line goes to stderr**, and never to the console. Vitest CAPTURES the console and its default reporter prints nothing of what it captured, so a warning written with `console.warn` is invisible in the very run that fired it — `npm test` showed zero lines on a run that had one, and a deadline nobody can see is a deadline nobody meets. The node-side adapters write the line to the runner's own stderr; a mounted component, which runs IN the page and has no stderr, hands it back to the node side through `server.commands`. Nothing but a plain `vitest --run` is needed to read it.
|
|
224
|
+
|
|
219
225
|
## `testId()` — the one escape hatch
|
|
220
226
|
|
|
221
227
|
`testId(id)` reads `data-testid` on the web and the accessibility identifier on a device. It is the ONLY descriptor that names something a user cannot perceive, and rule W2 is an error on it: a `testId()` carries a same-line comment naming what the element lacks.
|
package/docs/18-conventions.md
CHANGED
|
@@ -46,6 +46,8 @@ covers (`<file>.test.ts`, I2), and the ASSEMBLED product sits under
|
|
|
46
46
|
C12 and C18). A few specifications have neither address, and they are not
|
|
47
47
|
mistakes.
|
|
48
48
|
|
|
49
|
+
### What has no address of its own
|
|
50
|
+
|
|
49
51
|
What they specify is a REPOSITORY's own surface: the subpaths its
|
|
50
52
|
`package.json` publishes, a `.d.ts` beside every `.js` it ships, the lint guards
|
|
51
53
|
a tree owes, the files a build is expected to write. There is no one module to
|
|
@@ -55,14 +57,39 @@ runner"); put beside a module under `src/`, I2 refuses them ("no neighbour
|
|
|
55
57
|
`exports.ts` found"). Both refusals are right, and a legitimate file shape with
|
|
56
58
|
no green position is a hole in the conventions, not a defect in the file.
|
|
57
59
|
|
|
60
|
+
### The door, and the depth it is judged by
|
|
61
|
+
|
|
58
62
|
**The third door is a FIRST-LEVEL folder under `specs/` that is not a facet
|
|
59
63
|
name.** It is a repository suite: it covers a TREE rather than a product served
|
|
60
64
|
through an entry. C12's rename clause does not reach it, C18 and C20 do not
|
|
61
|
-
reach it, and C1's declared depth is what judges its shape.
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
65
|
+
reach it, and C1's declared depth is what judges its shape.
|
|
66
|
+
|
|
67
|
+
**C1 judges a suite exactly as it judges a facet.** The suite folder is the
|
|
68
|
+
first level and the tests sit one level down, in a folder named for what they
|
|
69
|
+
cover: at the default depth, `specs/<suite>/<domain>/<aspect>.test.ts`. A test
|
|
70
|
+
written straight into the suite folder is refused for the same reason one at a
|
|
71
|
+
facet root is — the level that says what a file covers would be missing. This
|
|
72
|
+
package's own `specs/lint/` is two levels deep for that reason.
|
|
73
|
+
|
|
74
|
+
### The two names the estate shares
|
|
75
|
+
|
|
76
|
+
Two suites carry names the whole estate shares, because two repositories
|
|
77
|
+
inventing two names for one idea is the drift a vocabulary exists to stop: a
|
|
78
|
+
package's own published SURFACE is `surface/`, and a repository's own lint
|
|
79
|
+
guards are `lint/`. Every other suite is named for what it covers.
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
specs/
|
|
83
|
+
├── surface/ # what the package publishes
|
|
84
|
+
│ └── package/
|
|
85
|
+
│ ├── exports.test.ts # every subpath the manifest resolves
|
|
86
|
+
│ └── types.test.ts # a `.d.ts` beside each `.js` it ships
|
|
87
|
+
└── lint/ # the guards the repository's own tree owes
|
|
88
|
+
└── <family>/
|
|
89
|
+
└── <rule>.test.ts
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### The suffix, and the one thing the door does not license
|
|
66
93
|
|
|
67
94
|
The files there carry `.test.ts` — the UNIT's word — because what they prove is
|
|
68
95
|
read without assembling anything, and `unit()` is what collects them (`roots`,
|
package/docs/19-linting.md
CHANGED
|
@@ -141,26 +141,26 @@ Every rule the framework enforces, across its enforcement channels — **statiqu
|
|
|
141
141
|
|
|
142
142
|
### C — Files & folders
|
|
143
143
|
|
|
144
|
-
| Code | Implementation | Channel | Reach | Convention
|
|
145
|
-
| ---- | ----------------------------------------------------------------------- | -------- | -------- |
|
|
146
|
-
| C1 | <a id="c1-domain-structure"></a>`c1-domain-structure` | statique | specs | A specs tree DECLARES its shape through the `depth` option: `facet-domain` (the default — a test at facet/domain depth, a `*.specification.ts` at the facet root), `facet` (a test at the facet root OR one domain folder down, never deeper), `mirror` (a test at any depth ≥ 1, named after its directory), `off`. In every mode, `off` included: a directory with a leading underscore is GROUND, never a domain — no spec lives inside one. One exception, and only one: ground may be CODE, and a `<module>.test.ts` sitting BESIDE the `<module>.ts` it tests is legal there (that is I2). A test with no neighbouring module, or a `*.specification.ts`, is still a violation. A `.test.tsx` is out of reach: a rendered unit is specified beside its component, never in a facet/domain tree — I2 holds its neighbourhood.
|
|
147
|
-
| C4 | <a id="c4-contract-shape"></a>`c4-contract-shape` | statique | contract | Under `specs/**/contracts/`: the ROOT carries only `*.contracts.ts` facades (a default export built by `defineContracts(...)` or a composition re-export; named exports are scenarios) and the provider folders `http` \| `openai` \| `anthropic`. A unit contract is `<provider>/<kebab-name>.ts` with an `export default defineContract(...)` (or a factory returning one), and its `request` builder must name the provider of its own folder. A provider folder is flat and holds only `*.ts` and `*.response.json`.
|
|
148
|
-
| C6 | <a id="c6-tomatch-extension"></a>`c6-tomatch-extension` | statique | tests | `toMatch`'s argument carries its extension (`'help.txt'`, `'<case>.aria.yaml'` for a tree golden), except for a directory snapshot; a file subject with no extension is an error.
|
|
149
|
-
| C7 | <a id="c7-seeds-sql-only"></a>`c7-seeds-sql-only` | statique | ground | `_seeds/` holds `*.sql` files only; any other file is an error.
|
|
150
|
-
| C8 | <a id="c8-referenced-fixture-exists"></a>`c8-referenced-fixture-exists` | statique | tests | A literal passed to `.request`/`.seed`/`.fixture`/`toMatch` must exist on disk under its conventional root; a missing path is an error.
|
|
151
|
-
| C10 | <a id="c10-contracts-boundary"></a>`c10-contracts-boundary` | statique | tests | Outside `specs/**/contracts/`, an import resolving into a provider folder (`contracts/{http,openai,anthropic}/…`) is an error: a test imports only the `*.contracts.ts` facades.
|
|
152
|
-
| C11 | <a id="c11-contract-data-pairing"></a>`c11-contract-data-pairing` | statique | contract | Inside a provider folder, every `*.response.json` and every `*.request.ts` has a `<stem>.ts` sibling — the stem is the name up to the FIRST dot (`events.fr.response.json` → `events.ts`). Orphan data is an error.
|
|
153
|
-
| C13 | <a id="c13-underscored-ground"></a>`c13-underscored-ground` | statique | specs | Under `specs/`, a spec's ground carries a leading underscore: `_fixtures/`, `_expected/`, `_requests/`, `_seeds/`. A `fixtures/`, `expected/`, `requests/` or `seeds/` directory is a pre-14 name — an error naming the rename.
|
|
154
|
-
| C9 | <a id="c9-dead-fixtures"></a>`c9-dead-fixtures` | checker | ground | No dead fixtures: every file under `_seeds/`/`_requests/`/`_fixtures/` and every top-level entry of `_expected/` must be referenced; a feature directory with no test file and no `*.spec.yaml` is an orphan (a warning when the argument is not a literal). A `<case>.spec.yaml` references the fixtures its `fixture:` entries name.
|
|
155
|
-
| C12 | <a id="c12-spec-file-name"></a>`c12-spec-file-name` | checker | specs | The suffix says the kind, and the tree agrees with it. A `.spec.ts` — the ASSEMBLED product — lives under `specs/<facet>/`; anywhere else is an error. A `.test.ts` — the UNIT — lives beside its module; under a facet folder (`api`, `cli`, `integration`, `jobs`, `mobile`, `website`) it is an error, fixable by `--fix` (a `git mv`). A `.test.ts` that reaches no runner is C18’s, whose fix is the opposite move, and the mover leaves it alone. A first level that is NOT a facet is a repository suite: it covers a tree, keeps `.test.ts`, and C1’s declared depth is what judges it — unless the tree carries its `*.specification.ts` at its own ROOT, which makes the whole tree one facet’s and every `.test.ts` below it a spec. A document is named `<case>.spec.yaml`: `<case>` in kebab-case, without the words `test`/`spec`/`cli` the suffix already carries, and never the bare name of its directory.
|
|
156
|
-
| C14 | <a id="c14-pool-fixture-shared"></a>`c14-pool-fixture-shared` | checker | ground | A pool fixture is SHARED, or it is local: a directory of `<specs>/_fixtures/` referenced (`$FIXTURES/<name>`) from a single spec directory — two documents of one leaf count as one — must live beside that leaf, at `<leaf>/_fixtures/<name>/`, referenced by the relative form. Zero references is the dead-fixture error (C9). Fixed by `--fix`: the directory is moved and the literals rewritten.
|
|
157
|
-
| C15 | <a id="c15-local-fixture-reach"></a>`c15-local-fixture-reach` | checker | ground | A local fixture is read only from its own directory: a `fixture:`/`.fixture()` path containing `..`, or whose target leaves the `_fixtures/` of the spec naming it, is an error — ground shared by several leaves goes in the `$FIXTURES` pool.
|
|
158
|
-
| C18 | <a id="c18-module-test-under-facet"></a>`c18-module-test-under-facet` | checker | specs | A test under `specs/<facet>/`, where the facet root holds a `*.specification.ts(x)`, imports that module or constructs a runner itself. Ground is out of reach — nothing under a leading-underscore folder is a spec, a `.test.ts` under `_expected/` being the GOLDEN a fixer writes. So is any first-level folder that is not a facet: a specification with no module to sit beside and no runner to reach lives in a repository suite there.
|
|
159
|
-
| C20 | <a id="c20-facet-folder"></a>`c20-facet-folder` | checker | specs | A first-level folder named for one of the six facets holds a `*.specification.ts(x)` calling `specification.<facet>(`. Positive-only: any other first-level name is a repository suite, judged by C1's declared depth alone.
|
|
160
|
-
| C21 | <a id="c21w-ground-owned-by-one"></a>`c21w-ground-owned-by-one` | checker | ground | Ground in a leaf holding ≥ 2 node tests (`.test.ts`, `.spec.ts`) that a single one of them reads is a warning. A `.test.tsx` tree is out of reach, as it is for C1.
|
|
161
|
-
| C23 | <a id="c23-seed-no-clock-read"></a>`c23-seed-no-clock-read` | checker | ground | A `_seeds/**/*.sql` states absolute timestamps: `CURRENT_TIMESTAMP`/`CURRENT_DATE`/`CURRENT_TIME`, `NOW()` and the quoted `'now'` SQLite's date functions take (`datetime('now')`, `strftime(…, 'now')`, `date('now', '-1 day')`) are refused. A comment is not state, so a clock read inside one is not a finding.
|
|
162
|
-
| C5 | <a id="c5-slash-is-a-subfolder"></a>`c5-slash-is-a-subfolder` | runtime | ground | A slash in a fixture name is a SUBFOLDER of `_expected/`: `toMatch('build/verbose.txt')` resolves to `_expected/build/verbose.txt`.
|
|
163
|
-
| C1 | <a id="c1-asset-grouping"></a>`c1-asset-grouping` | process | specs | The folder follows the assets: a test with asset directories of its own gets its own domain; tests with no local assets group as sibling `<aspect>` files. The static rule only checks the depth.
|
|
144
|
+
| Code | Implementation | Channel | Reach | Convention | Fix | Rationale |
|
|
145
|
+
| ---- | ----------------------------------------------------------------------- | -------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
146
|
+
| C1 | <a id="c1-domain-structure"></a>`c1-domain-structure` | statique | specs | A specs tree DECLARES its shape through the `depth` option: `facet-domain` (the default — a test at facet/domain depth, a `*.specification.ts` at the facet root), `facet` (a test at the facet root OR one domain folder down, never deeper), `mirror` (a test at any depth ≥ 1, named after its directory), `off`. A first-level folder that is no facet — a repository suite, `surface/` or `lint/` — is judged by the same depth as a facet is. In every mode, `off` included: a directory with a leading underscore is GROUND, never a domain — no spec lives inside one. One exception, and only one: ground may be CODE, and a `<module>.test.ts` sitting BESIDE the `<module>.ts` it tests is legal there (that is I2). A test with no neighbouring module, or a `*.specification.ts`, is still a violation. A `.test.tsx` is out of reach: a rendered unit is specified beside its component, never in a facet/domain tree — I2 holds its neighbourhood. | Move the file to the depth the tree declares, or declare the depth the tree actually has. | A fixed depth makes every file's place predictable and statically checkable. |
|
|
147
|
+
| C4 | <a id="c4-contract-shape"></a>`c4-contract-shape` | statique | contract | Under `specs/**/contracts/`: the ROOT carries only `*.contracts.ts` facades (a default export built by `defineContracts(...)` or a composition re-export; named exports are scenarios) and the provider folders `http` \| `openai` \| `anthropic`. A unit contract is `<provider>/<kebab-name>.ts` with an `export default defineContract(...)` (or a factory returning one), and its `request` builder must name the provider of its own folder. A provider folder is flat and holds only `*.ts` and `*.response.json`. | Move the file to the half it belongs to: a facade at the root, a unit under its provider. | A fixed tree separates the public facade from the internal units and makes the FOLDER carry the provider — filenames go back to being domain words, and bulky data sits beside the contract that serves it. |
|
|
148
|
+
| C6 | <a id="c6-tomatch-extension"></a>`c6-tomatch-extension` | statique | tests | `toMatch`'s argument carries its extension (`'help.txt'`, `'<case>.aria.yaml'` for a tree golden), except for a directory snapshot; a file subject with no extension is an error. | Write the fixture's full name, extension included. | The extension is part of the expected file's name — dropping it breaks `_expected/` resolution. |
|
|
149
|
+
| C7 | <a id="c7-seeds-sql-only"></a>`c7-seeds-sql-only` | statique | ground | `_seeds/` holds `*.sql` files only; any other file is an error. | Write the seed as SQL, or move the file to `_fixtures/` as the material it is. | `.seed()` carries database state only — no seed handlers, no prefix dispatch. |
|
|
150
|
+
| C8 | <a id="c8-referenced-fixture-exists"></a>`c8-referenced-fixture-exists` | statique | tests | A literal passed to `.request`/`.seed`/`.fixture`/`toMatch` must exist on disk under its conventional root; a missing path is an error. | Create the file, or fix the name — the message prints the path it looked for. | Catching a typo statically avoids a failure that would only surface at run time. |
|
|
151
|
+
| C10 | <a id="c10-contracts-boundary"></a>`c10-contracts-boundary` | statique | tests | Outside `specs/**/contracts/`, an import resolving into a provider folder (`contracts/{http,openai,anthropic}/…`) is an error: a test imports only the `*.contracts.ts` facades. | Add a named scenario export to the facade and import that instead. | Unit contracts are composition details; going through the facade keeps each scenario named in the same place as the world it derives from. |
|
|
152
|
+
| C11 | <a id="c11-contract-data-pairing"></a>`c11-contract-data-pairing` | statique | contract | Inside a provider folder, every `*.response.json` and every `*.request.ts` has a `<stem>.ts` sibling — the stem is the name up to the FIRST dot (`events.fr.response.json` → `events.ts`). Orphan data is an error. | Write the contract that serves the payload, or delete the payload. | Data exists only as served by a contract — a file with no owner is dead weight no test ever loads. |
|
|
153
|
+
| C13 | <a id="c13-underscored-ground"></a>`c13-underscored-ground` | statique | specs | Under `specs/`, a spec's ground carries a leading underscore: `_fixtures/`, `_expected/`, `_requests/`, `_seeds/`. A `fixtures/`, `expected/`, `requests/` or `seeds/` directory is a pre-14 name — an error naming the rename. | Rename the directory with the leading underscore the resolvers read. | No resolver reads the underscore-less name any more: left as is, the tree goes invisible rather than wrong, and the failure lands one step past its cause. |
|
|
154
|
+
| C9 | <a id="c9-dead-fixtures"></a>`c9-dead-fixtures` | checker | ground | No dead fixtures: every file under `_seeds/`/`_requests/`/`_fixtures/` and every top-level entry of `_expected/` must be referenced; a feature directory with no test file and no `*.spec.yaml` is an orphan (a warning when the argument is not a literal). A `<case>.spec.yaml` references the fixtures its `fixture:` entries name. | Reference it from the spec it stands under, or delete it. | The C8 mirror — a fixture nothing references is dead weight that misleads the reader. |
|
|
155
|
+
| C12 | <a id="c12-spec-file-name"></a>`c12-spec-file-name` | checker | specs | The suffix says the kind, and the tree agrees with it. A `.spec.ts` — the ASSEMBLED product — lives under `specs/<facet>/`; anywhere else is an error. A `.test.ts` — the UNIT — lives beside its module; under a facet folder (`api`, `cli`, `integration`, `jobs`, `mobile`, `website`) it is an error, fixable by `--fix` (a `git mv`). A `.test.ts` that reaches no runner is C18’s, whose fix is the opposite move, and the mover leaves it alone. A first level that is NOT a facet is a repository suite: it covers a tree, keeps `.test.ts`, and C1’s declared depth is what judges it — unless the tree carries its `*.specification.ts` at its own ROOT, which makes the whole tree one facet’s and every `.test.ts` below it a spec. A document is named `<case>.spec.yaml`: `<case>` in kebab-case, without the words `test`/`spec`/`cli` the suffix already carries, and never the bare name of its directory. | Move the `.spec.ts` under `specs/<facet>/` (or rename it `.test.ts` beside its module); rename the facet `.test.ts` to `.spec.ts` — `jterrazz-test-check <root> --fix` does it with `git mv`, and names every `include`/`exclude` entry of the member the rename broke. | With no agreement between the suffix and the tree, a reader cannot say what a test proves without opening it; and the filename is the first sentence they read — `rm/rm.spec.yaml` says nothing twice. |
|
|
156
|
+
| C14 | <a id="c14-pool-fixture-shared"></a>`c14-pool-fixture-shared` | checker | ground | A pool fixture is SHARED, or it is local: a directory of `<specs>/_fixtures/` referenced (`$FIXTURES/<name>`) from a single spec directory — two documents of one leaf count as one — must live beside that leaf, at `<leaf>/_fixtures/<name>/`, referenced by the relative form. Zero references is the dead-fixture error (C9). Fixed by `--fix`: the directory is moved and the literals rewritten. | Run `jterrazz-test-check <root> --fix`; it moves the directory and rewrites what named it. | A single scenario's fixture parked in the pool reads as shared ground: everyone assumes someone else depends on it and nobody dares touch it. |
|
|
157
|
+
| C15 | <a id="c15-local-fixture-reach"></a>`c15-local-fixture-reach` | checker | ground | A local fixture is read only from its own directory: a `fixture:`/`.fixture()` path containing `..`, or whose target leaves the `_fixtures/` of the spec naming it, is an error — ground shared by several leaves goes in the `$FIXTURES` pool. | Copy it under this leaf's own `_fixtures/`, or promote it to the pool. | A leaf reaching into a neighbour's fixtures is the pool through another door, without the pool's visibility: the neighbour breaks a spec two folders away without knowing. |
|
|
158
|
+
| C18 | <a id="c18-module-test-under-facet"></a>`c18-module-test-under-facet` | checker | specs | A test under `specs/<facet>/`, where the facet root holds a `*.specification.ts(x)`, imports that module or constructs a runner itself. Ground is out of reach — nothing under a leading-underscore folder is a spec, a `.test.ts` under `_expected/` being the GOLDEN a fixer writes. So is any first-level folder that is not a facet: a specification with no module to sit beside and no runner to reach lives in a repository suite there. | Import `{ <facet> }` from `<facet>.specification.js`, move a module test beside its module, or move a repository suite to a first-level folder of its own under `specs/`. | A test reaching no runner under a facet is a module test paying the facet's budget and services to prove something that belongs beside its module. |
|
|
159
|
+
| C20 | <a id="c20-facet-folder"></a>`c20-facet-folder` | checker | specs | A first-level folder named for one of the six facets holds a `*.specification.ts(x)` calling `specification.<facet>(`. Positive-only: any other first-level name is a repository suite, judged by C1's declared depth alone. | Create the specification the folder promises, or rename the folder. | Folder = constructor is the law the fork rests on; a facet folder with no runner tells the reader something false before they open a file. |
|
|
160
|
+
| C21 | <a id="c21w-ground-owned-by-one"></a>`c21w-ground-owned-by-one` | checker | ground | Ground in a leaf holding ≥ 2 node tests (`.test.ts`, `.spec.ts`) that a single one of them reads is a warning. A `.test.tsx` tree is out of reach, as it is for C1. | Give that spec its own domain folder, with the ground beside it. | Ground one spec reads, parked where several sit, reads as shared: everyone assumes someone else depends on it and nobody dares touch it. |
|
|
161
|
+
| C23 | <a id="c23-seed-no-clock-read"></a>`c23-seed-no-clock-read` | checker | ground | A `_seeds/**/*.sql` states absolute timestamps: `CURRENT_TIMESTAMP`/`CURRENT_DATE`/`CURRENT_TIME`, `NOW()` and the quoted `'now'` SQLite's date functions take (`datetime('now')`, `strftime(…, 'now')`, `date('now', '-1 day')`) are refused. A comment is not state, so a clock read inside one is not a finding. | Write the instant the case is about, and pin the run's clock with `.clock()` on the chain. | A seed that reads the clock makes the row's date the date the suite happened to run: a case about "published yesterday" proves something different every day, and under SQLite `datetime('now')` writes a space-separated text where an ORM writes ISO `T`, so the two misorder across a calendar day. The R8 dry run counts 36, every one of them signews-api's. |
|
|
162
|
+
| C5 | <a id="c5-slash-is-a-subfolder"></a>`c5-slash-is-a-subfolder` | runtime | ground | A slash in a fixture name is a SUBFOLDER of `_expected/`: `toMatch('build/verbose.txt')` resolves to `_expected/build/verbose.txt`. | Name the subfolder in the fixture name; the resolver creates it under `_expected/`. | A flat `_expected/` stops being readable at a dozen files, and the alternative — a second option naming a directory — would be a second way to say where a golden lives. |
|
|
163
|
+
| C1 | <a id="c1-asset-grouping"></a>`c1-asset-grouping` | process | specs | The folder follows the assets: a test with asset directories of its own gets its own domain; tests with no local assets group as sibling `<aspect>` files. The static rule only checks the depth. | Read what the assets say: a test that stands on its own ground earns a folder. | It is the assets that settle the grouping — a criterion no channel can decide alone. |
|
|
164
164
|
|
|
165
165
|
### D — Assertions
|
|
166
166
|
|
|
@@ -226,12 +226,12 @@ Every rule the framework enforces, across its enforcement channels — **statiqu
|
|
|
226
226
|
|
|
227
227
|
### J — Hygiene
|
|
228
228
|
|
|
229
|
-
| Code | Implementation | Channel | Reach | Convention
|
|
230
|
-
| ---- | ------------------------------------------------------------------------- | -------- | ----- |
|
|
231
|
-
| J2 | <a id="j2-no-sleep"></a>`j2-no-sleep` | statique | tests | No arbitrary sleep (`setTimeout`/`setInterval`/`Atomics.wait`, or a `node:timers/promises` import) in any test file — synchronisation is `see()`/`gone()` inside a scenario and `waitUntil(predicate)` everywhere else. A timer inside a test DOUBLE's implementation (`vi.fn(…)`, `mockImplementation(…)`, a `mockOf` handler)
|
|
232
|
-
| J6 | <a id="j6w-given-in-the-test"></a>`j6w-given-in-the-test` | statique | tests | A `beforeEach`/`beforeAll` in a test file is a warning, and so is an `afterEach`/`afterAll` whose body does more than restore (`cleanup`, `vi.useRealTimers`, `vi.restoreAllMocks`, `vi.resetAllMocks`, `vi.unstubAllGlobals`, `vi.unstubAllEnvs`) — handed over by name (`afterEach(vi.restoreAllMocks)`) or called in the body. `afterAll(cleanup)` in a `*.specification.ts` is A4's own idiom and out of reach by role.
|
|
233
|
-
| J10 | <a id="j10-one-describe-level"></a>`j10-one-describe-level` | upstream | tests | At most one level of `describe` — `vitest/max-nested-describe` with `{ max: 1 }`, set by the `testing` fragment.
|
|
234
|
-
| J9 | <a id="j9-checker-suppression-reason"></a>`j9-checker-suppression-reason` | checker | specs | Every `checker-disable-next-line` / `checker-disable-line` directive carries ` -- <reason>`, wherever a checker pass can be silenced: a test, a document, a fixture under ground. The word inside a string is prose, not a directive.
|
|
229
|
+
| Code | Implementation | Channel | Reach | Convention | Fix | Rationale |
|
|
230
|
+
| ---- | ------------------------------------------------------------------------- | -------- | ----- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
231
|
+
| J2 | <a id="j2-no-sleep"></a>`j2-no-sleep` | statique | tests | No arbitrary sleep (`setTimeout`/`setInterval`/`Atomics.wait`, or a `node:timers/promises` import) in any test file — synchronisation is `see()`/`gone()` inside a scenario and `waitUntil(predicate)` everywhere else. A timer inside a test DOUBLE's implementation is out of reach: the double is staging the world, not the test waiting on it. A double is a factory call (`vi.fn(…)`, `mockImplementation(…)`, a `mockOf` handler) or a function-valued PROPERTY of an object literal, which is the plainest double there is — `const git: GitGateway = { cloneRepository: async () => … }`, where the object itself is the double. | Wait for the condition, not for a duration. | A fixed sleep makes tests slow and flaky; waiting on a condition is deterministic. |
|
|
232
|
+
| J6 | <a id="j6w-given-in-the-test"></a>`j6w-given-in-the-test` | statique | tests | A `beforeEach`/`beforeAll` in a test file is a warning, and so is an `afterEach`/`afterAll` whose body does more than restore (`cleanup`, `vi.useRealTimers`, `vi.restoreAllMocks`, `vi.resetAllMocks`, `vi.unstubAllGlobals`, `vi.unstubAllEnvs`) — handed over by name (`afterEach(vi.restoreAllMocks)`) or called in the body. `afterAll(cleanup)` in a `*.specification.ts` is A4's own idiom and out of reach by role. | Call a function from each test instead of a hook; `clock.at()` and `intercept()` restore themselves. | A hook moves the setup out of the only place a reader looks, shares it with every test in the file, and half of them come to depend on it quietly. |
|
|
233
|
+
| J10 | <a id="j10-one-describe-level"></a>`j10-one-describe-level` | upstream | tests | At most one level of `describe` — `vitest/max-nested-describe` with `{ max: 1 }`, set by the `testing` fragment. | Flatten the nesting: the test name is the sentence, and one level groups the file. | A tree of contexts is state a reader has to hold in their head to know what a test is about, and the name of the test stops being the sentence it was. |
|
|
234
|
+
| J9 | <a id="j9-checker-suppression-reason"></a>`j9-checker-suppression-reason` | checker | specs | Every `checker-disable-next-line` / `checker-disable-line` directive carries ` -- <reason>`, wherever a checker pass can be silenced: a test, a document, a fixture under ground. The word inside a string is prose, not a directive. | Write `checker-disable-next-line <id> -- <reason>`. | A suppression with no reason is a rule turned off by someone no longer here: the next reader cannot tell whether the pass was wrong or simply in the way, so the line survives every review. The toolchain's own gate reads `oxlint-disable*` and nothing else. |
|
|
235
235
|
|
|
236
236
|
### K — Retro-propagation
|
|
237
237
|
|
|
@@ -442,6 +442,8 @@ F8 says which of the two it found: a carried dependency is "a transitive of `@jt
|
|
|
442
442
|
|
|
443
443
|
**The jest allowance follows the tests, not the manifest.** `@testing-library/react-native` stands where jest runs the member's tests, and jest is not always the member's own: a library co-locating its render tests in `src/` while the app beside it owns the only working React Native toolchain runs them from THAT app's `jest.config.*`. So the pass reads two sources — the member's own declaration (a `jest`/`jest-expo` dependency, a `jest` field, a `test` script calling it), and a `jest.config.*` anywhere in the workspace whose `roots`/`testMatch` resolve INTO the member (`<rootDir>` is jest's own token for the config's directory). A config that reaches another member clears nothing here: the allowance is about the files jest collects.
|
|
444
444
|
|
|
445
|
+
The workspace it searches is walked UP to, from the member, and never taken from the anchor the run was started at: the app that runs the member's tests sits BESIDE it, which is above the member and never below it. `cd packages/native && jterrazz-test-check` — the run that member's own gate makes — otherwise found no jest at all and reported F8 while the root-anchored run cleared the same member. The walk stops at the first directory declaring a workspace (`workspaces` in its manifest, or a `pnpm-workspace.yaml`) and at the checkout's own boundary, since a `.git` is where this repository ends.
|
|
446
|
+
|
|
445
447
|
`jterrazz-test-check --member <dir>` runs them over one member. `jterrazz-test-check` with NO path runs both halves over the whole project: every `specs/` root discovered from the root manifest's `workspaces`, and every member. That is the run the toolchain's ratchet rests on, so the contract is exactly "a path-less run reports what the per-root and per-member runs report" — and the discovery is the toolchain's, to the segment: a `workspaces` glob expands with `*` for one segment and `**` for any depth (bounded at six), and each member is walked for the spec roots it owns, a nested `web/specs` included. A shape `@jterrazz/typescript` reaches and this run did not would be a finding the ratchet could neither record nor clear.
|
|
446
448
|
|
|
447
449
|
`--format json` prints `[{ code, file, line, severity, message }]` on stdout and nothing else. `code` is `jterrazz-check(<id>)` — `jterrazz-check(d4)`, `jterrazz-check(f8)` — which is the namespace `@jterrazz/typescript`'s ratchet records the counts under, flat, in `oxlint.baseline.json` beside the oxlint ones. Severity travels but is never read by the ratchet: a finding a release ships at `warn` is exactly the debt a ratchet exists to hold down.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jterrazz/test",
|
|
3
|
-
"version": "16.1.
|
|
3
|
+
"version": "16.1.1",
|
|
4
4
|
"description": "Declarative testing framework for HTTP APIs, CLIs, background jobs, websites, mobile apps and rendered components — one entry point, real infrastructure and a real browser, golden-file assertions, plus an oxlint convention plugin.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"api-testing",
|
|
@@ -77,7 +77,7 @@
|
|
|
77
77
|
},
|
|
78
78
|
"devDependencies": {
|
|
79
79
|
"@hono/node-server": "^2.0.8",
|
|
80
|
-
"@jterrazz/typescript": "^10.
|
|
80
|
+
"@jterrazz/typescript": "^10.3.1",
|
|
81
81
|
"@types/better-sqlite3": "^7.6.13",
|
|
82
82
|
"@types/node": "^26.1.1",
|
|
83
83
|
"@types/pg": "^8.20.0",
|