@jterrazz/test 16.0.0 → 16.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/appium.adapter.js +59 -4
- package/dist/browser/index.js +88 -40
- package/dist/checker.js +272 -63
- package/dist/index.d.ts +33 -4
- package/dist/index.js +47 -24
- package/dist/oxlint.cjs +100 -27
- package/dist/oxlint.js +100 -27
- package/dist/playwright.adapter.js +14 -65
- package/dist/{ambiguity.js → substring-warning.js} +99 -1
- package/dist/vitest.js +71 -11
- package/docs/01-architecture.md +95 -0
- package/docs/02-developing.md +351 -0
- package/docs/03-testing.md +495 -0
- package/docs/04-operating.md +101 -0
- package/docs/05-module-tests.md +152 -0
- package/docs/06-integration.md +180 -0
- package/docs/07-component.md +246 -0
- package/docs/08-website.md +404 -0
- package/docs/09-mobile.md +270 -0
- package/docs/10-api.md +264 -0
- package/docs/11-jobs.md +157 -0
- package/docs/12-cli.md +677 -0
- package/docs/13-elements.md +242 -0
- package/docs/14-assertions.md +314 -0
- package/docs/15-tokens.md +253 -0
- package/docs/16-contracts.md +438 -0
- package/docs/17-services.md +188 -0
- package/docs/18-conventions.md +305 -0
- package/docs/19-linting.md +510 -0
- package/docs/README.md +36 -0
- package/docs/decisions/001-v9-conventions-on-three-channels.md +50 -0
- package/docs/decisions/002-the-statique-channel-holds-only-what-upstream-does-not.md +49 -0
- package/docs/decisions/003-a-rendered-component-is-a-facet-on-vitest-browser-mode.md +84 -0
- package/docs/decisions/004-a-module-against-real-services-or-a-golden-is-the-integration-facet.md +81 -0
- package/docs/decisions/005-the-doubles-ladder-is-closed.md +86 -0
- package/docs/decisions/006-the-catalogue-reaches-a-file-by-its-role.md +245 -0
- package/docs/decisions/007-compose-mode-leaves-the-package.md +53 -0
- package/docs/decisions/008-vitest-5-node-24-and-the-native-seams-as-optional-peers.md +75 -0
- package/docs/decisions/_template.md +22 -0
- package/docs/reference/classes/Matcher.md +120 -0
- package/docs/reference/classes/ProcessHandle.md +368 -0
- package/docs/reference/functions/defineContract.md +39 -0
- package/docs/reference/functions/defineContracts.md +36 -0
- package/docs/reference/functions/disabled.md +28 -0
- package/docs/reference/functions/enabled.md +20 -0
- package/docs/reference/functions/focused.md +26 -0
- package/docs/reference/functions/mockOf.md +79 -0
- package/docs/reference/functions/postgres.md +31 -0
- package/docs/reference/functions/process.md +33 -0
- package/docs/reference/functions/redis.md +28 -0
- package/docs/reference/functions/required.md +41 -0
- package/docs/reference/functions/selected.md +26 -0
- package/docs/reference/functions/sqlite.md +33 -0
- package/docs/reference/functions/testId.md +20 -0
- package/docs/reference/functions/text.md +40 -0
- package/docs/reference/functions/valued.md +28 -0
- package/docs/reference/functions/waitUntil.md +46 -0
- package/docs/reference/functions/within.md +29 -0
- package/docs/reference/index.md +196 -0
- package/docs/reference/interfaces/BaseResult.md +87 -0
- package/docs/reference/interfaces/CallResult.md +160 -0
- package/docs/reference/interfaces/CaptureScope.md +82 -0
- package/docs/reference/interfaces/CliResult.md +253 -0
- package/docs/reference/interfaces/ContainerAccessor.md +166 -0
- package/docs/reference/interfaces/DirectoryAccessor.md +33 -0
- package/docs/reference/interfaces/FetchResult.md +180 -0
- package/docs/reference/interfaces/FilesystemAccessor.md +46 -0
- package/docs/reference/interfaces/HttpResult.md +126 -0
- package/docs/reference/interfaces/JsonAccessor.md +46 -0
- package/docs/reference/interfaces/PageResult.md +356 -0
- package/docs/reference/interfaces/RenderResult.md +72 -0
- package/docs/reference/interfaces/RenderedText.md +110 -0
- package/docs/reference/interfaces/ResponseAccessor.md +47 -0
- package/docs/reference/interfaces/ScreenResult.md +128 -0
- package/docs/reference/interfaces/TableAccessor.md +26 -0
- package/docs/reference/interfaces/TextAccessor.md +96 -0
- package/docs/reference/type-aliases/ApiHandle.md +68 -0
- package/docs/reference/type-aliases/ApiSpecification.md +213 -0
- package/docs/reference/type-aliases/ApiSpecificationOptions.md +68 -0
- package/docs/reference/type-aliases/BrowserConsoleMessage.md +30 -0
- package/docs/reference/type-aliases/BrowserLinkElement.md +50 -0
- package/docs/reference/type-aliases/BrowserMetaElement.md +40 -0
- package/docs/reference/type-aliases/BrowserOpenOptions.md +87 -0
- package/docs/reference/type-aliases/BrowserPage.md +132 -0
- package/docs/reference/type-aliases/BrowserPort.md +51 -0
- package/docs/reference/type-aliases/CliEnv.md +11 -0
- package/docs/reference/type-aliases/CliHandle.md +80 -0
- package/docs/reference/type-aliases/CliInput.md +36 -0
- package/docs/reference/type-aliases/CliOutput.md +46 -0
- package/docs/reference/type-aliases/CliPort.md +58 -0
- package/docs/reference/type-aliases/CliSpecification.md +144 -0
- package/docs/reference/type-aliases/CliSpecificationOptions.md +134 -0
- package/docs/reference/type-aliases/ComponentChain.md +156 -0
- package/docs/reference/type-aliases/ComponentScenario.md +20 -0
- package/docs/reference/type-aliases/ComponentUi.md +47 -0
- package/docs/reference/type-aliases/ComponentVisitor.md +10 -0
- package/docs/reference/type-aliases/ContainerPort.md +113 -0
- package/docs/reference/type-aliases/Contract.md +68 -0
- package/docs/reference/type-aliases/ContractInput.md +13 -0
- package/docs/reference/type-aliases/ContractRequest.md +95 -0
- package/docs/reference/type-aliases/ContractResponder.md +23 -0
- package/docs/reference/type-aliases/ContractResponse.md +78 -0
- package/docs/reference/type-aliases/ContractResponseValue.md +13 -0
- package/docs/reference/type-aliases/Contracts.md +48 -0
- package/docs/reference/type-aliases/DatabaseKeys.md +16 -0
- package/docs/reference/type-aliases/DatabasePort.md +72 -0
- package/docs/reference/type-aliases/DeviceOpenOptions.md +46 -0
- package/docs/reference/type-aliases/DevicePort.md +51 -0
- package/docs/reference/type-aliases/DeviceScreen.md +32 -0
- package/docs/reference/type-aliases/DeviceTimeouts.md +39 -0
- package/docs/reference/type-aliases/DockerSpecConfig.md +43 -0
- package/docs/reference/type-aliases/DomMount.md +21 -0
- package/docs/reference/type-aliases/ElementKind.md +27 -0
- package/docs/reference/type-aliases/ElementMatch.md +74 -0
- package/docs/reference/type-aliases/ElementOptions.md +29 -0
- package/docs/reference/type-aliases/ElementRef.md +128 -0
- package/docs/reference/type-aliases/ExecOptions.md +37 -0
- package/docs/reference/type-aliases/FileAccessor.md +54 -0
- package/docs/reference/type-aliases/HonoApp.md +31 -0
- package/docs/reference/type-aliases/HttpContractFilter.md +49 -0
- package/docs/reference/type-aliases/HttpResponseInit.md +46 -0
- package/docs/reference/type-aliases/HttpStreamInit.md +58 -0
- package/docs/reference/type-aliases/IntegrationHandle.md +46 -0
- package/docs/reference/type-aliases/IntegrationSpecification.md +111 -0
- package/docs/reference/type-aliases/IntegrationSpecificationOptions.md +42 -0
- package/docs/reference/type-aliases/Intercept.md +19 -0
- package/docs/reference/type-aliases/InterceptOptions.md +28 -0
- package/docs/reference/type-aliases/InterceptScope.md +11 -0
- package/docs/reference/type-aliases/IsolationStrategy.md +75 -0
- package/docs/reference/type-aliases/JobHandle.md +34 -0
- package/docs/reference/type-aliases/JobsHandle.md +57 -0
- package/docs/reference/type-aliases/JobsResult.md +16 -0
- package/docs/reference/type-aliases/JobsSpecification.md +94 -0
- package/docs/reference/type-aliases/JobsSpecificationOptions.md +56 -0
- package/docs/reference/type-aliases/LandmarkKind.md +20 -0
- package/docs/reference/type-aliases/LiterateRunFlags.md +27 -0
- package/docs/reference/type-aliases/LiterateServeRegistration.md +70 -0
- package/docs/reference/type-aliases/MatchFixtureOptions.md +27 -0
- package/docs/reference/type-aliases/MatchableRequest.md +61 -0
- package/docs/reference/type-aliases/MatcherKind.md +34 -0
- package/docs/reference/type-aliases/MobileBackendOptions.md +28 -0
- package/docs/reference/type-aliases/MobileElementKind.md +14 -0
- package/docs/reference/type-aliases/MobileElementMatch.md +60 -0
- package/docs/reference/type-aliases/MobileElementRef.md +20 -0
- package/docs/reference/type-aliases/MobileHandle.md +65 -0
- package/docs/reference/type-aliases/MobileScenario.md +20 -0
- package/docs/reference/type-aliases/MobileSpecification.md +51 -0
- package/docs/reference/type-aliases/MobileSpecificationOptions.md +126 -0
- package/docs/reference/type-aliases/MobileVisitor.md +81 -0
- package/docs/reference/type-aliases/MockOfOptions.md +26 -0
- package/docs/reference/type-aliases/MockPort.md +10 -0
- package/docs/reference/type-aliases/PinnedClock.md +10 -0
- package/docs/reference/type-aliases/PostgresOptions.md +32 -0
- package/docs/reference/type-aliases/ProcessOptions.md +112 -0
- package/docs/reference/type-aliases/RedisOptions.md +20 -0
- package/docs/reference/type-aliases/RenderSubject.md +12 -0
- package/docs/reference/type-aliases/ScreenNode.md +67 -0
- package/docs/reference/type-aliases/ServerPort.md +36 -0
- package/docs/reference/type-aliases/ServerResponse.md +46 -0
- package/docs/reference/type-aliases/ServerSpec.md +21 -0
- package/docs/reference/type-aliases/ServiceHandle.md +216 -0
- package/docs/reference/type-aliases/ServiceRecord.md +12 -0
- package/docs/reference/type-aliases/SpecDocument.md +84 -0
- package/docs/reference/type-aliases/SpecEnvToken.md +21 -0
- package/docs/reference/type-aliases/SpecFileAssertion.md +27 -0
- package/docs/reference/type-aliases/SpecFixture.md +30 -0
- package/docs/reference/type-aliases/SpecKind.md +8 -0
- package/docs/reference/type-aliases/SpecRun.md +108 -0
- package/docs/reference/type-aliases/SpecServeEntry.md +40 -0
- package/docs/reference/type-aliases/SpecStream.md +32 -0
- package/docs/reference/type-aliases/SpecificationConfig.md +290 -0
- package/docs/reference/type-aliases/SqliteOptions.md +36 -0
- package/docs/reference/type-aliases/SseEvent.md +58 -0
- package/docs/reference/type-aliases/StreamBody.md +61 -0
- package/docs/reference/type-aliases/TextFilter.md +22 -0
- package/docs/reference/type-aliases/VisitScenario.md +20 -0
- package/docs/reference/type-aliases/Visitor.md +215 -0
- package/docs/reference/type-aliases/WaitUntilOptions.md +46 -0
- package/docs/reference/type-aliases/WebsiteBackendOptions.md +37 -0
- package/docs/reference/type-aliases/WebsiteHandle.md +51 -0
- package/docs/reference/type-aliases/WebsiteSpecification.md +118 -0
- package/docs/reference/type-aliases/WebsiteSpecificationOptions.md +40 -0
- package/docs/reference/variables/anthropic.md +19 -0
- package/docs/reference/variables/banner.md +10 -0
- package/docs/reference/variables/button.md +10 -0
- package/docs/reference/variables/clock.md +16 -0
- package/docs/reference/variables/complementary.md +10 -0
- package/docs/reference/variables/component.md +10 -0
- package/docs/reference/variables/content.md +10 -0
- package/docs/reference/variables/contentinfo.md +10 -0
- package/docs/reference/variables/dialog.md +10 -0
- package/docs/reference/variables/field.md +10 -0
- package/docs/reference/variables/form.md +10 -0
- package/docs/reference/variables/heading.md +10 -0
- package/docs/reference/variables/http.md +40 -0
- package/docs/reference/variables/intercept.md +11 -0
- package/docs/reference/variables/link.md +10 -0
- package/docs/reference/variables/listitem.md +10 -0
- package/docs/reference/variables/main.md +10 -0
- package/docs/reference/variables/match.md +8 -0
- package/docs/reference/variables/navigation.md +10 -0
- package/docs/reference/variables/openai.md +21 -0
- package/docs/reference/variables/option.md +17 -0
- package/docs/reference/variables/region.md +10 -0
- package/docs/reference/variables/row.md +10 -0
- package/docs/reference/variables/search.md +10 -0
- package/docs/reference/variables/specification.md +8 -0
- package/docs/reference/variables/status.md +10 -0
- package/docs/reference/variables/table.md +10 -0
- package/package.json +2 -1
package/dist/oxlint.js
CHANGED
|
@@ -437,7 +437,7 @@ const RULE_DOCS = {
|
|
|
437
437
|
},
|
|
438
438
|
"d8w-text-bypass": {
|
|
439
439
|
channel: "statique",
|
|
440
|
-
convention: "`expect(x.text).toContain/toMatch` short-circuits the typed accessor subject → warning.",
|
|
440
|
+
convention: "`expect(x.text).toContain/toMatch` short-circuits the typed accessor subject → warning. A RegExp argument is exempt: `toMatch` on an accessor takes a fixture NAME and throws on a regex, so reaching through to `.text` is the sanctioned form for a raw pattern (chapter 14).",
|
|
441
441
|
facet: "model",
|
|
442
442
|
family: "D",
|
|
443
443
|
fix: "Assert on the accessor itself (`expect(x)`), which carries the tokens and the fixture resolution.",
|
|
@@ -647,7 +647,7 @@ const RULE_DOCS = {
|
|
|
647
647
|
},
|
|
648
648
|
"i4-no-module-doubles": {
|
|
649
649
|
channel: "statique",
|
|
650
|
-
convention: "A test doubles a PORT, never a module: `vi.mock`/`vi.doMock` of a specifier outside the `modules` allow-list (a config comment states why each entry is there), a `__mocks__/` or `__fixtures__/` directory, and — in a `module`-role test — importing a data asset (`.json`, `.sql`, `.yaml`, …) are all errors. A dotted specifier (`./dashboard.post`) is still code. The `vi.stubGlobal` clause moved to M3 in 16.0: one owner per convention.",
|
|
650
|
+
convention: "A test doubles a PORT, never a module: `vi.mock`/`vi.doMock` of a specifier outside the `modules` allow-list (a config comment states why each entry is there), a `__mocks__/` or `__fixtures__/` directory, and — in a `module`-role test — importing a data asset (`.json`, `.sql`, `.yaml`, …) are all errors. The specifier is read in BOTH spellings — `vi.mock('<spec>')` and the `vi.mock(import('<spec>'))` form `vitest/prefer-import-in-mock` requires — so an allow-listed module has a green spelling under both rules. A dotted specifier (`./dashboard.post`) is still code. The `vi.stubGlobal` clause moved to M3 in 16.0: one owner per convention.",
|
|
651
651
|
facet: "model",
|
|
652
652
|
family: "I",
|
|
653
653
|
fix: "Double the port with `mockOf<Port>()`, keep a payload in a `*.fixtures.ts` neighbour, and allow-list a native module with its reason (chapter 05 § Doubles).",
|
|
@@ -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.",
|
|
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) is out of reach: the double is staging the world, not the test waiting on it.",
|
|
661
661
|
facet: "model",
|
|
662
662
|
family: "J",
|
|
663
663
|
fix: "Wait for the condition, not for a duration.",
|
|
@@ -725,10 +725,10 @@ const CHECKER_PASSES = [
|
|
|
725
725
|
},
|
|
726
726
|
{
|
|
727
727
|
channel: "checker",
|
|
728
|
-
convention: "A test under `specs/<facet>/`, where the facet root holds a `*.specification.ts(x)`, imports that module or constructs a runner itself.",
|
|
728
|
+
convention: "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.",
|
|
729
729
|
facet: "model",
|
|
730
730
|
family: "C",
|
|
731
|
-
fix: "Import `{ <facet> }` from `<facet>.specification.js`,
|
|
731
|
+
fix: "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/`.",
|
|
732
732
|
id: "C18",
|
|
733
733
|
name: "c18-module-test-under-facet",
|
|
734
734
|
rationale: "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.",
|
|
@@ -802,7 +802,7 @@ const CHECKER_PASSES = [
|
|
|
802
802
|
},
|
|
803
803
|
{
|
|
804
804
|
channel: "checker",
|
|
805
|
-
convention: "A member declares no dependency `@jterrazz/test` already carries (`msw`, `vitest-mock-extended`; `yaml` in devDependencies only — as a production dependency it is the product’s own library) and no retired seam (`happy-dom`, `jsdom`, `@testing-library/*`, `@playwright/test`, `mockdate`) — except `@testing-library/react-native`
|
|
805
|
+
convention: "A member declares no dependency `@jterrazz/test` already carries (`msw`, `vitest-mock-extended`; `yaml` in devDependencies only — as a production dependency it is the product’s own library) and no retired seam (`happy-dom`, `jsdom`, `@testing-library/*`, `@playwright/test`, `mockdate`) — except `@testing-library/react-native` where jest runs the tests of the member — the only vocabulary that runtime has until the React Native answer lands. Jest runs them when the member DECLARES it (a `jest`/`jest-expo` dependency, a `jest` field, a `test` script calling it) or when a `jest.config.*` elsewhere in the workspace ROOTS into the member, which is the shape a library co-locating its render tests takes when the app beside it owns the only working toolchain. The optional peers — `playwright`, `vite`, `react`, `better-sqlite3`, `pg`, `redis`, `testcontainers`… — are declared by the consumer, by design.",
|
|
806
806
|
facet: "model",
|
|
807
807
|
family: "F",
|
|
808
808
|
fix: "Remove the declaration: a transitive is already resolved by the package; a retired seam has a facet that replaces it (`component()`, `website()`, `clock`, `intercept()`). The diagnostic carries a sentence for each case.",
|
|
@@ -879,10 +879,10 @@ const CHECKER_PASSES = [
|
|
|
879
879
|
},
|
|
880
880
|
{
|
|
881
881
|
channel: "checker",
|
|
882
|
-
convention: "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. 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.",
|
|
882
|
+
convention: "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.",
|
|
883
883
|
facet: "model",
|
|
884
884
|
family: "C",
|
|
885
|
-
fix: "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
|
|
885
|
+
fix: "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.",
|
|
886
886
|
id: "C12",
|
|
887
887
|
name: "c12-spec-file-name",
|
|
888
888
|
rationale: "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.",
|
|
@@ -1400,7 +1400,7 @@ const RUNTIME_RULES = [
|
|
|
1400
1400
|
},
|
|
1401
1401
|
{
|
|
1402
1402
|
channel: "runtime",
|
|
1403
|
-
convention: "An element descriptor must designate exactly ONE element when a verb ACTS on it (`click`/`tap`/`fill`): if several match, the action is refused with an error enumerating the candidates and offering the rewrites (`within(...)`, another descriptor). The framework never acts on \"the first\". A name designates the WHOLE accessible name since 16.0 — `{ exact: false }` is the opt-out — and a descriptor that finds nothing whole but would have found something as a substring
|
|
1403
|
+
convention: "An element descriptor must designate exactly ONE element when a verb ACTS on it (`click`/`tap`/`fill`): if several match, the action is refused with an error enumerating the candidates and offering the rewrites (`within(...)`, another descriptor). The framework never acts on \"the first\". A name designates the WHOLE accessible name since 16.0 — `{ exact: false }` is the opt-out — and a descriptor that finds nothing whole but would have found something as a substring is still RESOLVED as a substring through 16.x, level by level, with a runtime warning naming the whole name to write, once per descriptor; in 17.0 it is simply not found. It holds on both scenario facets, website and mobile; on mobile, `see()` — which acts on nothing — is satisfied by any visible match (the XCUITest tree legitimately duplicates a label between a container and its child).",
|
|
1404
1404
|
facet: "model",
|
|
1405
1405
|
family: "W",
|
|
1406
1406
|
fix: "Scope it with `within(<landmark>, …)`, or name the element with a descriptor that designates one thing.",
|
|
@@ -1692,6 +1692,13 @@ function identifierName(node) {
|
|
|
1692
1692
|
const { name } = node;
|
|
1693
1693
|
return typeof name === "string" ? name : void 0;
|
|
1694
1694
|
}
|
|
1695
|
+
/** The source end offset of a node or comment (oxlint `end`, else `range[1]`). */
|
|
1696
|
+
function nodeEnd(node) {
|
|
1697
|
+
if (node === void 0) return -1;
|
|
1698
|
+
if (typeof node.end === "number") return node.end;
|
|
1699
|
+
const range = node.range;
|
|
1700
|
+
return Array.isArray(range) && typeof range[1] === "number" ? range[1] : -1;
|
|
1701
|
+
}
|
|
1695
1702
|
/** The three words a test narrates with, in the order they may appear. */
|
|
1696
1703
|
const MARKERS = [
|
|
1697
1704
|
"Given",
|
|
@@ -3145,8 +3152,8 @@ function testsItsNeighbour(filename) {
|
|
|
3145
3152
|
* product command/area with one or more `<aspect>.test.ts` files plus their
|
|
3146
3153
|
* shared asset dirs.
|
|
3147
3154
|
*
|
|
3148
|
-
* - a
|
|
3149
|
-
* `specs/<facet>/<domain>/<
|
|
3155
|
+
* - a spec sits at facet/domain depth — exactly
|
|
3156
|
+
* `specs/<facet>/<domain>/<aspect>.spec.ts`. A test directly at the facet root
|
|
3150
3157
|
* (or nested deeper than a domain) is rejected.
|
|
3151
3158
|
* - a `*.specification.ts` sits at the facet root — exactly
|
|
3152
3159
|
* `specs/<facet>/<file>.specification.ts`, never inside a domain.
|
|
@@ -3154,8 +3161,8 @@ function testsItsNeighbour(filename) {
|
|
|
3154
3161
|
* `depth: 'facet'`
|
|
3155
3162
|
*
|
|
3156
3163
|
* The same facet root, one degree looser: the folder follows the assets. A
|
|
3157
|
-
*
|
|
3158
|
-
* or one domain folder down (`specs/<facet>/<domain>/<aspect>.
|
|
3164
|
+
* spec sits EITHER at the facet root (`specs/<facet>/<aspect>.spec.ts`)
|
|
3165
|
+
* or one domain folder down (`specs/<facet>/<domain>/<aspect>.spec.ts`) — a
|
|
3159
3166
|
* test earns a domain folder when it carries assets of its own, and sits
|
|
3160
3167
|
* beside its siblings when it does not. Anything deeper is rejected, and so is
|
|
3161
3168
|
* a test loose at the specs root. A `*.specification.ts` sits at the facet
|
|
@@ -3164,9 +3171,10 @@ function testsItsNeighbour(filename) {
|
|
|
3164
3171
|
* `depth: 'mirror'`
|
|
3165
3172
|
*
|
|
3166
3173
|
* The tree mirrors something outside itself (a command tree, a source tree), so
|
|
3167
|
-
* the depth is that structure's and no fixed number can name it. A
|
|
3174
|
+
* the depth is that structure's and no fixed number can name it. A spec
|
|
3168
3175
|
* sits at ANY depth of at least one directory under `specs/`, and is NAMED
|
|
3169
|
-
* AFTER the directory holding it — `<dir>/<dir>.
|
|
3176
|
+
* AFTER the directory holding it — `<dir>/<dir>.spec.ts`, the suffix C12 gives
|
|
3177
|
+
* every file of a facet tree. That naming is what
|
|
3170
3178
|
* keeps the shape checkable: one test per mirrored node, no test loose at the
|
|
3171
3179
|
* specs root. Specification files are unconstrained: a mirror has no facet
|
|
3172
3180
|
* level to anchor them to.
|
|
@@ -3267,12 +3275,12 @@ const c1DomainStructure = {
|
|
|
3267
3275
|
messages: {
|
|
3268
3276
|
specInGround: "A spec must not live under \"{{ground}}/\" — a leading underscore marks GROUND (what the specs of a row stand on: _fixtures/, _expected/, _requests/, _seeds/), never a domain. Ground that is CODE keeps its own unit test: `<module>.test.ts` NEXT to the `<module>.ts` it tests (I2), and nothing else.",
|
|
3269
3277
|
specNotAtFacetRoot: "A `*.specification.ts` must sit at the facet root: `specs/<facet>/<name>.specification.ts`.",
|
|
3270
|
-
testAtFacetRoot: "A
|
|
3271
|
-
testAtSpecsRoot: "A
|
|
3272
|
-
testNotMirroringDirectory: "A
|
|
3273
|
-
testOutsideFacet: "A
|
|
3274
|
-
testTooDeep: "A
|
|
3275
|
-
testTooDeepForFacet: "A
|
|
3278
|
+
testAtFacetRoot: "A spec must live in a domain folder: `specs/<facet>/<domain>/<aspect>.spec.ts` — tests directly at the facet root are forbidden.",
|
|
3279
|
+
testAtSpecsRoot: "A spec must live in a directory under specs/ — with `depth: \"mirror\"` the tree mirrors a structure, and the specs root mirrors nothing.",
|
|
3280
|
+
testNotMirroringDirectory: "A spec must be named after the directory holding it — `{{directory}}/{{directory}}.spec.ts` — with `depth: \"mirror\"`.",
|
|
3281
|
+
testOutsideFacet: "A spec must live in a facet folder — `specs/<facet>/<aspect>.spec.ts`, or one domain deeper: with `depth: \"facet\"` the specs root holds facets, not tests.",
|
|
3282
|
+
testTooDeep: "A spec must sit at facet/domain depth: `specs/<facet>/<domain>/<aspect>.spec.ts` — no deeper nesting.",
|
|
3283
|
+
testTooDeepForFacet: "A spec must sit at the facet root or one domain folder down — `specs/<facet>/<aspect>.spec.ts` or `specs/<facet>/<domain>/<aspect>.spec.ts`: with `depth: \"facet\"` nothing nests deeper."
|
|
3276
3284
|
},
|
|
3277
3285
|
schema: [{
|
|
3278
3286
|
additionalProperties: false,
|
|
@@ -4011,11 +4019,22 @@ const d6wTransformTokenEquivalent = {
|
|
|
4011
4019
|
//#region src/lint/rules/model/d8w-text-bypass.ts
|
|
4012
4020
|
/** Text-oriented matchers that tempt a raw `.text` bypass (D8). */
|
|
4013
4021
|
const TEXT_MATCHERS = /* @__PURE__ */ new Set(["toContain", "toMatch"]);
|
|
4022
|
+
/** A regex literal, or `new RegExp(…)` — what an accessor subject refuses. */
|
|
4023
|
+
function isRegExp(node) {
|
|
4024
|
+
if (node === void 0) return false;
|
|
4025
|
+
if (node.type === "Literal") return "regex" in node;
|
|
4026
|
+
return node.type === "NewExpression" && identifierName(child(node, "callee")) === "RegExp";
|
|
4027
|
+
}
|
|
4014
4028
|
/**
|
|
4015
4029
|
* CONVENTIONS D8 (warning) — `.text` is the raw stream escape hatch. Asserting
|
|
4016
4030
|
* `expect(result.text).toContain(…)` throws away the typed subject (its token
|
|
4017
4031
|
* grammar, its `toMatch('file.txt')` fixture resolution) for a substring check.
|
|
4018
4032
|
* Prefer asserting on the accessor subject itself.
|
|
4033
|
+
*
|
|
4034
|
+
* A RegExp argument is exempt, because it is the one thing the typed subject
|
|
4035
|
+
* cannot take: `toMatch` on an accessor resolves a FIXTURE NAME and throws on a
|
|
4036
|
+
* regex, and chapter 14 names `expect(x.text).toMatch(/re/)` as the way through.
|
|
4037
|
+
* The rule warned on exactly the spelling its own manual prescribes.
|
|
4019
4038
|
*/
|
|
4020
4039
|
const d8wTextBypass = {
|
|
4021
4040
|
create(context) {
|
|
@@ -4027,6 +4046,7 @@ const d8wTextBypass = {
|
|
|
4027
4046
|
if (expectCall?.type !== "CallExpression") return;
|
|
4028
4047
|
const expectCallee = expectCall.callee;
|
|
4029
4048
|
if (expectCallee?.type !== "Identifier" || expectCallee.name !== "expect") return;
|
|
4049
|
+
if (isRegExp(childList(node, "arguments")[0])) return;
|
|
4030
4050
|
const subject = expectCall.arguments?.[0];
|
|
4031
4051
|
if (subject !== void 0 && memberPropertyName(subject) === "text") context.report({
|
|
4032
4052
|
data: { matcher },
|
|
@@ -4408,7 +4428,7 @@ const d16wAmbientValue = {
|
|
|
4408
4428
|
//#endregion
|
|
4409
4429
|
//#region src/lint/rules/model/d17w-double-only-oracle.ts
|
|
4410
4430
|
/** The three ways this vocabulary makes a double. */
|
|
4411
|
-
const DOUBLE_FACTORIES = /* @__PURE__ */ new Set([
|
|
4431
|
+
const DOUBLE_FACTORIES$1 = /* @__PURE__ */ new Set([
|
|
4412
4432
|
"mockOf",
|
|
4413
4433
|
"vi.fn",
|
|
4414
4434
|
"vi.spyOn"
|
|
@@ -4423,7 +4443,7 @@ function makesADouble(init) {
|
|
|
4423
4443
|
if (init.type === "AwaitExpression") return makesADouble(child(init, "argument"));
|
|
4424
4444
|
if (init.type !== "CallExpression") return false;
|
|
4425
4445
|
const path = memberPath(child(init, "callee"));
|
|
4426
|
-
return path !== void 0 && DOUBLE_FACTORIES.has(path);
|
|
4446
|
+
return path !== void 0 && DOUBLE_FACTORIES$1.has(path);
|
|
4427
4447
|
}
|
|
4428
4448
|
/** Every binding in the file whose value is a double this test built. */
|
|
4429
4449
|
function doubleBindings(root) {
|
|
@@ -5494,6 +5514,21 @@ function isModuleDouble(node) {
|
|
|
5494
5514
|
const property = child(callee, "property");
|
|
5495
5515
|
return identifierName(object) === "vi" && (identifierName(property) === "mock" || identifierName(property) === "doMock");
|
|
5496
5516
|
}
|
|
5517
|
+
/**
|
|
5518
|
+
* The specifier a module double names, in either spelling.
|
|
5519
|
+
*
|
|
5520
|
+
* `vi.mock('./x.js', …)` states it as a string; `vi.mock(import('./x.js'), …)`
|
|
5521
|
+
* states it as a dynamic import, which is the form `vitest/prefer-import-in-mock`
|
|
5522
|
+
* requires. Reading only the string left an allow-listed native module with no
|
|
5523
|
+
* green spelling: satisfy this rule and the upstream one fires, satisfy that one
|
|
5524
|
+
* and this fires.
|
|
5525
|
+
*/
|
|
5526
|
+
function mockedSpecifier(argument) {
|
|
5527
|
+
const literal = stringValue(argument);
|
|
5528
|
+
if (literal !== void 0) return literal;
|
|
5529
|
+
if (argument?.type !== "ImportExpression") return;
|
|
5530
|
+
return stringValue(child(argument, "source"));
|
|
5531
|
+
}
|
|
5497
5532
|
/** A bundler suffix (`./payload.json?raw`, `./doc.md#frag`) is not part of the extension. */
|
|
5498
5533
|
function withoutSuffix(source) {
|
|
5499
5534
|
return source.replace(/[?#].*$/u, "");
|
|
@@ -5536,7 +5571,7 @@ const i4NoModuleDoubles = {
|
|
|
5536
5571
|
const visitor = {
|
|
5537
5572
|
CallExpression(node) {
|
|
5538
5573
|
if (!isModuleDouble(node)) return;
|
|
5539
|
-
const named =
|
|
5574
|
+
const named = mockedSpecifier(childList(node, "arguments")[0]);
|
|
5540
5575
|
if (named === void 0 || !allowed.has(named)) context.report({
|
|
5541
5576
|
messageId: "viMock",
|
|
5542
5577
|
node
|
|
@@ -5579,6 +5614,26 @@ const i4NoModuleDoubles = {
|
|
|
5579
5614
|
};
|
|
5580
5615
|
//#endregion
|
|
5581
5616
|
//#region src/lint/rules/model/j2-no-sleep.ts
|
|
5617
|
+
/** The doubles whose implementation STAGES the world rather than waits on it. */
|
|
5618
|
+
const DOUBLE_FACTORIES = /* @__PURE__ */ new Set([
|
|
5619
|
+
"fn",
|
|
5620
|
+
"mockImplementation",
|
|
5621
|
+
"mockImplementationOnce",
|
|
5622
|
+
"mockOf"
|
|
5623
|
+
]);
|
|
5624
|
+
/**
|
|
5625
|
+
* Is this call a test double taking an implementation — `vi.fn(…)`,
|
|
5626
|
+
* `x.mockImplementation(…)`, `mockOf<Port>({ … })`?
|
|
5627
|
+
*
|
|
5628
|
+
* The member forms are read by their PROPERTY alone, because the object is
|
|
5629
|
+
* whatever the test called its double.
|
|
5630
|
+
*/
|
|
5631
|
+
function isDoubleImplementation(node) {
|
|
5632
|
+
const callee = child(node, "callee");
|
|
5633
|
+
if (callee === void 0) return false;
|
|
5634
|
+
const name = callee.type === "Identifier" ? identifierName(callee) : memberPropertyName(callee);
|
|
5635
|
+
return name !== void 0 && DOUBLE_FACTORIES.has(name);
|
|
5636
|
+
}
|
|
5582
5637
|
/**
|
|
5583
5638
|
* CONVENTIONS J2 — a test contains no arbitrary sleep, wherever it sits:
|
|
5584
5639
|
* synchronisation is `see()`/`gone()` inside a scenario and `waitUntil()`
|
|
@@ -5588,15 +5643,33 @@ const i4NoModuleDoubles = {
|
|
|
5588
5643
|
* The reach is every test file — a sleep in a module test beside `src/` waits
|
|
5589
5644
|
* exactly as blindly as one under `specs/`, and went unseen while the rule
|
|
5590
5645
|
* gated on the folder.
|
|
5646
|
+
*
|
|
5647
|
+
* What it does NOT reach is a timer inside a test DOUBLE's implementation. A
|
|
5648
|
+
* `vi.fn(() => setTimeout(…))` is the world being staged — a clone that settles
|
|
5649
|
+
* late, a handler that answers out of order — and the assertion that follows is
|
|
5650
|
+
* about the ORDER results come back in, which no predicate can state: a
|
|
5651
|
+
* `waitUntil` waits for something to become true, while what this test needs is
|
|
5652
|
+
* for something to happen late. The test is not the one waiting, so the rule is
|
|
5653
|
+
* not about it.
|
|
5591
5654
|
*/
|
|
5592
5655
|
const j2NoSleep = {
|
|
5593
5656
|
create(context) {
|
|
5594
5657
|
if (!isTestRole(roleOf(context.filename).role)) return {};
|
|
5658
|
+
const doubles = [];
|
|
5659
|
+
const insideADouble = (node) => doubles.some((span) => nodeStart(node) >= span.start && nodeEnd(node) <= span.end);
|
|
5595
5660
|
return {
|
|
5596
5661
|
CallExpression(node) {
|
|
5597
|
-
|
|
5662
|
+
if (isDoubleImplementation(node)) {
|
|
5663
|
+
doubles.push({
|
|
5664
|
+
end: nodeEnd(node),
|
|
5665
|
+
start: nodeStart(node)
|
|
5666
|
+
});
|
|
5667
|
+
return;
|
|
5668
|
+
}
|
|
5669
|
+
if (insideADouble(node)) return;
|
|
5670
|
+
const callee = child(node, "callee");
|
|
5598
5671
|
if (callee === void 0) return;
|
|
5599
|
-
const name = callee.type === "Identifier" ? callee
|
|
5672
|
+
const name = callee.type === "Identifier" ? identifierName(callee) : memberPropertyName(callee);
|
|
5600
5673
|
if (name === "setTimeout" || name === "setInterval") {
|
|
5601
5674
|
context.report({
|
|
5602
5675
|
messageId: "sleep",
|
|
@@ -5604,7 +5677,7 @@ const j2NoSleep = {
|
|
|
5604
5677
|
});
|
|
5605
5678
|
return;
|
|
5606
5679
|
}
|
|
5607
|
-
if (name === "wait" && callee.type === "MemberExpression" && callee
|
|
5680
|
+
if (name === "wait" && callee.type === "MemberExpression" && identifierName(child(callee, "object")) === "Atomics") context.report({
|
|
5608
5681
|
messageId: "sleep",
|
|
5609
5682
|
node
|
|
5610
5683
|
});
|
|
@@ -1,49 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { a as formatElement, i as describeAmbiguity, n as widenedForWindow, r as AmbiguousElementError } 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/substring-warning.ts
|
|
7
|
-
/**
|
|
8
|
-
* The transitional warning for a name that matches only as a SUBSTRING.
|
|
9
|
-
*
|
|
10
|
-
* Until 16.0 a descriptor's name matched any element whose accessible name
|
|
11
|
-
* CONTAINED it, so `link('Articles')` also designated "Read Articles" — and a
|
|
12
|
-
* test could pass for years against the wrong element without anyone noticing,
|
|
13
|
-
* because the wrong element was usually next to the right one. Exact is the
|
|
14
|
-
* default now.
|
|
15
|
-
*
|
|
16
|
-
* A lint rule cannot carry this: whether a name matched whole or in part is a
|
|
17
|
-
* fact about a RUN, and a verdict fed by a previous run's artefact is not
|
|
18
|
-
* deterministic. So it is a runtime warning, printed by the adapter that did
|
|
19
|
-
* the looking, at the moment it has both answers in hand — nothing found under
|
|
20
|
-
* exact, something found under substring.
|
|
21
|
-
*
|
|
22
|
-
* It is printed ONCE per descriptor per process: a scenario that clicks the
|
|
23
|
-
* same link in ten tests has one thing to fix, not ten lines to read.
|
|
24
|
-
*/
|
|
25
|
-
/** How long the two-release window lasts, stated in the message itself. */
|
|
26
|
-
const WINDOW = "16.0 and 16.1";
|
|
27
|
-
/** Descriptors already reported in this process. */
|
|
28
|
-
const reported = /* @__PURE__ */ new Set();
|
|
29
|
-
/** The identity a descriptor is deduplicated by — kind, name, and its scope chain. */
|
|
30
|
-
function identityOf(element) {
|
|
31
|
-
const own = `${element.kind}:${element.name ?? ""}`;
|
|
32
|
-
return element.scope === void 0 ? own : `${identityOf(element.scope)}>${own}`;
|
|
33
|
-
}
|
|
34
|
-
/**
|
|
35
|
-
* Report that `element` designated nothing as a whole name, but would have
|
|
36
|
-
* designated something as a substring. Returns whether a line was printed,
|
|
37
|
-
* which is what the adapters' own tests assert on.
|
|
38
|
-
*/
|
|
39
|
-
function warnSubstringOnly(element, where) {
|
|
40
|
-
const identity = identityOf(element);
|
|
41
|
-
if (reported.has(identity)) return false;
|
|
42
|
-
reported.add(identity);
|
|
43
|
-
console.warn(`@jterrazz/test: ${element.kind}('${element.name ?? ""}') matched nothing as a whole accessible name, and matched only as a SUBSTRING of a longer one (${where}). Names are exact from 16.0: write the name in full, scope it with \`within(…)\`, or state \`{ exact: false }\` to keep the old behaviour. This warning ships for ${WINDOW}; after that the substring match is gone and this is simply not found.`);
|
|
44
|
-
return true;
|
|
45
|
-
}
|
|
46
|
-
//#endregion
|
|
47
6
|
//#region src/seams/playwright/playwright.adapter.ts
|
|
48
7
|
/** How many candidates the ambiguity error enumerates before truncating. */
|
|
49
8
|
const MAX_REPORTED_MATCHES = 10;
|
|
@@ -130,14 +89,12 @@ async function captureMatches(locator) {
|
|
|
130
89
|
* would send the author to fix the wrong descriptor.
|
|
131
90
|
*/
|
|
132
91
|
async function act(page, element, action) {
|
|
92
|
+
const target = await widenedForWindow(element, page.url(), probeOf(page)) ?? element;
|
|
133
93
|
try {
|
|
134
|
-
return await action(locate(page,
|
|
94
|
+
return await action(locate(page, target));
|
|
135
95
|
} catch (error) {
|
|
136
|
-
if (!isStrictViolation(error))
|
|
137
|
-
|
|
138
|
-
throw error;
|
|
139
|
-
}
|
|
140
|
-
const culprit = await findAmbiguousLevel(page, element);
|
|
96
|
+
if (!isStrictViolation(error)) throw error;
|
|
97
|
+
const culprit = await findAmbiguousLevel(page, target);
|
|
141
98
|
const matches = await captureMatches(locate(page, culprit));
|
|
142
99
|
throw new AmbiguousElementError(describeAmbiguity({
|
|
143
100
|
element: culprit,
|
|
@@ -146,23 +103,15 @@ async function act(page, element, action) {
|
|
|
146
103
|
}));
|
|
147
104
|
}
|
|
148
105
|
}
|
|
149
|
-
/**
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
if (element.exact !== void 0 || element.name === void 0) return;
|
|
159
|
-
try {
|
|
160
|
-
if (await locate(page, element).count() > 0) return;
|
|
161
|
-
if (await locate(page, {
|
|
162
|
-
...element,
|
|
163
|
-
exact: false
|
|
164
|
-
}).count() > 0) warnSubstringOnly(element, page.url());
|
|
165
|
-
} catch {}
|
|
106
|
+
/** What the transitional window may ask of the page. */
|
|
107
|
+
function probeOf(page) {
|
|
108
|
+
return {
|
|
109
|
+
count: async (element) => await locate(page, element).count(),
|
|
110
|
+
nameOf: async (element) => {
|
|
111
|
+
const [match] = await captureMatches(locate(page, element));
|
|
112
|
+
return match?.accessibleName ?? match?.text;
|
|
113
|
+
}
|
|
114
|
+
};
|
|
166
115
|
}
|
|
167
116
|
/** Walk the scope chain outside-in; the outermost ambiguous level is the fault. */
|
|
168
117
|
async function findAmbiguousLevel(page, element) {
|
|
@@ -143,4 +143,102 @@ function describeAmbiguity(options) {
|
|
|
143
143
|
].join("\n");
|
|
144
144
|
}
|
|
145
145
|
//#endregion
|
|
146
|
-
|
|
146
|
+
//#region src/model/elements/substring-warning.ts
|
|
147
|
+
/**
|
|
148
|
+
* The transitional warning for a name that matches only as a SUBSTRING.
|
|
149
|
+
*
|
|
150
|
+
* Until 16.0 a descriptor's name matched any element whose accessible name
|
|
151
|
+
* CONTAINED it, so `link('Articles')` also designated "Read Articles" — and a
|
|
152
|
+
* test could pass for years against the wrong element without anyone noticing,
|
|
153
|
+
* because the wrong element was usually next to the right one. Exact is the
|
|
154
|
+
* default now.
|
|
155
|
+
*
|
|
156
|
+
* A lint rule cannot carry this: whether a name matched whole or in part is a
|
|
157
|
+
* fact about a RUN, and a verdict fed by a previous run's artefact is not
|
|
158
|
+
* deterministic. So it is a runtime warning, printed by the adapter that did
|
|
159
|
+
* the looking, at the moment it has both answers in hand — nothing found under
|
|
160
|
+
* exact, something found under substring.
|
|
161
|
+
*
|
|
162
|
+
* It is printed ONCE per descriptor per process: a scenario that clicks the
|
|
163
|
+
* same link in ten tests has one thing to fix, not ten lines to read.
|
|
164
|
+
*/
|
|
165
|
+
/** How long the window lasts, stated in the message itself. */
|
|
166
|
+
const WINDOW = "through 16.x";
|
|
167
|
+
/** Descriptors already reported in this process. */
|
|
168
|
+
const reported = /* @__PURE__ */ new Set();
|
|
169
|
+
/** The identity a descriptor is deduplicated by — kind, name, and its scope chain. */
|
|
170
|
+
function identityOf(element) {
|
|
171
|
+
const own = `${element.kind}:${element.name ?? ""}`;
|
|
172
|
+
return element.scope === void 0 ? own : `${identityOf(element.scope)}>${own}`;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Report that `element` designated nothing as a whole name, but would have
|
|
176
|
+
* designated something as a substring. Returns whether a line was printed,
|
|
177
|
+
* which is what the adapters' own tests assert on.
|
|
178
|
+
*/
|
|
179
|
+
function warnSubstringOnly(element, where, found) {
|
|
180
|
+
const identity = identityOf(element);
|
|
181
|
+
if (reported.has(identity)) return false;
|
|
182
|
+
reported.add(identity);
|
|
183
|
+
const spelling = found === void 0 ? "" : ` The name to write is '${found.replaceAll(/\s+/gu, " ").trim()}'.`;
|
|
184
|
+
console.warn(`@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
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* The descriptor to retry with, LEVEL by level, or `undefined` when nothing
|
|
189
|
+
* would change.
|
|
190
|
+
*
|
|
191
|
+
* Every level of the chain is asked the same question the target is asked —
|
|
192
|
+
* `within(button('Proof of authorship'), content('Verified'))` against a
|
|
193
|
+
* button named "Proof of authorship Verified" failed on the SCOPE, and a
|
|
194
|
+
* warning that only ever widened the target printed nothing at all. The chain
|
|
195
|
+
* is walked outside-in and rebuilt as it goes, since an inner level's answer
|
|
196
|
+
* is meaningless under a scope that designates nothing.
|
|
197
|
+
*
|
|
198
|
+
* A level that states an `exact` of its own is left alone: an author who wrote
|
|
199
|
+
* `{ exact: false }` chose the substring, and one who wrote `{ exact: true }`
|
|
200
|
+
* was already exact.
|
|
201
|
+
*
|
|
202
|
+
* Asked BEFORE the action, not after it fails: a verb waits its whole
|
|
203
|
+
* actionability budget, which is the test's own budget, so a retry afterwards
|
|
204
|
+
* never runs — the test is already over. The probes are counts, which wait for
|
|
205
|
+
* nothing, and the common case (the name matches whole) costs one per level.
|
|
206
|
+
*/
|
|
207
|
+
async function widenedForWindow(element, where, probe) {
|
|
208
|
+
const chain = [];
|
|
209
|
+
for (let level = element; level; level = level.scope) chain.unshift(level);
|
|
210
|
+
let rebuilt;
|
|
211
|
+
let widened = false;
|
|
212
|
+
for (const level of chain) {
|
|
213
|
+
const candidate = {
|
|
214
|
+
...level,
|
|
215
|
+
...rebuilt === void 0 ? {} : { scope: rebuilt }
|
|
216
|
+
};
|
|
217
|
+
const loose = await widenedLevel(candidate, where, probe);
|
|
218
|
+
widened ||= loose !== void 0;
|
|
219
|
+
rebuilt = loose ?? candidate;
|
|
220
|
+
}
|
|
221
|
+
return widened ? rebuilt : void 0;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* The one level, widened and warned about, or `undefined` when it designates
|
|
225
|
+
* something already, states its own `exact`, carries no name, or would
|
|
226
|
+
* designate nothing either way.
|
|
227
|
+
*/
|
|
228
|
+
async function widenedLevel(candidate, where, probe) {
|
|
229
|
+
if (candidate.exact !== void 0 || candidate.name === void 0) return;
|
|
230
|
+
try {
|
|
231
|
+
if (await probe.count(candidate) > 0) return;
|
|
232
|
+
const loose = {
|
|
233
|
+
...candidate,
|
|
234
|
+
exact: false
|
|
235
|
+
};
|
|
236
|
+
if (await probe.count(loose) !== 1) return;
|
|
237
|
+
warnSubstringOnly(candidate, where, await probe.nameOf(loose));
|
|
238
|
+
return loose;
|
|
239
|
+
} catch {
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
//#endregion
|
|
244
|
+
export { formatElement as a, describeAmbiguity as i, widenedForWindow as n, AmbiguousElementError as r, warnSubstringOnly as t };
|
package/dist/vitest.js
CHANGED
|
@@ -214,6 +214,60 @@ function rootDefaults() {
|
|
|
214
214
|
}
|
|
215
215
|
};
|
|
216
216
|
}
|
|
217
|
+
/** A glob list with its repeats taken out, or nothing where there is no list. */
|
|
218
|
+
function eachOnce(globs) {
|
|
219
|
+
return globs === void 0 ? void 0 : [...new Set(globs)];
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* The glob lists vite CONCATENATES rather than overrides, each glob kept once.
|
|
223
|
+
*
|
|
224
|
+
* A project built ON the defaults and then merged INTO them by
|
|
225
|
+
* `defineSpecConfig` carried every default twice. Glob matching is idempotent,
|
|
226
|
+
* so nothing collected differently — but the no-tests banner is the one place a
|
|
227
|
+
* consumer ever reads that list, and it read as three merges of the same three
|
|
228
|
+
* globs.
|
|
229
|
+
*/
|
|
230
|
+
function statedOnce(test) {
|
|
231
|
+
const exclude = eachOnce(test.exclude);
|
|
232
|
+
const include = eachOnce(test.include);
|
|
233
|
+
return {
|
|
234
|
+
...test,
|
|
235
|
+
...exclude === void 0 ? {} : { exclude },
|
|
236
|
+
...include === void 0 ? {} : { include }
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Build a project ON the defaults — the ONE way a project is built here.
|
|
241
|
+
*
|
|
242
|
+
* @internal
|
|
243
|
+
*/
|
|
244
|
+
function onProjectDefaults(stated) {
|
|
245
|
+
const merged = mergeConfig(projectDefaults(), stated);
|
|
246
|
+
return merged.test === void 0 ? merged : {
|
|
247
|
+
...merged,
|
|
248
|
+
test: statedOnce(merged.test)
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Is every project an inline object — one this preset merged its defaults into?
|
|
253
|
+
* A project named by a glob or handed as a promise carries a config this call
|
|
254
|
+
* never saw, and the root's exclusions are the only ones it gets from here.
|
|
255
|
+
*/
|
|
256
|
+
function allInline(projects) {
|
|
257
|
+
return projects.every((project) => typeof project === "object" && project !== null && !("then" in project));
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* The root's exclusions, beside projects that already carry them.
|
|
261
|
+
*
|
|
262
|
+
* With `projects`, the root collects nothing itself: its `exclude` only travels
|
|
263
|
+
* INTO each project, where vitest CONCATENATES it with the project's own — so
|
|
264
|
+
* every preset glob reached the banner twice. The preset's own are dropped
|
|
265
|
+
* here, since each inline project states them in full; what the CONSUMER stated
|
|
266
|
+
* at the root stays, because no project holds a copy of that.
|
|
267
|
+
*/
|
|
268
|
+
function rootExcludeBeside(projects, exclude) {
|
|
269
|
+
return allInline(projects) ? exclude.filter((glob) => !EXCLUDE.includes(glob)) : exclude;
|
|
270
|
+
}
|
|
217
271
|
/**
|
|
218
272
|
* A project inherits NOTHING from the root `test` block — vitest resolves each
|
|
219
273
|
* project as its own config — so the defaults are merged into every inline one.
|
|
@@ -222,19 +276,26 @@ function rootDefaults() {
|
|
|
222
276
|
*/
|
|
223
277
|
function withProjectDefaults(project) {
|
|
224
278
|
if (typeof project !== "object" || project === null || "then" in project) return project;
|
|
225
|
-
return
|
|
279
|
+
return onProjectDefaults(project);
|
|
226
280
|
}
|
|
227
281
|
function defineSpecConfig(config = {}) {
|
|
228
282
|
const { literate: literateOptions, ...userConfig } = config;
|
|
229
283
|
const stated = literateOptions ? mergeConfig({ plugins: [literate(literateOptions)] }, userConfig) : userConfig;
|
|
230
284
|
const merged = mergeConfig(rootDefaults(), stated);
|
|
231
|
-
const
|
|
232
|
-
if (
|
|
285
|
+
const root = merged.test === void 0 ? void 0 : statedOnce(merged.test);
|
|
286
|
+
if (root === void 0) return merged;
|
|
287
|
+
const { projects } = root;
|
|
288
|
+
if (projects === void 0) return {
|
|
289
|
+
...merged,
|
|
290
|
+
test: root
|
|
291
|
+
};
|
|
292
|
+
const inline = projects.map(withProjectDefaults);
|
|
233
293
|
return {
|
|
234
294
|
...merged,
|
|
235
295
|
test: {
|
|
236
|
-
...
|
|
237
|
-
|
|
296
|
+
...root,
|
|
297
|
+
...root.exclude === void 0 ? {} : { exclude: rootExcludeBeside(inline, root.exclude) },
|
|
298
|
+
projects: inline
|
|
238
299
|
}
|
|
239
300
|
};
|
|
240
301
|
}
|
|
@@ -242,7 +303,7 @@ function defineSpecConfig(config = {}) {
|
|
|
242
303
|
//#region src/runner/facet-project.ts
|
|
243
304
|
/** The globs a node facet collects, and the ones a caller stated instead. */
|
|
244
305
|
function facetProject(facet, options) {
|
|
245
|
-
return
|
|
306
|
+
return onProjectDefaults({ test: {
|
|
246
307
|
...options.exclude === void 0 ? {} : { exclude: options.exclude },
|
|
247
308
|
include: options.include ?? [`specs/${facet}/**/*.spec.ts`],
|
|
248
309
|
...options.serial === true ? { fileParallelism: false } : {},
|
|
@@ -578,7 +639,7 @@ async function component(options = {}) {
|
|
|
578
639
|
const { playwright } = await import("@vitest/browser-playwright");
|
|
579
640
|
const pipeline = options.vite === void 0 ? {} : await reroot(options.vite, root);
|
|
580
641
|
const consumerPlugins = Array.isArray(pipeline.plugins) ? pipeline.plugins : [];
|
|
581
|
-
|
|
642
|
+
return onProjectDefaults({
|
|
582
643
|
...pipeline,
|
|
583
644
|
...jsxPipeline(pipeline),
|
|
584
645
|
optimizeDeps: { include: SEAM_DEPENDENCIES.filter(resolves) },
|
|
@@ -605,8 +666,7 @@ async function component(options = {}) {
|
|
|
605
666
|
sequence: { groupOrder: 2 },
|
|
606
667
|
...options.timeout === void 0 ? {} : { testTimeout: options.timeout }
|
|
607
668
|
}
|
|
608
|
-
};
|
|
609
|
-
return mergeConfig(projectDefaults(), project);
|
|
669
|
+
});
|
|
610
670
|
}
|
|
611
671
|
//#endregion
|
|
612
672
|
//#region src/facets/integration/integration.project.ts
|
|
@@ -638,7 +698,7 @@ function mobile(options = {}) {
|
|
|
638
698
|
* a 2-vCPU runner.
|
|
639
699
|
*/
|
|
640
700
|
function website(options = {}) {
|
|
641
|
-
return
|
|
701
|
+
return onProjectDefaults({ test: {
|
|
642
702
|
...options.exclude === void 0 ? {} : { exclude: options.exclude },
|
|
643
703
|
...options.serial === true ? { fileParallelism: false } : {},
|
|
644
704
|
include: options.include ?? ["specs/website/**/*.spec.ts"],
|
|
@@ -660,7 +720,7 @@ function website(options = {}) {
|
|
|
660
720
|
*/
|
|
661
721
|
function unit(options = {}) {
|
|
662
722
|
const fromRoots = options.roots?.map((root) => `${root.replace(/\/+$/u, "")}/**/*.test.ts`);
|
|
663
|
-
return
|
|
723
|
+
return onProjectDefaults({ test: {
|
|
664
724
|
exclude: options.exclude ?? ["specs/**", "**/*.test.tsx"],
|
|
665
725
|
include: options.include ?? fromRoots ?? ["**/*.test.ts"],
|
|
666
726
|
...options.serial === true ? { fileParallelism: false } : {},
|