@jterrazz/test 16.1.1 → 18.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 +1 -2980
- package/dist/browser/services.js +1 -0
- package/dist/browser/stubs.js +1 -0
- package/dist/browser-shared-index.browser.js +3233 -0
- package/dist/checker.d.ts +6 -1
- package/dist/checker.js +3721 -2624
- package/dist/config.d.ts +40 -0
- package/dist/config.js +309 -0
- package/dist/in-place.global-setup.d.ts +12 -0
- package/dist/in-place.global-setup.js +21 -0
- package/dist/index.d.ts +1641 -3004
- package/dist/index.js +5095 -3835
- package/dist/mobile-session.global-setup.d.ts +15 -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 +3979 -5799
- package/dist/route-coverage.global-setup.d.ts +43 -0
- package/dist/route-coverage.global-setup.js +1 -0
- package/dist/services.d.ts +3 -0
- package/dist/services.global-setup.d.ts +43 -0
- package/dist/services.global-setup.js +1 -0
- package/dist/services.js +597 -0
- package/dist/shared-appium.adapter.js +428 -0
- package/dist/shared-artifacts.js +34 -0
- package/dist/shared-chapter.js +7 -0
- package/dist/shared-container.port.d.ts +31 -0
- package/dist/shared-derived-projects.js +73 -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.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-listened-port.js +19 -0
- package/dist/shared-literate-plugin.js +184 -0
- package/dist/shared-manifest.js +1321 -0
- package/dist/shared-pause.js +22 -0
- package/dist/shared-peer.js +88 -0
- package/dist/shared-playwright.adapter.js +328 -0
- package/dist/shared-process-alive.js +13 -0
- package/dist/shared-process.adapter.js +74 -0
- package/dist/shared-record-key.js +13 -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 +407 -0
- package/dist/shared-service-images.js +10 -0
- package/dist/shared-services.global-setup.js +174 -0
- package/dist/shared-session-ledger.js +305 -0
- package/dist/shared-setup-entry.js +12 -0
- package/dist/shared-source-scan.js +133 -0
- package/dist/shared-spec-document.js +303 -0
- package/dist/shared-spec-grammar.js +66 -0
- package/dist/shared-specifications.js +26 -0
- package/dist/shared-specs-modules.js +1033 -0
- package/dist/shared-stub-reply.js +167 -0
- package/dist/shared-stub-server.js +142 -0
- package/dist/shared-temp-root.global-setup.js +73 -0
- package/dist/shared-test-baseline.js +26 -0
- package/dist/shared-test-section.js +58 -0
- package/dist/shared-testcontainers.adapter.js +306 -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 +1 -0
- package/dist/vitest.d.ts +93 -164
- package/dist/vitest.js +382 -550
- 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 +566 -0
- package/docs/guides/component-specs.md +252 -0
- package/docs/guides/develop.md +97 -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 +44 -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 +500 -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 +228 -0
- package/package.json +56 -36
- 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,167 @@
|
|
|
1
|
+
import { n as refusal } from "./shared-refusal.js";
|
|
2
|
+
import { c as urlMatches, o as describeRoute, s as routePatternOf } from "./shared-exchange.js";
|
|
3
|
+
import { t as pause } from "./shared-pause.js";
|
|
4
|
+
//#region src/stubs/matching/stub-queue.ts
|
|
5
|
+
function stubMatches(stub, request) {
|
|
6
|
+
const { absentHeaders, match, method, url } = stub.request;
|
|
7
|
+
if (method !== "*" && method.toUpperCase() !== request.method.toUpperCase()) return false;
|
|
8
|
+
if (!urlMatches(url, request.url)) return false;
|
|
9
|
+
if ((absentHeaders ?? []).some((name) => request.headers[name.toLowerCase()] !== void 0)) return false;
|
|
10
|
+
return match ? match(request) : true;
|
|
11
|
+
}
|
|
12
|
+
function isNever({ required, times }) {
|
|
13
|
+
return required === true && times === 0;
|
|
14
|
+
}
|
|
15
|
+
function stateOf({ stub, served }) {
|
|
16
|
+
const { times } = stub;
|
|
17
|
+
if (isNever(stub)) return served === 0 ? " (never)" : ` (never, reached ${served} time(s))`;
|
|
18
|
+
if (served >= (times ?? Number.POSITIVE_INFINITY)) return ` (exhausted after ${times})`;
|
|
19
|
+
return served === 0 ? "" : ` (served ${served} time(s))`;
|
|
20
|
+
}
|
|
21
|
+
/** The first unexhausted stub in declaration order answers; `times: n` is spent after n serves. */
|
|
22
|
+
var StubQueue = class {
|
|
23
|
+
entries;
|
|
24
|
+
constructor(stubs) {
|
|
25
|
+
this.entries = stubs.map((stub) => ({
|
|
26
|
+
reached: [],
|
|
27
|
+
stub,
|
|
28
|
+
served: 0
|
|
29
|
+
}));
|
|
30
|
+
}
|
|
31
|
+
/** Deduped by pattern: two stubs on one URL share one handler and one queue. */
|
|
32
|
+
get routes() {
|
|
33
|
+
const routes = /* @__PURE__ */ new Map();
|
|
34
|
+
for (const { stub } of this.entries) {
|
|
35
|
+
const url = routePatternOf(stub.request.url);
|
|
36
|
+
const key = String(url);
|
|
37
|
+
const route = routes.get(key) ?? {
|
|
38
|
+
methods: [],
|
|
39
|
+
url
|
|
40
|
+
};
|
|
41
|
+
if (!route.methods.includes(stub.request.method)) route.methods.push(stub.request.method);
|
|
42
|
+
routes.set(key, route);
|
|
43
|
+
}
|
|
44
|
+
return [...routes.values()];
|
|
45
|
+
}
|
|
46
|
+
declaredRoutes() {
|
|
47
|
+
return this.entries.map(({ stub }) => describeRoute(stub.request));
|
|
48
|
+
}
|
|
49
|
+
/** `null` when nothing unexhausted matches: the strict-violation signal. */
|
|
50
|
+
take(request) {
|
|
51
|
+
for (const entry of this.entries) {
|
|
52
|
+
if (entry.served >= (entry.stub.times ?? Number.POSITIVE_INFINITY) && !isNever(entry.stub) || !stubMatches(entry.stub, request)) continue;
|
|
53
|
+
entry.served += 1;
|
|
54
|
+
entry.reached.push(`${request.method} ${request.url}`);
|
|
55
|
+
return entry.stub;
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
verdict(unmatched = null) {
|
|
60
|
+
const errors = [
|
|
61
|
+
this.neverError(),
|
|
62
|
+
unmatched,
|
|
63
|
+
this.unmetError()
|
|
64
|
+
].filter((error) => error !== null);
|
|
65
|
+
if (errors.length <= 1) return errors[0] ?? null;
|
|
66
|
+
const reports = errors.map(({ message }) => message).join("\n\n");
|
|
67
|
+
return new AggregateError(errors, refusal({
|
|
68
|
+
fix: "correct each outcome in turn, by its own fix",
|
|
69
|
+
found: `${errors.length} outcomes of one action, each with its own refusal:\n\n${reports}`,
|
|
70
|
+
read: "docs/reference/stubs.md#strict-by-construction",
|
|
71
|
+
why: "A reached never stub, an unmatched request and an unmet required stub are independent facts about one action, so all are reported at once."
|
|
72
|
+
}));
|
|
73
|
+
}
|
|
74
|
+
neverError() {
|
|
75
|
+
const reached = this.entries.filter(({ stub, served }) => isNever(stub) && served > 0);
|
|
76
|
+
if (reached.length === 0) return null;
|
|
77
|
+
const lines = reached.map(({ reached: calls, stub }) => ` - ${describeRoute(stub.request)}: reached by ${calls.join(", ")}`);
|
|
78
|
+
return new Error(refusal({
|
|
79
|
+
fix: "take the call out of the path under test or, if the call is now intended, replace the never stub with one that answers it",
|
|
80
|
+
found: `a route the chain declared never requested was reached:\n${lines.join("\n")}`,
|
|
81
|
+
read: "docs/reference/stubs.md#never-a-call-that-must-not-happen",
|
|
82
|
+
why: "A never stub states the subject must not reach its route within the chain's terminal action, or the `intercept()` scope, it is declared for."
|
|
83
|
+
}));
|
|
84
|
+
}
|
|
85
|
+
unmetError() {
|
|
86
|
+
const unmet = [];
|
|
87
|
+
for (const { stub, served } of this.entries) {
|
|
88
|
+
if (stub.required !== true || isNever(stub)) continue;
|
|
89
|
+
const { times } = stub;
|
|
90
|
+
if (times === void 0) {
|
|
91
|
+
if (served === 0) unmet.push(` - ${describeRoute(stub.request)}: declared and required but never requested`);
|
|
92
|
+
} else if (served !== times) unmet.push(` - ${describeRoute(stub.request)}: declared required with times: ${times} but was requested ${served} time(s)`);
|
|
93
|
+
}
|
|
94
|
+
if (unmet.length === 0) return null;
|
|
95
|
+
return new Error(refusal({
|
|
96
|
+
fix: "the code no longer makes the call, or the stub no longer describes it: correct the one that drifted",
|
|
97
|
+
found: `required stub(s) never satisfied during the chain:\n${unmet.join("\n")}`,
|
|
98
|
+
read: "docs/reference/stubs.md#required-is-verified-per-terminal-action",
|
|
99
|
+
why: "A required stub states the call MUST happen, within the terminal action it is declared for."
|
|
100
|
+
}));
|
|
101
|
+
}
|
|
102
|
+
unmatchedError(method, url) {
|
|
103
|
+
const declared = this.entries.length === 0 ? " (no stubs declared)" : this.entries.map((entry) => ` - ${describeRoute(entry.stub.request)}${stateOf(entry)}`).join("\n");
|
|
104
|
+
return new Error(refusal("network-strict-after-first-stub", {
|
|
105
|
+
fix: `declare a stub for ${method} ${url}, or raise the \`times\` of the one it exhausted`,
|
|
106
|
+
found: `an unmatched outgoing request, ${method} ${url}; declared:\n${declared}`
|
|
107
|
+
}));
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
//#endregion
|
|
111
|
+
//#region src/stubs/delivery/stream-body.ts
|
|
112
|
+
async function nextTask() {
|
|
113
|
+
const { port1, port2 } = new MessageChannel();
|
|
114
|
+
await new Promise((resolve) => {
|
|
115
|
+
port1.onmessage = () => {
|
|
116
|
+
port1.close();
|
|
117
|
+
resolve();
|
|
118
|
+
};
|
|
119
|
+
port2.postMessage(null);
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
/** One serialisation for all three deliveries (msw server, msw worker, `node:http` stub). */
|
|
123
|
+
function toReadableStream(body) {
|
|
124
|
+
const encoder = new TextEncoder();
|
|
125
|
+
let index = 0;
|
|
126
|
+
return new ReadableStream({ async pull(controller) {
|
|
127
|
+
const chunk = body.chunks[index];
|
|
128
|
+
if (chunk === void 0) {
|
|
129
|
+
controller.close();
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (index > 0) {
|
|
133
|
+
await pause(body.delayBetweenChunks);
|
|
134
|
+
await nextTask();
|
|
135
|
+
}
|
|
136
|
+
index += 1;
|
|
137
|
+
controller.enqueue(encoder.encode(chunk));
|
|
138
|
+
} });
|
|
139
|
+
}
|
|
140
|
+
//#endregion
|
|
141
|
+
//#region src/stubs/delivery/stub-reply.ts
|
|
142
|
+
/** A request's body as a stub's filter reads it: the parsed JSON, the raw text when it is none, `null` when there is none. */
|
|
143
|
+
function requestBody(raw) {
|
|
144
|
+
if (raw.length === 0) return null;
|
|
145
|
+
try {
|
|
146
|
+
return JSON.parse(raw);
|
|
147
|
+
} catch {
|
|
148
|
+
return raw;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/** The reply a matched stub states for this request, once its delay has passed. */
|
|
152
|
+
async function replyOf(response, observed) {
|
|
153
|
+
const reply = typeof response === "function" ? response(observed) : response;
|
|
154
|
+
await pause(reply.delay ?? 0);
|
|
155
|
+
return reply;
|
|
156
|
+
}
|
|
157
|
+
/** The `error` of the 501 every delivery answers a request it refuses, whichever process serves it. */
|
|
158
|
+
function refusedReply(summary) {
|
|
159
|
+
return `@jterrazz/test strict stubs: ${summary}`;
|
|
160
|
+
}
|
|
161
|
+
function unmatchedSummary(method, url) {
|
|
162
|
+
return `unmatched request ${method} ${url}`;
|
|
163
|
+
}
|
|
164
|
+
//#endregion
|
|
165
|
+
export { toReadableStream as a, unmatchedSummary as i, replyOf as n, StubQueue as o, requestBody as r, refusedReply as t };
|
|
166
|
+
|
|
167
|
+
//# sourceMappingURL=shared-stub-reply.js.map
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { n as refusal } from "./shared-refusal.js";
|
|
2
|
+
import { n as isStreamBody } from "./shared-exchange.js";
|
|
3
|
+
import { a as toReadableStream, i as unmatchedSummary, n as replyOf, o as StubQueue, r as requestBody, t as refusedReply } from "./shared-stub-reply.js";
|
|
4
|
+
import { METHODS } from "node:http";
|
|
5
|
+
//#region src/stubs/delivery/msw-handlers.ts
|
|
6
|
+
const NEVER_CACHED = { "cache-control": "no-store" };
|
|
7
|
+
function toMswResponse(msw, response) {
|
|
8
|
+
const { body, headers, status = 200 } = response;
|
|
9
|
+
if (response.transport === "network-error") return msw.HttpResponse.error();
|
|
10
|
+
if (body === null || body === void 0) return new msw.HttpResponse(null, {
|
|
11
|
+
headers: {
|
|
12
|
+
...NEVER_CACHED,
|
|
13
|
+
...headers
|
|
14
|
+
},
|
|
15
|
+
status
|
|
16
|
+
});
|
|
17
|
+
if (isStreamBody(body)) return new msw.HttpResponse(toReadableStream(body), {
|
|
18
|
+
headers: {
|
|
19
|
+
...NEVER_CACHED,
|
|
20
|
+
"content-type": body.contentType,
|
|
21
|
+
...headers
|
|
22
|
+
},
|
|
23
|
+
status
|
|
24
|
+
});
|
|
25
|
+
if (typeof body === "string") return new msw.HttpResponse(body, {
|
|
26
|
+
headers: {
|
|
27
|
+
...NEVER_CACHED,
|
|
28
|
+
"content-type": "text/plain; charset=utf-8",
|
|
29
|
+
...headers
|
|
30
|
+
},
|
|
31
|
+
status
|
|
32
|
+
});
|
|
33
|
+
return msw.HttpResponse.json(body, {
|
|
34
|
+
headers: {
|
|
35
|
+
...NEVER_CACHED,
|
|
36
|
+
...headers
|
|
37
|
+
},
|
|
38
|
+
status
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
function handlerOf(msw, method) {
|
|
42
|
+
return {
|
|
43
|
+
"*": msw.http.all,
|
|
44
|
+
DELETE: msw.http.delete,
|
|
45
|
+
GET: msw.http.get,
|
|
46
|
+
HEAD: msw.http.head,
|
|
47
|
+
OPTIONS: msw.http.options,
|
|
48
|
+
PATCH: msw.http.patch,
|
|
49
|
+
POST: msw.http.post,
|
|
50
|
+
PUT: msw.http.put
|
|
51
|
+
}[method.toUpperCase()];
|
|
52
|
+
}
|
|
53
|
+
function buildStubHandlers(msw, stubs, bypass) {
|
|
54
|
+
const queue = new StubQueue(stubs);
|
|
55
|
+
let violation = null;
|
|
56
|
+
const refuse = (error, summary) => {
|
|
57
|
+
violation ??= error;
|
|
58
|
+
return msw.HttpResponse.json({ error: refusedReply(summary) }, {
|
|
59
|
+
headers: NEVER_CACHED,
|
|
60
|
+
status: 501
|
|
61
|
+
});
|
|
62
|
+
};
|
|
63
|
+
const recordViolation = (method, url) => refuse(queue.unmatchedError(method, url), unmatchedSummary(method, url));
|
|
64
|
+
const handlers = [];
|
|
65
|
+
for (const route of queue.routes) for (const method of route.methods) {
|
|
66
|
+
const handlerFn = handlerOf(msw, method);
|
|
67
|
+
if (handlerFn === void 0) continue;
|
|
68
|
+
handlers.push(handlerFn(route.url, async ({ request }) => {
|
|
69
|
+
const observed = await observe(request);
|
|
70
|
+
const stub = queue.take(observed);
|
|
71
|
+
if (!stub) return recordViolation(request.method, request.url);
|
|
72
|
+
return toMswResponse(msw, await replyOf(stub.response, observed));
|
|
73
|
+
}));
|
|
74
|
+
}
|
|
75
|
+
handlers.push(msw.http.all("*", ({ request }) => bypass?.(request.url) === true ? msw.passthrough() : recordViolation(request.method, request.url)));
|
|
76
|
+
return {
|
|
77
|
+
handlers,
|
|
78
|
+
refuse,
|
|
79
|
+
violation: () => queue.verdict(violation)
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
async function observe(request) {
|
|
83
|
+
return {
|
|
84
|
+
body: requestBody(await request.clone().text()),
|
|
85
|
+
headers: Object.fromEntries(request.headers),
|
|
86
|
+
method: request.method.toUpperCase(),
|
|
87
|
+
url: request.url
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
//#endregion
|
|
91
|
+
//#region src/stubs/delivery/same-process/stub-server.ts
|
|
92
|
+
async function loadMsw() {
|
|
93
|
+
const [{ setupServer }, msw] = await Promise.all([import("msw/node"), import("msw")]);
|
|
94
|
+
return {
|
|
95
|
+
msw,
|
|
96
|
+
setupServer: () => setupServer()
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
let serverInstance = null;
|
|
100
|
+
let refuseOffTheWire = null;
|
|
101
|
+
function offTheWireError(request) {
|
|
102
|
+
return new Error(refusal("network-strict-after-first-stub", {
|
|
103
|
+
fix: "send a method in the capitals the HTTP parser reads (`PATCH`, not `patch`): fetch upper-cases DELETE, GET, HEAD, OPTIONS, POST and PUT, and sends any other as written",
|
|
104
|
+
found: `an outgoing request the network double cannot read off the wire, ${request.method} ${request.url}: its method is not one of node's \`http.METHODS\`, so no stub could answer it and it would reach the network`
|
|
105
|
+
}));
|
|
106
|
+
}
|
|
107
|
+
function guardOffTheWireMethods() {
|
|
108
|
+
const next = globalThis.fetch;
|
|
109
|
+
globalThis.fetch = async (input, init) => {
|
|
110
|
+
const request = new Request(input, init);
|
|
111
|
+
if (refuseOffTheWire !== null && !METHODS.includes(request.method)) return refuseOffTheWire(request);
|
|
112
|
+
return await next(request);
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
async function ensureStubServer() {
|
|
116
|
+
if (serverInstance !== null) return serverInstance;
|
|
117
|
+
const { setupServer } = await loadMsw();
|
|
118
|
+
const server = setupServer();
|
|
119
|
+
server.listen({ onUnhandledFrame: "bypass" });
|
|
120
|
+
guardOffTheWireMethods();
|
|
121
|
+
serverInstance = server;
|
|
122
|
+
return server;
|
|
123
|
+
}
|
|
124
|
+
/** The trailing catch-all records any request no stub accepted; the chain's pipeline rethrows it. */
|
|
125
|
+
async function registerStubs(stubs) {
|
|
126
|
+
const server = await ensureStubServer();
|
|
127
|
+
const { msw } = await loadMsw();
|
|
128
|
+
const { handlers, refuse, violation } = buildStubHandlers(msw, stubs);
|
|
129
|
+
server.use(...handlers);
|
|
130
|
+
refuseOffTheWire = (request) => refuse(offTheWireError(request), `unreadable request ${request.method} ${request.url}`);
|
|
131
|
+
return {
|
|
132
|
+
cleanup: () => {
|
|
133
|
+
refuseOffTheWire = null;
|
|
134
|
+
server.resetHandlers();
|
|
135
|
+
},
|
|
136
|
+
violation
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
//#endregion
|
|
140
|
+
export { registerStubs };
|
|
141
|
+
|
|
142
|
+
//# sourceMappingURL=shared-stub-server.js.map
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { a as EVIDENCE_DIR } from "./shared-artifacts.js";
|
|
2
|
+
import { t as setupEntry } from "./shared-setup-entry.js";
|
|
3
|
+
import { t as processAlive } from "./shared-process-alive.js";
|
|
4
|
+
import { existsSync, mkdirSync, mkdtempSync, readdirSync, rmSync } from "node:fs";
|
|
5
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
6
|
+
import { tmpdir } from "node:os";
|
|
7
|
+
//#region src/chain/paths/run-folders.ts
|
|
8
|
+
function runOf(entry) {
|
|
9
|
+
const pid = Number(entry.split("-")[0]);
|
|
10
|
+
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
11
|
+
}
|
|
12
|
+
/** Leaves alone every name no run gave: the parent of temp roots is shared with whatever else a user keeps there. */
|
|
13
|
+
function sweepOrphanRoots(parent) {
|
|
14
|
+
for (const entry of readdirSync(parent)) {
|
|
15
|
+
const pid = runOf(entry);
|
|
16
|
+
if (pid !== null && !processAlive(pid)) rmSync(join(parent, entry), {
|
|
17
|
+
force: true,
|
|
18
|
+
recursive: true
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/** Keeps a living run's evidence alone: a finished run's stays until the next run starts, and nothing else is ever kept. */
|
|
23
|
+
function sweepFinishedEvidence(folder) {
|
|
24
|
+
if (!existsSync(folder)) return;
|
|
25
|
+
for (const entry of readdirSync(folder)) {
|
|
26
|
+
const pid = runOf(entry);
|
|
27
|
+
if (pid === null || !processAlive(pid)) rmSync(join(folder, entry), {
|
|
28
|
+
force: true,
|
|
29
|
+
recursive: true
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
//#endregion
|
|
34
|
+
//#region src/chain/paths/temp-root.global-setup.ts
|
|
35
|
+
/** Named by uid: a temp directory users share never holds a folder one of them cannot sweep. */
|
|
36
|
+
const TEMP_ROOTS_PARENT = `jterrazz-test-runs-${process.getuid?.() ?? "user"}`;
|
|
37
|
+
const TEMP_VARIABLES = [
|
|
38
|
+
"TMPDIR",
|
|
39
|
+
"TMP",
|
|
40
|
+
"TEMP"
|
|
41
|
+
];
|
|
42
|
+
function setVariables(values) {
|
|
43
|
+
for (const [name, value] of Object.entries(values)) if (value === void 0) Reflect.deleteProperty(process.env, name);
|
|
44
|
+
else process.env[name] = value;
|
|
45
|
+
}
|
|
46
|
+
function tempRootSetup() {
|
|
47
|
+
return setupEntry(import.meta.dirname, "temp-root");
|
|
48
|
+
}
|
|
49
|
+
function joinedRoot() {
|
|
50
|
+
const current = tmpdir();
|
|
51
|
+
return basename(dirname(current)) === TEMP_ROOTS_PARENT && basename(current).startsWith(`${process.pid}-`);
|
|
52
|
+
}
|
|
53
|
+
function setup() {
|
|
54
|
+
if (joinedRoot()) return () => {};
|
|
55
|
+
const parent = join(tmpdir(), TEMP_ROOTS_PARENT);
|
|
56
|
+
mkdirSync(parent, { recursive: true });
|
|
57
|
+
sweepOrphanRoots(parent);
|
|
58
|
+
sweepFinishedEvidence(resolve(EVIDENCE_DIR));
|
|
59
|
+
const root = mkdtempSync(join(parent, `${process.pid}-`));
|
|
60
|
+
const previous = Object.fromEntries(TEMP_VARIABLES.map((name) => [name, process.env[name]]));
|
|
61
|
+
setVariables(Object.fromEntries(TEMP_VARIABLES.map((name) => [name, root])));
|
|
62
|
+
return () => {
|
|
63
|
+
setVariables(previous);
|
|
64
|
+
rmSync(root, {
|
|
65
|
+
force: true,
|
|
66
|
+
recursive: true
|
|
67
|
+
});
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
//#endregion
|
|
71
|
+
export { setup as n, tempRootSetup as r, TEMP_ROOTS_PARENT as t };
|
|
72
|
+
|
|
73
|
+
//# sourceMappingURL=shared-temp-root.global-setup.js.map
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { t as TEST_SECTION } from "./shared-test-section.js";
|
|
2
|
+
import { labelOf, sectionOf, writeSection } from "@jterrazz/typescript/rules";
|
|
3
|
+
//#region lint/kit/baseline/test-baseline.ts
|
|
4
|
+
const ROUTES_KEY = "routes/unreached";
|
|
5
|
+
const ROUTES_LEDGER = `${labelOf(TEST_SECTION)} › ${ROUTES_KEY}`;
|
|
6
|
+
const heldOf = (root) => sectionOf(root, "test") ?? {};
|
|
7
|
+
const withoutRoutes = (counts) => Object.fromEntries(Object.entries(counts).filter(([key]) => key !== ROUTES_KEY));
|
|
8
|
+
function writeBaseline(root, counts) {
|
|
9
|
+
const held = heldOf(root)[ROUTES_KEY];
|
|
10
|
+
writeSection(root, TEST_SECTION, held === void 0 ? { ...counts } : {
|
|
11
|
+
...counts,
|
|
12
|
+
[ROUTES_KEY]: held
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
const heldUnreached = (root) => heldOf(root)[ROUTES_KEY] ?? 0;
|
|
16
|
+
function recordUnreached(root, count) {
|
|
17
|
+
const others = withoutRoutes(heldOf(root));
|
|
18
|
+
writeSection(root, TEST_SECTION, count === 0 ? others : {
|
|
19
|
+
...others,
|
|
20
|
+
[ROUTES_KEY]: count
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
//#endregion
|
|
24
|
+
export { writeBaseline as i, heldUnreached as n, recordUnreached as r, ROUTES_LEDGER as t };
|
|
25
|
+
|
|
26
|
+
//# sourceMappingURL=shared-test-baseline.js.map
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { t as isRecord } from "./shared-json-shape.js";
|
|
2
|
+
import { readSettings, readSettingsSync, refusalBlock } from "@jterrazz/typescript/rules";
|
|
3
|
+
//#region lint/kit/project/test-section.ts
|
|
4
|
+
const TEST_SECTION = "test";
|
|
5
|
+
const UNREAD = Object.freeze({
|
|
6
|
+
collections: [],
|
|
7
|
+
methodless: [],
|
|
8
|
+
production: { allow: {} }
|
|
9
|
+
});
|
|
10
|
+
function wordsOf(value) {
|
|
11
|
+
return Array.isArray(value) && value.every((entry) => typeof entry === "string") ? value : void 0;
|
|
12
|
+
}
|
|
13
|
+
function reasonsOf(value) {
|
|
14
|
+
if (!isRecord(value)) return;
|
|
15
|
+
const reasons = {};
|
|
16
|
+
for (const [name, reason] of Object.entries(value)) {
|
|
17
|
+
if (typeof reason !== "string" || reason.trim() === "") return;
|
|
18
|
+
reasons[name] = reason;
|
|
19
|
+
}
|
|
20
|
+
return reasons;
|
|
21
|
+
}
|
|
22
|
+
const BASE_PATH = /^(?:\/[a-z0-9][\w.~-]*)+$/u;
|
|
23
|
+
const isMountPath = (value) => BASE_PATH.test(value);
|
|
24
|
+
function basePathOf(value) {
|
|
25
|
+
if (typeof value !== "string") return;
|
|
26
|
+
if (!isMountPath(value)) throw new Error(refusalBlock({
|
|
27
|
+
fix: "write the path the routes are mounted under from the root, with no slash at its end: test: { basePath: '/api' }",
|
|
28
|
+
found: `jterrazz.config.ts: test.basePath ${JSON.stringify(value)} is no mount path`,
|
|
29
|
+
read: "docs/reference/linting.md#test-settings-section",
|
|
30
|
+
why: "The base path is cut from every route before its subject folder is derived, so it is read as a route prefix is written: one leading slash, lowercase segments, no trailing slash."
|
|
31
|
+
}));
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
/** A key of the wrong shape is read as its default, never half-used: the type check refuses it first; a base path the type lets through and no route can carry is refused here. */
|
|
35
|
+
function testSettingsOf(test) {
|
|
36
|
+
const section = isRecord(test) ? test : {};
|
|
37
|
+
const allow = isRecord(section.production) ? reasonsOf(section.production.allow) : void 0;
|
|
38
|
+
const basePath = basePathOf(section.basePath);
|
|
39
|
+
return {
|
|
40
|
+
collections: wordsOf(section.collections) ?? UNREAD.collections,
|
|
41
|
+
methodless: Object.keys(reasonsOf(section.methodless) ?? {}),
|
|
42
|
+
production: allow === void 0 ? UNREAD.production : { allow },
|
|
43
|
+
...typeof section.routes === "string" ? { routes: section.routes } : {},
|
|
44
|
+
...basePath === void 0 ? {} : { basePath }
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/** Every pass is sync, so the file is loaded synchronously, once per process; the runtime reads the same resolution, so the run and the checker agree. A file that does not resolve is no settings: the toolchain already reports it. */
|
|
48
|
+
const testSettingsAtSync = (root) => testSettingsOf(readSettingsSync(root).resolved.sections[TEST_SECTION]);
|
|
49
|
+
/** Whether the unit's settings list this package's preset, the one whose checker judges every specs tree under it. */
|
|
50
|
+
const listsTestPreset = (root) => readSettingsSync(root).resolved.presets.some((preset) => preset.package === "@jterrazz/test");
|
|
51
|
+
async function testSettingsAt(root) {
|
|
52
|
+
const { resolved } = await readSettings(root);
|
|
53
|
+
return testSettingsOf(resolved.sections[TEST_SECTION]);
|
|
54
|
+
}
|
|
55
|
+
//#endregion
|
|
56
|
+
export { testSettingsAtSync as i, listsTestPreset as n, testSettingsAt as r, TEST_SECTION as t };
|
|
57
|
+
|
|
58
|
+
//# sourceMappingURL=shared-test-section.js.map
|