@jterrazz/test 16.1.1 → 17.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -171
- package/dist/browser/index.js +2 -2980
- package/dist/browser/services.js +2 -0
- package/dist/browser/stubs.js +2 -0
- package/dist/checker.d.ts +6 -1
- package/dist/checker.js +4534 -2628
- package/dist/config.d.ts +40 -0
- package/dist/config.js +309 -0
- package/dist/in-place.global-setup.d.ts +11 -0
- package/dist/in-place.global-setup.js +21 -0
- package/dist/index.d.ts +1653 -3004
- package/dist/index.js +5226 -3845
- package/dist/mobile-session.global-setup.d.ts +14 -0
- package/dist/mobile-session.global-setup.js +31 -0
- package/dist/oxlint.cjs +6069 -5634
- package/dist/oxlint.d.cts +43 -7212
- package/dist/oxlint.d.ts +43 -7212
- package/dist/oxlint.js +6127 -5693
- package/dist/route-coverage.global-setup.d.ts +42 -0
- package/dist/route-coverage.global-setup.js +2 -0
- package/dist/services.d.ts +3 -0
- package/dist/services.global-setup.d.ts +43 -0
- package/dist/services.global-setup.js +2 -0
- package/dist/services.js +597 -0
- package/dist/shared-appium.adapter.js +428 -0
- package/dist/shared-artifacts.js +32 -0
- package/dist/shared-chapter.js +7 -0
- package/dist/shared-container.port.d.ts +31 -0
- package/dist/shared-derived-projects.js +58 -0
- package/dist/shared-device.port.d.ts +189 -0
- package/dist/shared-exchange.js +447 -0
- package/dist/shared-in-place.js +172 -0
- package/dist/shared-index.browser.js +3191 -0
- package/dist/shared-index.services.d.ts +346 -0
- package/dist/shared-index.stubs.d.ts +287 -0
- package/dist/shared-json-shape.js +17 -0
- package/dist/shared-landmarks.js +71 -0
- package/dist/shared-ledger.js +68 -0
- package/dist/shared-literate-plugin.js +183 -0
- package/dist/shared-manifest.js +1321 -0
- package/dist/shared-pause.js +11 -0
- package/dist/shared-peer.js +88 -0
- package/dist/shared-playwright.adapter.js +330 -0
- package/dist/shared-process.adapter.js +74 -0
- package/dist/shared-refusal.js +35 -0
- package/dist/shared-resolve.js +24 -0
- package/dist/shared-route-coverage.global-setup.js +187 -0
- package/dist/shared-scenario-failure.js +198 -0
- package/dist/shared-serve.js +365 -0
- package/dist/shared-service-images.js +10 -0
- package/dist/shared-services.global-setup.js +179 -0
- package/dist/shared-session-ledger.js +301 -0
- package/dist/shared-setup-entry.js +12 -0
- package/dist/shared-source-scan.js +133 -0
- package/dist/shared-spec-document.js +365 -0
- package/dist/shared-specifications.js +26 -0
- package/dist/shared-stream-body.js +130 -0
- package/dist/shared-stub-server.js +190 -0
- package/dist/shared-temp-root.global-setup.js +80 -0
- package/dist/shared-test-baseline.js +26 -0
- package/dist/shared-test-section.js +58 -0
- package/dist/shared-testcontainers.adapter.js +203 -0
- package/dist/shared-tokens.js +55 -0
- package/dist/shared-yaml-document.js +119 -0
- package/dist/stubs.d.ts +2 -0
- package/dist/stubs.js +657 -0
- package/dist/temp-root.global-setup.d.ts +7 -0
- package/dist/temp-root.global-setup.js +2 -0
- package/dist/vitest.d.ts +97 -163
- package/dist/vitest.js +373 -525
- package/docs/README.md +82 -36
- package/docs/concepts/architecture.md +118 -0
- package/docs/concepts/conventions.md +333 -0
- package/docs/guides/api-specs.md +374 -0
- package/docs/guides/cli-specs.md +564 -0
- package/docs/guides/component-specs.md +252 -0
- package/docs/guides/develop.md +94 -0
- package/docs/guides/integration-specs.md +181 -0
- package/docs/guides/job-specs.md +180 -0
- package/docs/guides/lint-specs.md +174 -0
- package/docs/guides/mobile-specs.md +319 -0
- package/docs/guides/module-tests.md +149 -0
- package/docs/guides/release.md +43 -0
- package/docs/guides/spec-documents.md +189 -0
- package/docs/guides/website-specs.md +413 -0
- package/docs/reference/assertions.md +345 -0
- package/docs/reference/glossary.md +20 -0
- package/docs/reference/linting.md +483 -0
- package/docs/reference/services.md +236 -0
- package/docs/reference/stubs.md +498 -0
- package/docs/reference/time-and-doubles.md +137 -0
- package/docs/reference/tokens.md +258 -0
- package/docs/reference/visitor.md +221 -0
- package/docs/start/getting-started.md +187 -0
- package/docs/start/set-up-a-project.md +214 -0
- package/package.json +50 -30
- package/schema/spec.schema.json +43 -17
- package/dist/appium.adapter.js +0 -489
- package/dist/artifacts.js +0 -32
- package/dist/fixtures.js +0 -110
- package/dist/literate.js +0 -1747
- package/dist/match.js +0 -804
- package/dist/playwright.adapter.js +0 -438
- package/dist/server.js +0 -160
- package/dist/stream.js +0 -321
- package/dist/substring-warning.js +0 -282
- package/docs/01-architecture.md +0 -95
- package/docs/02-developing.md +0 -351
- package/docs/03-testing.md +0 -495
- package/docs/04-operating.md +0 -101
- package/docs/05-module-tests.md +0 -152
- package/docs/06-integration.md +0 -180
- package/docs/07-component.md +0 -246
- package/docs/08-website.md +0 -404
- package/docs/09-mobile.md +0 -270
- package/docs/10-api.md +0 -264
- package/docs/11-jobs.md +0 -157
- package/docs/12-cli.md +0 -677
- package/docs/13-elements.md +0 -248
- package/docs/14-assertions.md +0 -314
- package/docs/15-tokens.md +0 -253
- package/docs/16-contracts.md +0 -438
- package/docs/17-services.md +0 -188
- package/docs/18-conventions.md +0 -332
- package/docs/19-linting.md +0 -512
- package/docs/decisions/001-v9-conventions-on-three-channels.md +0 -50
- package/docs/decisions/002-the-statique-channel-holds-only-what-upstream-does-not.md +0 -49
- package/docs/decisions/003-a-rendered-component-is-a-facet-on-vitest-browser-mode.md +0 -84
- package/docs/decisions/004-a-module-against-real-services-or-a-golden-is-the-integration-facet.md +0 -81
- package/docs/decisions/005-the-doubles-ladder-is-closed.md +0 -86
- package/docs/decisions/006-the-catalogue-reaches-a-file-by-its-role.md +0 -245
- package/docs/decisions/007-compose-mode-leaves-the-package.md +0 -53
- package/docs/decisions/008-vitest-5-node-24-and-the-native-seams-as-optional-peers.md +0 -75
- package/docs/decisions/_template.md +0 -22
- package/docs/reference/classes/Matcher.md +0 -120
- package/docs/reference/classes/ProcessHandle.md +0 -368
- package/docs/reference/functions/defineContract.md +0 -39
- package/docs/reference/functions/defineContracts.md +0 -36
- package/docs/reference/functions/disabled.md +0 -28
- package/docs/reference/functions/enabled.md +0 -20
- package/docs/reference/functions/focused.md +0 -26
- package/docs/reference/functions/mockOf.md +0 -79
- package/docs/reference/functions/postgres.md +0 -31
- package/docs/reference/functions/process.md +0 -33
- package/docs/reference/functions/redis.md +0 -28
- package/docs/reference/functions/required.md +0 -41
- package/docs/reference/functions/selected.md +0 -26
- package/docs/reference/functions/sqlite.md +0 -33
- package/docs/reference/functions/testId.md +0 -20
- package/docs/reference/functions/text.md +0 -40
- package/docs/reference/functions/valued.md +0 -28
- package/docs/reference/functions/waitUntil.md +0 -46
- package/docs/reference/functions/within.md +0 -29
- package/docs/reference/index.md +0 -196
- package/docs/reference/interfaces/BaseResult.md +0 -87
- package/docs/reference/interfaces/CallResult.md +0 -160
- package/docs/reference/interfaces/CaptureScope.md +0 -82
- package/docs/reference/interfaces/CliResult.md +0 -253
- package/docs/reference/interfaces/ContainerAccessor.md +0 -166
- package/docs/reference/interfaces/DirectoryAccessor.md +0 -33
- package/docs/reference/interfaces/FetchResult.md +0 -180
- package/docs/reference/interfaces/FilesystemAccessor.md +0 -46
- package/docs/reference/interfaces/HttpResult.md +0 -126
- package/docs/reference/interfaces/JsonAccessor.md +0 -46
- package/docs/reference/interfaces/PageResult.md +0 -356
- package/docs/reference/interfaces/RenderResult.md +0 -72
- package/docs/reference/interfaces/RenderedText.md +0 -110
- package/docs/reference/interfaces/ResponseAccessor.md +0 -47
- package/docs/reference/interfaces/ScreenResult.md +0 -128
- package/docs/reference/interfaces/TableAccessor.md +0 -26
- package/docs/reference/interfaces/TextAccessor.md +0 -96
- package/docs/reference/type-aliases/ApiHandle.md +0 -68
- package/docs/reference/type-aliases/ApiSpecification.md +0 -213
- package/docs/reference/type-aliases/ApiSpecificationOptions.md +0 -68
- package/docs/reference/type-aliases/BrowserConsoleMessage.md +0 -30
- package/docs/reference/type-aliases/BrowserLinkElement.md +0 -50
- package/docs/reference/type-aliases/BrowserMetaElement.md +0 -40
- package/docs/reference/type-aliases/BrowserOpenOptions.md +0 -87
- package/docs/reference/type-aliases/BrowserPage.md +0 -132
- package/docs/reference/type-aliases/BrowserPort.md +0 -51
- package/docs/reference/type-aliases/CliEnv.md +0 -11
- package/docs/reference/type-aliases/CliHandle.md +0 -80
- package/docs/reference/type-aliases/CliInput.md +0 -36
- package/docs/reference/type-aliases/CliOutput.md +0 -46
- package/docs/reference/type-aliases/CliPort.md +0 -58
- package/docs/reference/type-aliases/CliSpecification.md +0 -144
- package/docs/reference/type-aliases/CliSpecificationOptions.md +0 -134
- package/docs/reference/type-aliases/ComponentChain.md +0 -156
- package/docs/reference/type-aliases/ComponentScenario.md +0 -20
- package/docs/reference/type-aliases/ComponentUi.md +0 -47
- package/docs/reference/type-aliases/ComponentVisitor.md +0 -10
- package/docs/reference/type-aliases/ContainerPort.md +0 -113
- package/docs/reference/type-aliases/Contract.md +0 -68
- package/docs/reference/type-aliases/ContractInput.md +0 -13
- package/docs/reference/type-aliases/ContractRequest.md +0 -95
- package/docs/reference/type-aliases/ContractResponder.md +0 -23
- package/docs/reference/type-aliases/ContractResponse.md +0 -78
- package/docs/reference/type-aliases/ContractResponseValue.md +0 -13
- package/docs/reference/type-aliases/Contracts.md +0 -48
- package/docs/reference/type-aliases/DatabaseKeys.md +0 -16
- package/docs/reference/type-aliases/DatabasePort.md +0 -72
- package/docs/reference/type-aliases/DeviceOpenOptions.md +0 -46
- package/docs/reference/type-aliases/DevicePort.md +0 -51
- package/docs/reference/type-aliases/DeviceScreen.md +0 -32
- package/docs/reference/type-aliases/DeviceTimeouts.md +0 -39
- package/docs/reference/type-aliases/DockerSpecConfig.md +0 -43
- package/docs/reference/type-aliases/DomMount.md +0 -21
- package/docs/reference/type-aliases/ElementKind.md +0 -27
- package/docs/reference/type-aliases/ElementMatch.md +0 -74
- package/docs/reference/type-aliases/ElementOptions.md +0 -29
- package/docs/reference/type-aliases/ElementRef.md +0 -128
- package/docs/reference/type-aliases/ExecOptions.md +0 -37
- package/docs/reference/type-aliases/FileAccessor.md +0 -54
- package/docs/reference/type-aliases/HonoApp.md +0 -31
- package/docs/reference/type-aliases/HttpContractFilter.md +0 -49
- package/docs/reference/type-aliases/HttpResponseInit.md +0 -46
- package/docs/reference/type-aliases/HttpStreamInit.md +0 -58
- package/docs/reference/type-aliases/IntegrationHandle.md +0 -46
- package/docs/reference/type-aliases/IntegrationSpecification.md +0 -111
- package/docs/reference/type-aliases/IntegrationSpecificationOptions.md +0 -42
- package/docs/reference/type-aliases/Intercept.md +0 -19
- package/docs/reference/type-aliases/InterceptOptions.md +0 -28
- package/docs/reference/type-aliases/InterceptScope.md +0 -11
- package/docs/reference/type-aliases/IsolationStrategy.md +0 -75
- package/docs/reference/type-aliases/JobHandle.md +0 -34
- package/docs/reference/type-aliases/JobsHandle.md +0 -57
- package/docs/reference/type-aliases/JobsResult.md +0 -16
- package/docs/reference/type-aliases/JobsSpecification.md +0 -94
- package/docs/reference/type-aliases/JobsSpecificationOptions.md +0 -56
- package/docs/reference/type-aliases/LandmarkKind.md +0 -20
- package/docs/reference/type-aliases/LiterateRunFlags.md +0 -27
- package/docs/reference/type-aliases/LiterateServeRegistration.md +0 -70
- package/docs/reference/type-aliases/MatchFixtureOptions.md +0 -27
- package/docs/reference/type-aliases/MatchableRequest.md +0 -61
- package/docs/reference/type-aliases/MatcherKind.md +0 -34
- package/docs/reference/type-aliases/MobileBackendOptions.md +0 -28
- package/docs/reference/type-aliases/MobileElementKind.md +0 -14
- package/docs/reference/type-aliases/MobileElementMatch.md +0 -60
- package/docs/reference/type-aliases/MobileElementRef.md +0 -20
- package/docs/reference/type-aliases/MobileHandle.md +0 -65
- package/docs/reference/type-aliases/MobileScenario.md +0 -20
- package/docs/reference/type-aliases/MobileSpecification.md +0 -51
- package/docs/reference/type-aliases/MobileSpecificationOptions.md +0 -126
- package/docs/reference/type-aliases/MobileVisitor.md +0 -81
- package/docs/reference/type-aliases/MockOfOptions.md +0 -26
- package/docs/reference/type-aliases/MockPort.md +0 -10
- package/docs/reference/type-aliases/PinnedClock.md +0 -10
- package/docs/reference/type-aliases/PostgresOptions.md +0 -32
- package/docs/reference/type-aliases/ProcessOptions.md +0 -112
- package/docs/reference/type-aliases/RedisOptions.md +0 -20
- package/docs/reference/type-aliases/RenderSubject.md +0 -12
- package/docs/reference/type-aliases/ScreenNode.md +0 -67
- package/docs/reference/type-aliases/ServerPort.md +0 -36
- package/docs/reference/type-aliases/ServerResponse.md +0 -46
- package/docs/reference/type-aliases/ServerSpec.md +0 -21
- package/docs/reference/type-aliases/ServiceHandle.md +0 -216
- package/docs/reference/type-aliases/ServiceRecord.md +0 -12
- package/docs/reference/type-aliases/SpecDocument.md +0 -84
- package/docs/reference/type-aliases/SpecEnvToken.md +0 -21
- package/docs/reference/type-aliases/SpecFileAssertion.md +0 -27
- package/docs/reference/type-aliases/SpecFixture.md +0 -30
- package/docs/reference/type-aliases/SpecKind.md +0 -8
- package/docs/reference/type-aliases/SpecRun.md +0 -108
- package/docs/reference/type-aliases/SpecServeEntry.md +0 -40
- package/docs/reference/type-aliases/SpecStream.md +0 -32
- package/docs/reference/type-aliases/SpecificationConfig.md +0 -290
- package/docs/reference/type-aliases/SqliteOptions.md +0 -36
- package/docs/reference/type-aliases/SseEvent.md +0 -58
- package/docs/reference/type-aliases/StreamBody.md +0 -61
- package/docs/reference/type-aliases/TextFilter.md +0 -22
- package/docs/reference/type-aliases/VisitScenario.md +0 -20
- package/docs/reference/type-aliases/Visitor.md +0 -215
- package/docs/reference/type-aliases/WaitUntilOptions.md +0 -46
- package/docs/reference/type-aliases/WebsiteBackendOptions.md +0 -37
- package/docs/reference/type-aliases/WebsiteHandle.md +0 -51
- package/docs/reference/type-aliases/WebsiteSpecification.md +0 -118
- package/docs/reference/type-aliases/WebsiteSpecificationOptions.md +0 -40
- package/docs/reference/variables/anthropic.md +0 -19
- package/docs/reference/variables/banner.md +0 -10
- package/docs/reference/variables/button.md +0 -10
- package/docs/reference/variables/clock.md +0 -16
- package/docs/reference/variables/complementary.md +0 -10
- package/docs/reference/variables/component.md +0 -10
- package/docs/reference/variables/content.md +0 -10
- package/docs/reference/variables/contentinfo.md +0 -10
- package/docs/reference/variables/dialog.md +0 -10
- package/docs/reference/variables/field.md +0 -10
- package/docs/reference/variables/form.md +0 -10
- package/docs/reference/variables/heading.md +0 -10
- package/docs/reference/variables/http.md +0 -40
- package/docs/reference/variables/intercept.md +0 -11
- package/docs/reference/variables/link.md +0 -10
- package/docs/reference/variables/listitem.md +0 -10
- package/docs/reference/variables/main.md +0 -10
- package/docs/reference/variables/match.md +0 -8
- package/docs/reference/variables/navigation.md +0 -10
- package/docs/reference/variables/openai.md +0 -21
- package/docs/reference/variables/option.md +0 -17
- package/docs/reference/variables/region.md +0 -10
- package/docs/reference/variables/row.md +0 -10
- package/docs/reference/variables/search.md +0 -10
- package/docs/reference/variables/specification.md +0 -8
- package/docs/reference/variables/status.md +0 -10
- package/docs/reference/variables/table.md +0 -10
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { t as isRecord } from "./shared-json-shape.js";
|
|
2
|
+
import { n as refusal } from "./shared-refusal.js";
|
|
3
|
+
import { a as readLedgers, i as measure, n as clearLedgers, r as markPartial, s as shardLedgersDir, t as ROUTES_DIR, u as shouldUpdateSnapshots } from "./shared-ledger.js";
|
|
4
|
+
import { u as parseYamlSource } from "./shared-yaml-document.js";
|
|
5
|
+
import { r as testSettingsAt } from "./shared-test-section.js";
|
|
6
|
+
import { t as setupEntry } from "./shared-setup-entry.js";
|
|
7
|
+
import { n as heldUnreached, r as recordUnreached, t as ROUTES_LEDGER } from "./shared-test-baseline.js";
|
|
8
|
+
import { readFileSync } from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
//#region src/specifications/api/routes/methodless.ts
|
|
11
|
+
function shapeOf(path) {
|
|
12
|
+
return path.replaceAll(/:[^/]+/gu, ":");
|
|
13
|
+
}
|
|
14
|
+
function methodlessDrift(table, methodless) {
|
|
15
|
+
const named = new Set(methodless.map((path) => shapeOf(path)));
|
|
16
|
+
const any = [...new Map(table.filter((route) => route.method === "ALL").map((route) => [shapeOf(route.path), route.path]))];
|
|
17
|
+
const taken = new Set(any.map(([shape]) => shape));
|
|
18
|
+
const missed = any.filter(([shape]) => !named.has(shape)).map(([, path]) => ` ALL ${path}: the app takes it on any method, and the list misses it, so add '${path}' with why the app takes it on any method`);
|
|
19
|
+
const stale = methodless.filter((path) => !taken.has(shapeOf(path))).map((path) => table.some((route) => shapeOf(route.path) === shapeOf(path)) ? ` ${path}: the list names it, and the app takes it on its own methods only, so remove '${path}'` : ` ${path}: the list names it, and the app's table holds no such route, so remove '${path}'`);
|
|
20
|
+
return [...missed, ...stale];
|
|
21
|
+
}
|
|
22
|
+
function methodlessDrifted(drift) {
|
|
23
|
+
return {
|
|
24
|
+
fix: "state in `test: { methodless }` exactly the routes the app takes on any method, each with its reason",
|
|
25
|
+
found: `api routes: \`test: { methodless }\` in jterrazz.config.ts drifted from the app's table:\n${drift.join("\n")}`,
|
|
26
|
+
read: "docs/guides/api-specs.md#a-route-taken-on-any-method",
|
|
27
|
+
why: "`test: { methodless }` names the routes the app takes on any method, which the checker files by their path, whatever verb a spec sends."
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
//#endregion
|
|
31
|
+
//#region src/specifications/api/routes/routes.ts
|
|
32
|
+
const METHODS = /* @__PURE__ */ new Set([
|
|
33
|
+
"delete",
|
|
34
|
+
"get",
|
|
35
|
+
"head",
|
|
36
|
+
"options",
|
|
37
|
+
"patch",
|
|
38
|
+
"post",
|
|
39
|
+
"put",
|
|
40
|
+
"trace"
|
|
41
|
+
]);
|
|
42
|
+
const BRACED_PARAMETER = /^\{[^}]+\}$/u;
|
|
43
|
+
function isParameter(segment) {
|
|
44
|
+
return segment.startsWith(":") || BRACED_PARAMETER.test(segment);
|
|
45
|
+
}
|
|
46
|
+
function takesSegment(declared, sent, template) {
|
|
47
|
+
if (sent === void 0) return false;
|
|
48
|
+
if (template && isParameter(sent)) return isParameter(declared);
|
|
49
|
+
return isParameter(declared) ? sent.length > 0 && !template : sent === declared;
|
|
50
|
+
}
|
|
51
|
+
/** A template request is matched by its template, any other by its path; `ALL` takes any method, a `*` tail the rest. */
|
|
52
|
+
function reachesRoute(route, sent) {
|
|
53
|
+
if (route.method !== "ALL" && route.method !== sent.method) return false;
|
|
54
|
+
const template = sent.template !== void 0;
|
|
55
|
+
const declared = route.path.split("/");
|
|
56
|
+
const segments = ((sent.template ?? sent.path).split("?")[0] ?? "").split("/");
|
|
57
|
+
if (declared.at(-1) === "*") return declared.slice(0, -1).every((segment, index) => takesSegment(segment, segments[index], template));
|
|
58
|
+
return declared.length === segments.length && declared.every((segment, index) => takesSegment(segment, segments[index], template));
|
|
59
|
+
}
|
|
60
|
+
function unreachedRoutes(declared, reached) {
|
|
61
|
+
const seen = /* @__PURE__ */ new Set();
|
|
62
|
+
return declared.filter((route) => {
|
|
63
|
+
const key = `${route.method} ${route.path}`;
|
|
64
|
+
if (seen.has(key) || reached.some((sent) => reachesRoute(route, sent))) return false;
|
|
65
|
+
seen.add(key);
|
|
66
|
+
return true;
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
function openApiRoutes(text, json) {
|
|
70
|
+
const parsed = json ? JSON.parse(text) : parseYamlSource(text).document.toJSON();
|
|
71
|
+
const paths = isRecord(parsed) ? parsed.paths : void 0;
|
|
72
|
+
if (!isRecord(paths)) return [];
|
|
73
|
+
return Object.entries(paths).flatMap(([path, item]) => isRecord(item) ? Object.keys(item).filter((method) => METHODS.has(method)).map((method) => ({
|
|
74
|
+
method: method.toUpperCase(),
|
|
75
|
+
path
|
|
76
|
+
})) : []);
|
|
77
|
+
}
|
|
78
|
+
async function declaredRoutes(root) {
|
|
79
|
+
const { routes } = await testSettingsAt(root);
|
|
80
|
+
if (routes === void 0) return;
|
|
81
|
+
return openApiRoutes(readFileSync(join(root, routes), "utf8"), routes.endsWith(".json"));
|
|
82
|
+
}
|
|
83
|
+
async function declaredMethodless(root) {
|
|
84
|
+
const { methodless } = await testSettingsAt(root);
|
|
85
|
+
return [...methodless];
|
|
86
|
+
}
|
|
87
|
+
//#endregion
|
|
88
|
+
//#region src/specifications/api/routes/route-judge.ts
|
|
89
|
+
const UNKNOWN_TABLE = {
|
|
90
|
+
fix: "hand `server` the Hono app itself (its `app.routes` is the table), or declare the routes as an OpenAPI document: `test: { routes: './openapi.yaml' }` in jterrazz.config.ts",
|
|
91
|
+
found: "api routes: an app specification.api() started exposes no route table",
|
|
92
|
+
read: "docs/guides/api-specs.md#an-app-that-is-not-hono",
|
|
93
|
+
why: "A route is judged reached against the table the app declares."
|
|
94
|
+
};
|
|
95
|
+
function spelled(route) {
|
|
96
|
+
return `${route.method} ${route.path}`;
|
|
97
|
+
}
|
|
98
|
+
const SHARD_UNJUDGED = "api routes: not judged, since a shard's ledgers are a part of the run's and the route ratchet is judged only over every shard's ledgers (docs/guides/api-specs.md)";
|
|
99
|
+
const PARTIAL_UNJUDGED = "api routes: not judged, since a partial run (--changed, a file filter or -t) reaches only some routes";
|
|
100
|
+
async function judgeRoutes(root, directory, run) {
|
|
101
|
+
return await judgeLedgers(root, readLedgers(directory), run);
|
|
102
|
+
}
|
|
103
|
+
function unjudgedRun({ failed, scope, update }) {
|
|
104
|
+
if (scope === "shard") return { line: SHARD_UNJUDGED };
|
|
105
|
+
if (scope === "partial") return { line: PARTIAL_UNJUDGED };
|
|
106
|
+
if (failed === 0) return;
|
|
107
|
+
const why = `${failed} file(s) of the run failed, and a failed file may not have sent its requests`;
|
|
108
|
+
return update ? { failure: {
|
|
109
|
+
fix: "make the run green, then record with TEST_UPDATE=1",
|
|
110
|
+
found: `api routes: not recorded: ${why}`,
|
|
111
|
+
read: "docs/guides/api-specs.md#when-a-run-is-not-judged",
|
|
112
|
+
why: "The route ratchet is recorded only from a run that is green and whole."
|
|
113
|
+
} } : { line: `api routes: not judged: ${why}` };
|
|
114
|
+
}
|
|
115
|
+
function ratchetMoved(missing, held, named) {
|
|
116
|
+
return missing > held ? {
|
|
117
|
+
fix: "send each from a spec, or record them with TEST_UPDATE=1",
|
|
118
|
+
found: `api routes: ${missing} declared route(s) no test reached, and the ratchet holds ${held}:\n${named}`,
|
|
119
|
+
read: "docs/guides/api-specs.md#the-ratchet",
|
|
120
|
+
why: "A route no test reached fails the run, beyond the count the ratchet holds."
|
|
121
|
+
} : {
|
|
122
|
+
fix: "the debt fell: record it with TEST_UPDATE=1",
|
|
123
|
+
found: `api routes: ${missing} declared route(s) no test reached, below the ${held} the ratchet holds`,
|
|
124
|
+
read: "docs/guides/api-specs.md#the-ratchet",
|
|
125
|
+
why: "The ratchet is exact in both directions."
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
async function judgeLedgers(root, ledgers, { failed, scope, update }) {
|
|
129
|
+
if (ledgers.length === 0) return {};
|
|
130
|
+
const unjudged = unjudgedRun({
|
|
131
|
+
failed,
|
|
132
|
+
scope,
|
|
133
|
+
update
|
|
134
|
+
});
|
|
135
|
+
if (unjudged !== void 0) return unjudged;
|
|
136
|
+
const run = measure(ledgers);
|
|
137
|
+
const table = await declaredRoutes(root) ?? run.declared;
|
|
138
|
+
if (table === void 0) return { failure: UNKNOWN_TABLE };
|
|
139
|
+
const drift = run.declared === void 0 ? [] : methodlessDrift(run.declared, await declaredMethodless(root));
|
|
140
|
+
if (drift.length > 0) return { failure: methodlessDrifted(drift) };
|
|
141
|
+
const declared = [...new Map(table.map((route) => [spelled(route), route])).values()];
|
|
142
|
+
const missing = unreachedRoutes(declared, run.reached);
|
|
143
|
+
const named = missing.map((route) => ` ${spelled(route)}`).join("\n");
|
|
144
|
+
if (update) {
|
|
145
|
+
recordUnreached(root, missing.length);
|
|
146
|
+
return { line: `api routes: ${missing.length} unreached recorded under ${ROUTES_LEDGER}` };
|
|
147
|
+
}
|
|
148
|
+
const held = heldUnreached(root);
|
|
149
|
+
if (missing.length !== held) return { failure: ratchetMoved(missing.length, held, named) };
|
|
150
|
+
return { line: missing.length === 0 ? `api routes: all ${declared.length} declared routes reached` : `api routes: ${missing.length} unreached, held by ${ROUTES_LEDGER}:\n${named}` };
|
|
151
|
+
}
|
|
152
|
+
//#endregion
|
|
153
|
+
//#region src/specifications/api/routes/route-coverage.global-setup.ts
|
|
154
|
+
async function scopeOf(project) {
|
|
155
|
+
if (project.vitest.config.shard !== void 0) return "shard";
|
|
156
|
+
if (project.vitest.config.testNamePattern !== void 0) return "partial";
|
|
157
|
+
const ran = new Set(project.vitest.state.getFiles().filter((file) => file.projectName === project.name).map((file) => file.filepath));
|
|
158
|
+
const { testFiles } = await project.globTestFiles();
|
|
159
|
+
return testFiles.every((file) => ran.has(file)) ? "whole" : "partial";
|
|
160
|
+
}
|
|
161
|
+
function routeCoverageSetup() {
|
|
162
|
+
return setupEntry(import.meta.dirname, "route-coverage");
|
|
163
|
+
}
|
|
164
|
+
function setupRoutes(project) {
|
|
165
|
+
const { root } = project.config;
|
|
166
|
+
const shard = project.vitest.config.shard;
|
|
167
|
+
const directory = join(root, shard === void 0 ? ROUTES_DIR : shardLedgersDir(shard));
|
|
168
|
+
clearLedgers(directory);
|
|
169
|
+
if (shard !== void 0 && project.vitest.config.testNamePattern !== void 0) markPartial(directory);
|
|
170
|
+
project.provide("routesLedger", directory);
|
|
171
|
+
return async () => {
|
|
172
|
+
const { failure, line } = await judgeRoutes(root, directory, {
|
|
173
|
+
failed: project.vitest.state.getFailedFilepaths().length,
|
|
174
|
+
scope: await scopeOf(project),
|
|
175
|
+
update: shouldUpdateSnapshots()
|
|
176
|
+
});
|
|
177
|
+
if (failure !== void 0) throw new Error(refusal(failure));
|
|
178
|
+
if (line !== void 0) process.stdout.write(`${line}\n`);
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
function setup(project) {
|
|
182
|
+
return setupRoutes(project);
|
|
183
|
+
}
|
|
184
|
+
//#endregion
|
|
185
|
+
export { setup as n, setupRoutes as r, routeCoverageSetup as t };
|
|
186
|
+
|
|
187
|
+
//# sourceMappingURL=shared-route-coverage.global-setup.js.map
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { n as refusal$1 } from "./shared-refusal.js";
|
|
2
|
+
import { lessonParts, refusalBlock } from "@jterrazz/typescript/rules";
|
|
3
|
+
//#region src/visitor/targets/as-written.ts
|
|
4
|
+
const FUNCTION_NAME_OF_KIND = {
|
|
5
|
+
banner: "banner",
|
|
6
|
+
button: "button",
|
|
7
|
+
complementary: "complementary",
|
|
8
|
+
contentinfo: "contentinfo",
|
|
9
|
+
dialog: "dialog",
|
|
10
|
+
field: "field",
|
|
11
|
+
form: "form",
|
|
12
|
+
heading: "heading",
|
|
13
|
+
link: "link",
|
|
14
|
+
listitem: "listitem",
|
|
15
|
+
main: "main",
|
|
16
|
+
navigation: "navigation",
|
|
17
|
+
option: "option",
|
|
18
|
+
region: "region",
|
|
19
|
+
row: "row",
|
|
20
|
+
search: "search",
|
|
21
|
+
status: "status",
|
|
22
|
+
table: "table",
|
|
23
|
+
testId: "testId",
|
|
24
|
+
text: "content"
|
|
25
|
+
};
|
|
26
|
+
/** Spoken in the vocabulary the author wrote, never playwright's: an option appears only when the author stated one. */
|
|
27
|
+
function asWritten(target) {
|
|
28
|
+
const bare = asWrittenUnscoped(target);
|
|
29
|
+
return target.scope ? `within(${asWritten(target.scope)}, ${bare})` : bare;
|
|
30
|
+
}
|
|
31
|
+
function asWrittenUnscoped(target) {
|
|
32
|
+
const args = [];
|
|
33
|
+
if (target.name !== void 0) args.push(JSON.stringify(target.name));
|
|
34
|
+
if (target.exact !== void 0) args.push(`{ exact: ${String(target.exact)} }`);
|
|
35
|
+
return `${FUNCTION_NAME_OF_KIND[target.kind]}(${args.join(", ")})`;
|
|
36
|
+
}
|
|
37
|
+
//#endregion
|
|
38
|
+
//#region src/visitor/matching/landmark-of-tag.ts
|
|
39
|
+
const LANDMARK_ELEMENTS = {
|
|
40
|
+
banner: "header",
|
|
41
|
+
complementary: "aside",
|
|
42
|
+
contentinfo: "footer",
|
|
43
|
+
form: "form",
|
|
44
|
+
main: "main",
|
|
45
|
+
navigation: "nav",
|
|
46
|
+
region: "section",
|
|
47
|
+
search: "search"
|
|
48
|
+
};
|
|
49
|
+
const LANDMARK_SELECTOR = Object.entries(LANDMARK_ELEMENTS).flatMap(([kind, tag]) => [tag, `[role="${kind}"]`]).join(", ");
|
|
50
|
+
function isLandmarkKind(value) {
|
|
51
|
+
return Object.hasOwn(LANDMARK_ELEMENTS, value);
|
|
52
|
+
}
|
|
53
|
+
function landmarkOfTagOrRole(value) {
|
|
54
|
+
const kind = Object.entries(LANDMARK_ELEMENTS).find(([, tag]) => tag === value)?.[0] ?? value;
|
|
55
|
+
return isLandmarkKind(kind) ? kind : void 0;
|
|
56
|
+
}
|
|
57
|
+
//#endregion
|
|
58
|
+
//#region src/visitor/matching/ambiguous.ts
|
|
59
|
+
function landmarkOf(match) {
|
|
60
|
+
const kind = landmarkOfTagOrRole(match.context ?? "");
|
|
61
|
+
if (kind !== "region") return kind === void 0 ? void 0 : `${FUNCTION_NAME_OF_KIND[kind]}()`;
|
|
62
|
+
return match.contextName === void 0 ? void 0 : `${FUNCTION_NAME_OF_KIND.region}(${JSON.stringify(match.contextName)})`;
|
|
63
|
+
}
|
|
64
|
+
function formatMatch(match, index) {
|
|
65
|
+
const attribute = match.detail === void 0 ? "" : ` ${quoteDetail(match)}`;
|
|
66
|
+
const matchedOnLabel = match.accessibleName === void 0 || match.accessibleName === "" ? "" : ` named ${JSON.stringify(match.accessibleName)}`;
|
|
67
|
+
const context = match.context === void 0 || match.context === "" ? "" : ` in <${match.context}>`;
|
|
68
|
+
return ` ${index + 1}. <${match.tag}${attribute}>${match.text}</${match.tag}>${matchedOnLabel}${context}`;
|
|
69
|
+
}
|
|
70
|
+
function quoteDetail(match) {
|
|
71
|
+
return `${match.tag === "a" ? "href" : "name"}=${JSON.stringify(match.detail)}`;
|
|
72
|
+
}
|
|
73
|
+
function landmarkCounts(matches) {
|
|
74
|
+
const counts = /* @__PURE__ */ new Map();
|
|
75
|
+
for (const match of matches) {
|
|
76
|
+
const landmark = landmarkOf(match);
|
|
77
|
+
if (landmark !== void 0) counts.set(landmark, (counts.get(landmark) ?? 0) + 1);
|
|
78
|
+
}
|
|
79
|
+
return [...counts].toSorted(([, a], [, b]) => a - b);
|
|
80
|
+
}
|
|
81
|
+
function formatFixes(target, matches) {
|
|
82
|
+
const fixes = [];
|
|
83
|
+
const landmarksThatCutTheSet = landmarkCounts(matches).filter(([, count]) => count < matches.length);
|
|
84
|
+
if (!target.scope) {
|
|
85
|
+
const [first, ...rest] = landmarksThatCutTheSet;
|
|
86
|
+
const alsoHere = rest.length > 0 ? ` [also here: ${rest.map(([name]) => name).join(", ")}]` : "";
|
|
87
|
+
fixes.push(first === void 0 ? `scope it within(<a container holding only this one>, ${asWrittenUnscoped(target)})` : `scope it within(${first[0]}, ${asWrittenUnscoped(target)}) [leaves ${first[1]} of ${matches.length}]${alsoHere}`);
|
|
88
|
+
}
|
|
89
|
+
if (target.exact === false) {
|
|
90
|
+
const remaining = matches.filter((match) => match.text === target.name).length;
|
|
91
|
+
if (remaining > 0 && remaining < matches.length) fixes.push(`exact name ${asWrittenUnscoped({
|
|
92
|
+
...target,
|
|
93
|
+
exact: true
|
|
94
|
+
})} [leaves ${remaining} of ${matches.length}]`);
|
|
95
|
+
}
|
|
96
|
+
fixes.push("other element a heading(), button() or field() may name one thing where this does not");
|
|
97
|
+
return fixes;
|
|
98
|
+
}
|
|
99
|
+
function ambiguityError(options) {
|
|
100
|
+
const { target, fixes, lines, where } = options;
|
|
101
|
+
return new Error(refusal$1("target-matches-one-element", {
|
|
102
|
+
fix: ["designate one of them:", ...fixes.map((fix) => ` • ${fix}`)].join("\n"),
|
|
103
|
+
found: [`Ambiguous element: ${asWritten(target)} matched ${lines.length} elements on ${where}:`, ...lines].join("\n")
|
|
104
|
+
}));
|
|
105
|
+
}
|
|
106
|
+
function ambiguousTargetError(options) {
|
|
107
|
+
const { target, matches, url } = options;
|
|
108
|
+
return ambiguityError({
|
|
109
|
+
target,
|
|
110
|
+
fixes: formatFixes(target, matches),
|
|
111
|
+
lines: matches.map((match, index) => formatMatch(match, index)),
|
|
112
|
+
where: url
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
function shortened(text) {
|
|
116
|
+
return text.replaceAll(/\s+/gu, " ").trim().slice(0, 80);
|
|
117
|
+
}
|
|
118
|
+
const CANDIDATE_LIMITS = {
|
|
119
|
+
landmarks: LANDMARK_SELECTOR,
|
|
120
|
+
limit: 10,
|
|
121
|
+
textLength: 80
|
|
122
|
+
};
|
|
123
|
+
/** Serialized into the page by playwright: it reads nothing from module scope. */
|
|
124
|
+
function candidatesOf(nodes, { landmarks, limit, textLength }) {
|
|
125
|
+
const landmarkOf = (node) => {
|
|
126
|
+
const landmark = node.parentElement?.closest(landmarks);
|
|
127
|
+
if (landmark === null || landmark === void 0) return {
|
|
128
|
+
context: void 0,
|
|
129
|
+
contextName: void 0
|
|
130
|
+
};
|
|
131
|
+
const labelledBy = (landmark.getAttribute("aria-labelledby") ?? "").split(/\s+/u).filter((id) => id !== "").map((id) => document.querySelector(`#${CSS.escape(id)}`)?.textContent ?? "").join(" ");
|
|
132
|
+
const name = (landmark.getAttribute("aria-label") ?? labelledBy).replaceAll(/\s+/gu, " ").trim();
|
|
133
|
+
return {
|
|
134
|
+
context: landmark.getAttribute("role") ?? landmark.tagName.toLowerCase(),
|
|
135
|
+
contextName: name === "" ? void 0 : name
|
|
136
|
+
};
|
|
137
|
+
};
|
|
138
|
+
return nodes.slice(0, limit).map((node) => {
|
|
139
|
+
const detail = node.getAttribute("href") ?? node.getAttribute("name");
|
|
140
|
+
const text = (node.textContent ?? "").replaceAll(/\s+/gu, " ").trim().slice(0, textLength);
|
|
141
|
+
const label = node.getAttribute("aria-label")?.trim();
|
|
142
|
+
const { context, contextName } = landmarkOf(node);
|
|
143
|
+
return {
|
|
144
|
+
accessibleName: label === void 0 || label === "" || label === text ? void 0 : label,
|
|
145
|
+
context,
|
|
146
|
+
contextName,
|
|
147
|
+
detail: detail ?? void 0,
|
|
148
|
+
tag: node.tagName.toLowerCase(),
|
|
149
|
+
text
|
|
150
|
+
};
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
//#endregion
|
|
154
|
+
//#region src/chain/refusals/scenario-failure.ts
|
|
155
|
+
const SURFACE_OF = {
|
|
156
|
+
open: "screen",
|
|
157
|
+
visit: "page"
|
|
158
|
+
};
|
|
159
|
+
const CHAPTER_ANCHOR = /^docs\/[^#]+\.md#.+$/u;
|
|
160
|
+
const isAnchor = (read) => CHAPTER_ANCHOR.test(read);
|
|
161
|
+
function finishedLesson(error) {
|
|
162
|
+
if (!(error instanceof Error)) return;
|
|
163
|
+
const { fix = "", found = "", read, why = "" } = lessonParts(error.message) ?? {};
|
|
164
|
+
const anchor = String(read);
|
|
165
|
+
return [
|
|
166
|
+
fix,
|
|
167
|
+
found,
|
|
168
|
+
why
|
|
169
|
+
].includes("") || !isAnchor(anchor) ? void 0 : {
|
|
170
|
+
fix,
|
|
171
|
+
found,
|
|
172
|
+
read: anchor,
|
|
173
|
+
why
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
function scenarioFailure(error, evidence, verb) {
|
|
177
|
+
const suffix = evidence === null ? "" : `\nEvidence: ${evidence}`;
|
|
178
|
+
const lesson = finishedLesson(error);
|
|
179
|
+
if (error instanceof Error && lesson !== void 0) {
|
|
180
|
+
error.message = refusalBlock({
|
|
181
|
+
...lesson,
|
|
182
|
+
found: `${lesson.found}${suffix}`
|
|
183
|
+
});
|
|
184
|
+
return error;
|
|
185
|
+
}
|
|
186
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
187
|
+
const surface = SURFACE_OF[verb];
|
|
188
|
+
return new Error(refusal$1({
|
|
189
|
+
fix: `correct the step that failed, or the ${surface} it drives`,
|
|
190
|
+
found: `the ${verb} scenario failed:\n ${message.replaceAll("\n", "\n ")}${suffix}`,
|
|
191
|
+
read: verb === "visit" ? "docs/guides/website-specs.md#visit-scenarios-the-when" : "docs/guides/mobile-specs.md#open-scenarios-the-when",
|
|
192
|
+
why: `${verb === "visit" ? "A" : "An"} ${verb} scenario drives the ${surface} step by step, and the first step that fails ends it.`
|
|
193
|
+
}), { cause: error });
|
|
194
|
+
}
|
|
195
|
+
//#endregion
|
|
196
|
+
export { ambiguityError as a, asWrittenUnscoped as c, shortened as i, CANDIDATE_LIMITS as n, ambiguousTargetError as o, candidatesOf as r, asWritten as s, scenarioFailure as t };
|
|
197
|
+
|
|
198
|
+
//# sourceMappingURL=shared-scenario-failure.js.map
|