@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,287 @@
|
|
|
1
|
+
//#region src/stubs/exchange.d.ts
|
|
2
|
+
/** An observed outgoing request, reduced to what stub matchers inspect. */
|
|
3
|
+
type MatchableRequest = {
|
|
4
|
+
/** Parsed JSON body when the payload is JSON, the raw text otherwise, or `null` when absent. */
|
|
5
|
+
body: unknown;
|
|
6
|
+
/** Request headers, keyed by lowercased header name. */
|
|
7
|
+
headers: Record<string, string>;
|
|
8
|
+
/** Uppercased HTTP method of the observed request. */
|
|
9
|
+
method: string;
|
|
10
|
+
/** The request URL: fully-qualified, or origin-relative for the stub backend. */
|
|
11
|
+
url: string;
|
|
12
|
+
};
|
|
13
|
+
/** The request half of a stub: an absolute URL, or a path form (`/articles/{{uuid}}`) matching any origin. */
|
|
14
|
+
type StubRequest = {
|
|
15
|
+
/** Adapter name: `http` | `openai` | `anthropic`. */
|
|
16
|
+
adapter: string;
|
|
17
|
+
/** HTTP method to match. `*` matches any method. */
|
|
18
|
+
method: string;
|
|
19
|
+
/** Absolute URL (string | RegExp) or an any-origin path form (`/articles/{{uuid}}`). */
|
|
20
|
+
url: RegExp | string;
|
|
21
|
+
/** Optional request matcher; the stub only fires if this returns true. */
|
|
22
|
+
match?: ((request: MatchableRequest) => boolean) | undefined;
|
|
23
|
+
/** Header names, case-insensitive, whose presence keeps the stub from firing; `http.*` writes them from `without.headers`. */
|
|
24
|
+
absentHeaders?: readonly string[] | undefined;
|
|
25
|
+
/** Transform raw data into a provider-specific response envelope. */
|
|
26
|
+
wrap: (data: unknown) => StubResponse;
|
|
27
|
+
};
|
|
28
|
+
/** The response half of a stub. */
|
|
29
|
+
type StubResponse = {
|
|
30
|
+
/** HTTP status code (default: 200). */
|
|
31
|
+
status?: number;
|
|
32
|
+
/** Response body: an object is JSON, a string text, `null` empty, a `StreamBody` arrives in pieces. */
|
|
33
|
+
body: unknown;
|
|
34
|
+
headers?: Record<string, string> | undefined;
|
|
35
|
+
/** Delay in ms before responding (for timeout testing). */
|
|
36
|
+
delay?: number | undefined;
|
|
37
|
+
/** A transport failure instead of a reply: `fetch` rejects, as no status code can express. */
|
|
38
|
+
transport?: 'network-error' | undefined;
|
|
39
|
+
};
|
|
40
|
+
/** Each adapter serialises an object as JSON, so a stream declared as a plain value would arrive as `{}`: the tag tells them apart. */
|
|
41
|
+
declare const STREAM_BODY = "@jterrazz/test:stream";
|
|
42
|
+
/** A body that arrives in pieces, as `http.stream()` and `http.sse()` build it. */
|
|
43
|
+
type StreamBody = {
|
|
44
|
+
/** The pieces, written in order. */
|
|
45
|
+
chunks: readonly string[];
|
|
46
|
+
/** The `content-type` the stream is served under. */
|
|
47
|
+
contentType: string;
|
|
48
|
+
/** Milliseconds between two chunks. Default 0: everything at once. */
|
|
49
|
+
delayBetweenChunks: number;
|
|
50
|
+
kind: typeof STREAM_BODY;
|
|
51
|
+
};
|
|
52
|
+
/** A response computed from the observed request at serve time. */
|
|
53
|
+
type StubResponder = (request: MatchableRequest) => StubResponse;
|
|
54
|
+
/** A fixed `StubResponse`, or a `StubResponder` evaluated per served request. */
|
|
55
|
+
type StubResponseValue = StubResponder | StubResponse;
|
|
56
|
+
//#endregion
|
|
57
|
+
//#region src/stubs/stub.d.ts
|
|
58
|
+
/** A declared external interaction: what to match and what to reply. */
|
|
59
|
+
type Stub = {
|
|
60
|
+
/** Which outgoing call this stub speaks for. */
|
|
61
|
+
request: StubRequest;
|
|
62
|
+
/** A fixed reply, or a function of the observed request. */
|
|
63
|
+
response: StubResponseValue;
|
|
64
|
+
/** How many times it may serve; omitted is unlimited. `0` with `required` is a never stub: docs/reference/stubs.md#never-a-call-that-must-not-happen. */
|
|
65
|
+
times?: number;
|
|
66
|
+
/** Fail the chain unless it was requested: exactly `times` times when set, so `times: 0` fails it once requested. */
|
|
67
|
+
required?: boolean;
|
|
68
|
+
};
|
|
69
|
+
/** A composite of stubs; `.with()` derives a variant without touching the original. */
|
|
70
|
+
type Stubs = {
|
|
71
|
+
/** The flattened stubs, in selection order. */
|
|
72
|
+
readonly stubs: readonly Stub[];
|
|
73
|
+
/** Overrides replace base stubs of the same route and are prepended, so a more specific override wins first match. */
|
|
74
|
+
with: (...overrides: (Stub | Stub[] | Stubs)[]) => Stubs;
|
|
75
|
+
};
|
|
76
|
+
/** Any accepted stub input: one, a list, or a composite. */
|
|
77
|
+
type StubInput = Stub | Stub[] | Stubs;
|
|
78
|
+
declare function defineStub(stub: Stub): Stub;
|
|
79
|
+
/** Compose stubs, lists and composites, order preserved; `.with()` derives a scenario. */
|
|
80
|
+
declare function defineStubs(...items: StubInput[]): Stubs;
|
|
81
|
+
//#endregion
|
|
82
|
+
//#region lint/kit/project/tokens.d.ts
|
|
83
|
+
declare const TOKEN_KINDS: readonly ["any", "base64", "date", "duration", "email", "float", "hex", "int", "ip", "iso8601", "number", "path", "port", "semver", "sha", "string", "time", "ulid", "url", "uuid", "workdir"];
|
|
84
|
+
type TokenKind = (typeof TOKEN_KINDS)[number];
|
|
85
|
+
//#endregion
|
|
86
|
+
//#region src/expectations/tokens/match.d.ts
|
|
87
|
+
/** The frozen token vocabulary (rule `no-unknown-token`) plus the code-only kinds. */
|
|
88
|
+
type MatcherKind = 'any' | 'base64' | 'date' | 'duration' | 'email' | 'float' | 'hex' | 'includes' | 'int' | 'ip' | 'iso8601' | 'number' | 'path' | 'port' | 'ref' | 'regex' | 'semver' | 'sha' | 'string' | 'time' | 'ulid' | 'url' | 'uuid' | 'workdir';
|
|
89
|
+
/** A dynamic-value matcher, created through the `match` factories. */
|
|
90
|
+
declare class Matcher {
|
|
91
|
+
/** For kind 'includes': the substring the actual value must contain. */
|
|
92
|
+
readonly includes?: string | undefined;
|
|
93
|
+
readonly kind: MatcherKind;
|
|
94
|
+
/** For kind 'ref': the capture to assert inequality against. */
|
|
95
|
+
readonly notRef?: string | undefined;
|
|
96
|
+
/** For kind 'ref': the capture name. */
|
|
97
|
+
readonly refName?: string | undefined;
|
|
98
|
+
/** For kind 'regex': the pattern the actual value must match. */
|
|
99
|
+
readonly regex?: RegExp | undefined;
|
|
100
|
+
constructor(kind: MatcherKind, options?: {
|
|
101
|
+
includes?: string | undefined;
|
|
102
|
+
notRef?: string | undefined;
|
|
103
|
+
refName?: string | undefined;
|
|
104
|
+
regex?: RegExp | undefined;
|
|
105
|
+
});
|
|
106
|
+
/** Placeholder-style rendering used in failure diffs and serialization. */
|
|
107
|
+
toString(): string;
|
|
108
|
+
toJSON(): string;
|
|
109
|
+
}
|
|
110
|
+
type TypedMatcher = () => Matcher;
|
|
111
|
+
/** Dynamic-value matchers, the code-side mirror of the `{{token}}` grammar (`no-unknown-token`). */
|
|
112
|
+
type MatchFactories = Record<TokenKind, TypedMatcher> & {
|
|
113
|
+
includes: (substring: string) => Matcher;
|
|
114
|
+
ref: (name: string, options?: {
|
|
115
|
+
not?: string;
|
|
116
|
+
}) => Matcher;
|
|
117
|
+
regex: (regex: RegExp) => Matcher;
|
|
118
|
+
};
|
|
119
|
+
declare const match: MatchFactories;
|
|
120
|
+
//#endregion
|
|
121
|
+
//#region src/stubs/kinds/text-filter.d.ts
|
|
122
|
+
/** A text filter on a provider request builder: a string is exact, a `RegExp` or `match.*` loosens it. */
|
|
123
|
+
type TextFilter = Matcher | RegExp | string;
|
|
124
|
+
//#endregion
|
|
125
|
+
//#region src/stubs/kinds/anthropic.d.ts
|
|
126
|
+
type AnthropicMessagesFilter = {
|
|
127
|
+
model?: TextFilter;
|
|
128
|
+
system?: TextFilter;
|
|
129
|
+
user?: TextFilter;
|
|
130
|
+
tools?: string[];
|
|
131
|
+
};
|
|
132
|
+
/** Init options for a streamed Anthropic reply. */
|
|
133
|
+
type AnthropicStreamInit = {
|
|
134
|
+
/** Milliseconds between two events. Default 0: every event at once. */
|
|
135
|
+
delay?: number;
|
|
136
|
+
};
|
|
137
|
+
/** Anthropic API stub helpers. */
|
|
138
|
+
declare const anthropic: {
|
|
139
|
+
/** Match Messages API calls, optionally through a gateway URL; a string filter is exact equality. */
|
|
140
|
+
messages(filter?: AnthropicMessagesFilter, url?: string): StubRequest;
|
|
141
|
+
/** A stub the chain fails on if the subject reaches this endpoint: `http.never()`. */
|
|
142
|
+
never(request: StubRequest): Stub;
|
|
143
|
+
/** Response: data wrapped in the Messages format. */
|
|
144
|
+
reply(data: unknown): StubResponse;
|
|
145
|
+
/** Response: the text streamed as server-sent events, one `text_delta` per piece. */
|
|
146
|
+
stream(pieces: readonly string[], init?: AnthropicStreamInit): StubResponse;
|
|
147
|
+
/** Response: return an Anthropic error. */
|
|
148
|
+
error(status: number, message?: string): StubResponse;
|
|
149
|
+
/** Response: simulate a timeout. */
|
|
150
|
+
timeout(): StubResponse;
|
|
151
|
+
};
|
|
152
|
+
//#endregion
|
|
153
|
+
//#region src/stubs/kinds/http-filter.d.ts
|
|
154
|
+
/** Request filters; a request matches when every provided field matches. */
|
|
155
|
+
type HttpStubFilter = {
|
|
156
|
+
/** An object is a deep subset match (leaves may be `match.*`); a string is containment, a RegExp `test()`s the raw body. */
|
|
157
|
+
body?: object | RegExp | string;
|
|
158
|
+
/** Header subset. Names are case-insensitive; string = exact value, RegExp = `test()`. */
|
|
159
|
+
headers?: Record<string, RegExp | string>;
|
|
160
|
+
/** Query-param subset. string = exact value, RegExp = `test()`. */
|
|
161
|
+
query?: Record<string, RegExp | string>;
|
|
162
|
+
/** Headers the request must not carry; a name present with any value keeps the stub from firing. */
|
|
163
|
+
without?: {
|
|
164
|
+
headers?: readonly string[];
|
|
165
|
+
};
|
|
166
|
+
};
|
|
167
|
+
//#endregion
|
|
168
|
+
//#region src/stubs/kinds/http.d.ts
|
|
169
|
+
/** Init options for a streamed reply. */
|
|
170
|
+
type HttpStreamInit = {
|
|
171
|
+
/** The `content-type` the stream is served under. Default `application/octet-stream`. */
|
|
172
|
+
contentType?: string;
|
|
173
|
+
/** Milliseconds between two chunks. Default 0: every chunk at once. */
|
|
174
|
+
delay?: number;
|
|
175
|
+
/** Response headers, merged over the builder's own. */
|
|
176
|
+
headers?: Record<string, string>;
|
|
177
|
+
/** HTTP status code. Default 200. */
|
|
178
|
+
status?: number;
|
|
179
|
+
};
|
|
180
|
+
/** One server-sent event, as `http.sse()` frames it. */
|
|
181
|
+
type SseEvent = {
|
|
182
|
+
/** The `data:` payload. An object is serialised as JSON; a string is sent as-is. */
|
|
183
|
+
data: unknown;
|
|
184
|
+
/** The `event:` name. Omitted for an unnamed (default `message`) event. */
|
|
185
|
+
event?: string;
|
|
186
|
+
/** The `id:` the client echoes as `Last-Event-ID` when it reconnects. */
|
|
187
|
+
id?: string;
|
|
188
|
+
/** The `retry:` hint, in milliseconds. */
|
|
189
|
+
retry?: number;
|
|
190
|
+
};
|
|
191
|
+
/** Init options shared by the response builders. */
|
|
192
|
+
type HttpResponseInit = {
|
|
193
|
+
/** Delay in ms before responding (for timeout testing). */
|
|
194
|
+
delay?: number;
|
|
195
|
+
/** Response headers, merged over the builder's own. */
|
|
196
|
+
headers?: Record<string, string>;
|
|
197
|
+
/** HTTP status code. */
|
|
198
|
+
status?: number;
|
|
199
|
+
};
|
|
200
|
+
/** Generic HTTP stub helpers: an absolute URL, or a path form (`/articles/{{uuid}}`) matching any origin. */
|
|
201
|
+
declare const http: {
|
|
202
|
+
any(url: RegExp | string, filter?: HttpStubFilter): StubRequest;
|
|
203
|
+
delete(url: RegExp | string, filter?: HttpStubFilter): StubRequest;
|
|
204
|
+
get(url: RegExp | string, filter?: HttpStubFilter): StubRequest;
|
|
205
|
+
patch(url: RegExp | string, filter?: HttpStubFilter): StubRequest;
|
|
206
|
+
post(url: RegExp | string, filter?: HttpStubFilter): StubRequest;
|
|
207
|
+
put(url: RegExp | string, filter?: HttpStubFilter): StubRequest;
|
|
208
|
+
/** A stub the chain fails on if the subject reaches its route: `required` with `times: 0`. */
|
|
209
|
+
never(request: StubRequest): Stub;
|
|
210
|
+
/** Response: a body-less reply (204 by default). */
|
|
211
|
+
empty(status?: number): StubResponse;
|
|
212
|
+
/** Response: an error status. Without a body, `{ error: 'HTTP <status>' }`. */
|
|
213
|
+
error(status: number, body?: unknown): StubResponse;
|
|
214
|
+
/** Response: a JSON body (200 by default). */
|
|
215
|
+
json(body: unknown, init?: HttpResponseInit): StubResponse;
|
|
216
|
+
/** Response: no response at all; `fetch` rejects, as when no server is running. */
|
|
217
|
+
unreachable(): StubResponse;
|
|
218
|
+
/** Response: a body that arrives in pieces, written in order. */
|
|
219
|
+
stream(chunks: readonly string[], init?: HttpStreamInit): StubResponse;
|
|
220
|
+
/** Response: a server-sent event stream, served as `text/event-stream`. */
|
|
221
|
+
sse(events: readonly SseEvent[], init?: Omit<HttpStreamInit, "contentType">): StubResponse;
|
|
222
|
+
/** Response: a text body, served as `text/plain` (200 by default). */
|
|
223
|
+
text(body: string, init?: HttpResponseInit): StubResponse;
|
|
224
|
+
};
|
|
225
|
+
//#endregion
|
|
226
|
+
//#region src/stubs/kinds/openai.d.ts
|
|
227
|
+
type OpenAIChatFilter = {
|
|
228
|
+
model?: TextFilter;
|
|
229
|
+
system?: TextFilter;
|
|
230
|
+
user?: TextFilter;
|
|
231
|
+
tools?: string[];
|
|
232
|
+
temperature?: number;
|
|
233
|
+
};
|
|
234
|
+
type OpenAIResponsesFilter = {
|
|
235
|
+
model?: TextFilter;
|
|
236
|
+
system?: TextFilter;
|
|
237
|
+
user?: TextFilter;
|
|
238
|
+
tools?: string[];
|
|
239
|
+
};
|
|
240
|
+
type OpenAIStreamInit = {
|
|
241
|
+
/** The API whose stream is served: Chat Completions (default) or Responses. */
|
|
242
|
+
api?: 'chat' | 'responses';
|
|
243
|
+
/** Milliseconds between two events. Default 0: every event at once. */
|
|
244
|
+
delay?: number;
|
|
245
|
+
/** The tokens the stream reports it spent: a Chat Completions stream ends on a usage chunk, as `stream_options.include_usage` asks, and a Responses stream carries them in its completed response. Default 10 and 10. */
|
|
246
|
+
usage?: {
|
|
247
|
+
completionTokens: number;
|
|
248
|
+
promptTokens: number;
|
|
249
|
+
};
|
|
250
|
+
};
|
|
251
|
+
/** OpenAI API stub helpers. */
|
|
252
|
+
declare const openai: {
|
|
253
|
+
/** Match Chat Completions calls, at `url` where the client reaches another host; a string filter is exact equality, a RegExp or `match.includes` loosens it. */
|
|
254
|
+
chat(filter?: OpenAIChatFilter, url?: string): StubRequest;
|
|
255
|
+
/** Match Responses API calls; a JSON file is wrapped in the Responses envelope. */
|
|
256
|
+
responses(filter?: OpenAIResponsesFilter, url?: string): StubRequest;
|
|
257
|
+
/** A stub the chain fails on if the subject reaches this endpoint: `http.never()`. */
|
|
258
|
+
never(request: StubRequest): Stub;
|
|
259
|
+
/** Response: data wrapped in the Chat Completions format. */
|
|
260
|
+
reply(data: unknown): StubResponse;
|
|
261
|
+
/** Response: the text streamed as server-sent events; `api` picks Chat Completions (default) or Responses. */
|
|
262
|
+
stream(pieces: readonly string[], init?: OpenAIStreamInit): StubResponse;
|
|
263
|
+
/** Response: return an OpenAI error. */
|
|
264
|
+
error(status: number, message?: string): StubResponse;
|
|
265
|
+
/** Response: return malformed content. */
|
|
266
|
+
malformed(content: string): StubResponse;
|
|
267
|
+
/** Response: simulate a timeout. */
|
|
268
|
+
timeout(): StubResponse;
|
|
269
|
+
};
|
|
270
|
+
//#endregion
|
|
271
|
+
//#region src/stubs/intercept/intercept.d.ts
|
|
272
|
+
/** Disposed at the end of the declaring scope: handlers dropped, an unmatched request thrown. */
|
|
273
|
+
type InterceptScope = AsyncDisposable;
|
|
274
|
+
/** What a scope may state about the network it declares. */
|
|
275
|
+
type InterceptOptions = {
|
|
276
|
+
/** The base a relative request resolves against for the life of the scope; under node `fetch('/api/posts')` cannot parse. */
|
|
277
|
+
origin?: string;
|
|
278
|
+
};
|
|
279
|
+
/** The module-scope network double; awaited, so the network double listens before the subject runs. */
|
|
280
|
+
type Intercept = ((stubs: StubInput, options?: InterceptOptions) => Promise<InterceptScope>) & ((request: StubRequest, response: StubResponder | StubResponse, options?: InterceptOptions) => Promise<InterceptScope>);
|
|
281
|
+
//#endregion
|
|
282
|
+
//#region src/stubs/delivery/same-process/intercept.d.ts
|
|
283
|
+
/** The module-scope network double, on msw's node server. */
|
|
284
|
+
declare const intercept: Intercept;
|
|
285
|
+
//#endregion
|
|
286
|
+
export { StubRequest as C, StubResponseValue as E, StreamBody as S, StubResponse as T, StubInput as _, openai as a, defineStubs as b, SseEvent as c, anthropic as d, TextFilter as f, Stub as g, match as h, InterceptScope as i, http as l, MatcherKind as m, Intercept as n, HttpResponseInit as o, Matcher as p, InterceptOptions as r, HttpStreamInit as s, intercept as t, HttpStubFilter as u, Stubs as v, StubResponder as w, MatchableRequest as x, defineStub as y };
|
|
287
|
+
//# sourceMappingURL=shared-index.stubs.d.ts.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
//#region lint/kit/project/json-shape.ts
|
|
2
|
+
/** `JSON.parse` answers `any`: these read a parsed value field by field, and a field of another shape reads as absent. */
|
|
3
|
+
function isRecord(value) {
|
|
4
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5
|
+
}
|
|
6
|
+
/** What a text that is no JSON stands for is the caller's to say. */
|
|
7
|
+
function parsedJson(text, otherwise) {
|
|
8
|
+
try {
|
|
9
|
+
return JSON.parse(text);
|
|
10
|
+
} catch {
|
|
11
|
+
return otherwise;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
//#endregion
|
|
15
|
+
export { parsedJson as n, isRecord as t };
|
|
16
|
+
|
|
17
|
+
//# sourceMappingURL=shared-json-shape.js.map
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
//#region src/visitor/targets/roles.ts
|
|
2
|
+
const stated = (options) => options?.exact === void 0 ? {} : { exact: options.exact };
|
|
3
|
+
const named = (kind) => (name, options) => ({
|
|
4
|
+
kind,
|
|
5
|
+
name,
|
|
6
|
+
...stated(options)
|
|
7
|
+
});
|
|
8
|
+
const optionallyNamed = (kind) => (name, options) => ({
|
|
9
|
+
kind,
|
|
10
|
+
...name === void 0 ? {} : { name },
|
|
11
|
+
...stated(options)
|
|
12
|
+
});
|
|
13
|
+
/** A button (or element with the button role), by accessible name. */
|
|
14
|
+
const button = named("button");
|
|
15
|
+
/** A form field, by label. */
|
|
16
|
+
const field = named("field");
|
|
17
|
+
/** A heading, by accessible name. */
|
|
18
|
+
const heading = named("heading");
|
|
19
|
+
/** A link, by accessible name. */
|
|
20
|
+
const link = named("link");
|
|
21
|
+
/** An element containing the given text. */
|
|
22
|
+
const content = named("text");
|
|
23
|
+
/** An option of a select or listbox, by label; asserted by presence, since a collapsed select's options have no box. */
|
|
24
|
+
const option = named("option");
|
|
25
|
+
/** A modal or non-modal `<dialog>`, by accessible name. A closed one is absent. */
|
|
26
|
+
const dialog = optionallyNamed("dialog");
|
|
27
|
+
/** A live region announcing a result: `role="status"`, an `<output>`. */
|
|
28
|
+
const status = optionallyNamed("status");
|
|
29
|
+
/** A table, by its caption or accessible name. */
|
|
30
|
+
const table = optionallyNamed("table");
|
|
31
|
+
/** A row of a table or grid, by the text of its cells. */
|
|
32
|
+
const row = optionallyNamed("row");
|
|
33
|
+
/** An item of a list, by its text. */
|
|
34
|
+
const listitem = optionallyNamed("listitem");
|
|
35
|
+
/** The escape hatch: an element by `data-testid`. Prefer user-facing targets. */
|
|
36
|
+
const testId = (id) => ({
|
|
37
|
+
kind: "testId",
|
|
38
|
+
name: id
|
|
39
|
+
});
|
|
40
|
+
//#endregion
|
|
41
|
+
//#region src/visitor/targets/landmarks.ts
|
|
42
|
+
/** The `banner` landmark: the page header. */
|
|
43
|
+
const banner = optionallyNamed("banner");
|
|
44
|
+
/** The `complementary` landmark: an `<aside>`, a sidebar. */
|
|
45
|
+
const complementary = optionallyNamed("complementary");
|
|
46
|
+
/** The `contentinfo` landmark: the page footer. */
|
|
47
|
+
const contentinfo = optionallyNamed("contentinfo");
|
|
48
|
+
/** The `form` landmark: a form carrying an accessible name. */
|
|
49
|
+
const form = optionallyNamed("form");
|
|
50
|
+
/** The `main` landmark: the primary content of the document. */
|
|
51
|
+
const main = optionallyNamed("main");
|
|
52
|
+
/** The `navigation` landmark: a `<nav>`. Name it when a page has several. */
|
|
53
|
+
const navigation = optionallyNamed("navigation");
|
|
54
|
+
/** The `region` landmark: a `<section>` carrying an accessible name. */
|
|
55
|
+
const region = optionallyNamed("region");
|
|
56
|
+
/** The `search` landmark. */
|
|
57
|
+
const search = optionallyNamed("search");
|
|
58
|
+
/** Restrict a target to the inside of another: `click(within(navigation(), link('Articles')))`. */
|
|
59
|
+
const within = (scope, target) => ({
|
|
60
|
+
...target,
|
|
61
|
+
scope: target.scope ? within(scope, target.scope) : scope
|
|
62
|
+
});
|
|
63
|
+
function scopeChain(target) {
|
|
64
|
+
const chain = [];
|
|
65
|
+
for (let current = target; current; current = current.scope) chain.unshift(current);
|
|
66
|
+
return chain;
|
|
67
|
+
}
|
|
68
|
+
//#endregion
|
|
69
|
+
export { testId as S, listitem as _, main as a, status as b, scopeChain as c, button as d, content as f, link as g, heading as h, form as i, search as l, field as m, complementary as n, navigation as o, dialog as p, contentinfo as r, region as s, banner as t, within as u, option as v, table as x, row as y };
|
|
70
|
+
|
|
71
|
+
//# sourceMappingURL=shared-landmarks.js.map
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { t as isRecord } from "./shared-json-shape.js";
|
|
2
|
+
import { u as VITEST_ARTIFACTS_DIR } from "./shared-artifacts.js";
|
|
3
|
+
import { mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { randomUUID } from "node:crypto";
|
|
6
|
+
//#region src/expectations/in-place/update.ts
|
|
7
|
+
/** `TEST_UPDATE=1` or vitest's `-u` / `--update`. */
|
|
8
|
+
function shouldUpdateSnapshots() {
|
|
9
|
+
if (process.env.TEST_UPDATE === "1") return true;
|
|
10
|
+
return process.argv.includes("-u") || process.argv.includes("--update");
|
|
11
|
+
}
|
|
12
|
+
const UPDATE_HINT = "Run with TEST_UPDATE=1 (or vitest -u) to create it.";
|
|
13
|
+
const FROZEN_HINT = "This golden is `{ frozen }`: update mode never writes it. Write the file by hand, or drop `{ frozen }` for one TEST_UPDATE=1 run and put it back.";
|
|
14
|
+
function missingHint(frozen) {
|
|
15
|
+
return frozen ? FROZEN_HINT : UPDATE_HINT;
|
|
16
|
+
}
|
|
17
|
+
//#endregion
|
|
18
|
+
//#region src/specifications/api/routes/ledger.ts
|
|
19
|
+
const isLedger = (value) => isRecord(value) && Array.isArray(value.reached) && (value.declared === void 0 || Array.isArray(value.declared));
|
|
20
|
+
function entriesOf(directory) {
|
|
21
|
+
try {
|
|
22
|
+
return readdirSync(directory, { withFileTypes: true });
|
|
23
|
+
} catch {
|
|
24
|
+
return [];
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const ROUTES_DIR = `${VITEST_ARTIFACTS_DIR}/routes`;
|
|
28
|
+
function shardLedgersDir(shard) {
|
|
29
|
+
return `${ROUTES_DIR}/shard-${shard.index}-${shard.count}`;
|
|
30
|
+
}
|
|
31
|
+
function clearLedgers(directory) {
|
|
32
|
+
rmSync(directory, {
|
|
33
|
+
force: true,
|
|
34
|
+
recursive: true
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
function writeLedger(directory, ledger) {
|
|
38
|
+
mkdirSync(directory, { recursive: true });
|
|
39
|
+
writeFileSync(join(directory, `${randomUUID()}.json`), `${JSON.stringify(ledger)}\n`);
|
|
40
|
+
}
|
|
41
|
+
const parsed = (path) => JSON.parse(readFileSync(path, "utf8"));
|
|
42
|
+
function readLedgers(directory) {
|
|
43
|
+
return entriesOf(directory).filter((entry) => entry.name.endsWith(".json")).map((entry) => parsed(join(directory, entry.name))).filter((ledger) => isLedger(ledger));
|
|
44
|
+
}
|
|
45
|
+
const PARTIAL_MARK = "partial";
|
|
46
|
+
function markPartial(directory) {
|
|
47
|
+
mkdirSync(directory, { recursive: true });
|
|
48
|
+
writeFileSync(join(directory, PARTIAL_MARK), "");
|
|
49
|
+
}
|
|
50
|
+
function measure(ledgers) {
|
|
51
|
+
const tables = ledgers.map((ledger) => ledger.declared);
|
|
52
|
+
return {
|
|
53
|
+
declared: tables.every((table) => table !== void 0) ? tables.flat() : void 0,
|
|
54
|
+
reached: ledgers.flatMap((ledger) => ledger.reached)
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
async function routesLedger() {
|
|
58
|
+
try {
|
|
59
|
+
const { inject } = await import("vitest");
|
|
60
|
+
return inject("routesLedger");
|
|
61
|
+
} catch {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
//#endregion
|
|
66
|
+
export { readLedgers as a, writeLedger as c, measure as i, missingHint as l, clearLedgers as n, routesLedger as o, markPartial as r, shardLedgersDir as s, ROUTES_DIR as t, shouldUpdateSnapshots as u };
|
|
67
|
+
|
|
68
|
+
//# sourceMappingURL=shared-ledger.js.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { t as invariant } from "./shared-refusal.js";
|
|
2
|
+
//#region src/chain/listened-port.ts
|
|
3
|
+
/** Listens, on `host` when one is named, and answers the port it got; a port another server holds rejects with its error. */
|
|
4
|
+
async function listenedPort(server, { host, port = 0 } = {}) {
|
|
5
|
+
await new Promise((resolve, reject) => {
|
|
6
|
+
server.once("error", reject);
|
|
7
|
+
server.listen(port, host, () => {
|
|
8
|
+
server.off("error", reject);
|
|
9
|
+
resolve();
|
|
10
|
+
});
|
|
11
|
+
});
|
|
12
|
+
const address = server.address();
|
|
13
|
+
if (address === null || typeof address === "string") throw invariant("a server listening on a TCP port answered no port");
|
|
14
|
+
return address.port;
|
|
15
|
+
}
|
|
16
|
+
//#endregion
|
|
17
|
+
export { listenedPort as t };
|
|
18
|
+
|
|
19
|
+
//# sourceMappingURL=shared-listened-port.js.map
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { n as refusal } from "./shared-refusal.js";
|
|
2
|
+
import { c as isRelativePath, s as SpecSyntaxError } from "./shared-spec-grammar.js";
|
|
3
|
+
import { i as readSpecFile, o as dedent } from "./shared-spec-document.js";
|
|
4
|
+
import { readFileSync } from "node:fs";
|
|
5
|
+
import { basename, isAbsolute, resolve } from "node:path";
|
|
6
|
+
//#region src/disk/spec-path.ts
|
|
7
|
+
const DISK_KEY = "@jterrazz/test:disk";
|
|
8
|
+
const DISK = Symbol.for(DISK_KEY);
|
|
9
|
+
/** Name one file, by its path from the spec file. */
|
|
10
|
+
function file(path) {
|
|
11
|
+
requireRelativePath("file", path);
|
|
12
|
+
if (path.endsWith("/")) {
|
|
13
|
+
const bare = path.replace(/\/+$/u, "");
|
|
14
|
+
throw new TypeError(refusal("disk-named-by-relative-path-at-runtime", {
|
|
15
|
+
fix: `name the directory with directory(${JSON.stringify(path)}), or the file without its /: file(${JSON.stringify(bare)})`,
|
|
16
|
+
found: `file(${JSON.stringify(path)}), a path ending in /, which is a directory's`
|
|
17
|
+
}));
|
|
18
|
+
}
|
|
19
|
+
return diskValue("file", path);
|
|
20
|
+
}
|
|
21
|
+
function diskValue(brand, path) {
|
|
22
|
+
const value = {
|
|
23
|
+
[DISK]: brand,
|
|
24
|
+
path
|
|
25
|
+
};
|
|
26
|
+
Object.defineProperty(value, Symbol.match, { value: () => {
|
|
27
|
+
throw new TypeError(refusal("one-verb-per-meaning", {
|
|
28
|
+
fix: `wrap the string to golden it, \`expect(text(value)).toMatch(${brand}(...))\`, or compare it to a value, \`toMatch('...')\``,
|
|
29
|
+
found: `toMatch(${brand}(${JSON.stringify(path)})) on a string, a value with no golden`
|
|
30
|
+
}));
|
|
31
|
+
} });
|
|
32
|
+
return Object.freeze(value);
|
|
33
|
+
}
|
|
34
|
+
/** Name one directory, by its path from the spec file, ending in `/`. */
|
|
35
|
+
function directory(path) {
|
|
36
|
+
requireRelativePath("directory", path);
|
|
37
|
+
if (!path.endsWith("/")) throw new TypeError(refusal("disk-named-by-relative-path-at-runtime", {
|
|
38
|
+
fix: `end it in /: directory(${JSON.stringify(`${path}/`)}), or name a file with file(${JSON.stringify(path)})`,
|
|
39
|
+
found: `directory(${JSON.stringify(path)}), a path that does not end in /`
|
|
40
|
+
}));
|
|
41
|
+
return diskValue("directory", path);
|
|
42
|
+
}
|
|
43
|
+
function isDiskFile(value) {
|
|
44
|
+
return brandOf(value) === "file";
|
|
45
|
+
}
|
|
46
|
+
function isDiskDirectory(value) {
|
|
47
|
+
return brandOf(value) === "directory";
|
|
48
|
+
}
|
|
49
|
+
function isDiskTarget(value) {
|
|
50
|
+
return brandOf(value) !== void 0;
|
|
51
|
+
}
|
|
52
|
+
function brandOf(value) {
|
|
53
|
+
if (value === null || typeof value !== "object") return;
|
|
54
|
+
const brand = Reflect.get(value, DISK);
|
|
55
|
+
return brand === "file" || brand === "directory" ? brand : void 0;
|
|
56
|
+
}
|
|
57
|
+
function spelled(value) {
|
|
58
|
+
if (isDiskTarget(value)) return `${brandOf(value)}(${JSON.stringify(value.path)})`;
|
|
59
|
+
return typeof value === "string" ? JSON.stringify(value) : String(value);
|
|
60
|
+
}
|
|
61
|
+
function requireDiskFile(verb, value, extension = "") {
|
|
62
|
+
if (!isDiskFile(value)) throw new TypeError(refusal("disk-named-by-relative-path-at-runtime", {
|
|
63
|
+
fix: `name one file: ${verb}(file('./${exampleOf(verb, extension)}'))`,
|
|
64
|
+
found: isDiskDirectory(value) ? `${verb}(${spelled(value)}), a directory where one file is read` : `${verb}(${spelled(value)}), a value where a file is read`
|
|
65
|
+
}));
|
|
66
|
+
if (!value.path.endsWith(extension)) throw new TypeError(refusal("disk-named-by-relative-path-at-runtime", {
|
|
67
|
+
fix: `name the ${extension} file ${verb}() reads: ${verb}(file('./${exampleOf(verb, extension)}'))`,
|
|
68
|
+
found: `${verb}(${spelled(value)}), a file not ending in ${extension}`
|
|
69
|
+
}));
|
|
70
|
+
return value;
|
|
71
|
+
}
|
|
72
|
+
function requireDiskTarget(verb, value) {
|
|
73
|
+
if (!isDiskTarget(value)) throw new TypeError(refusal("disk-named-by-relative-path-at-runtime", {
|
|
74
|
+
fix: `name the disk: ${verb}(file('./<path>')) for a file, ${verb}(directory('./<path>/')) for a directory`,
|
|
75
|
+
found: `${verb}(${spelled(value)}), a value where the disk is read`
|
|
76
|
+
}));
|
|
77
|
+
return value;
|
|
78
|
+
}
|
|
79
|
+
function exampleOf(verb, extension) {
|
|
80
|
+
return {
|
|
81
|
+
fixture: "__fixtures__/config.json",
|
|
82
|
+
request: "__requests__/create.http",
|
|
83
|
+
run: "case.spec.yaml",
|
|
84
|
+
seed: "__seeds__/rows.sql"
|
|
85
|
+
}[verb] ?? `__expected__/output${extension || ".txt"}`;
|
|
86
|
+
}
|
|
87
|
+
function asRelative(path) {
|
|
88
|
+
const bare = typeof path === "string" ? path.replace(/^\/+/u, "") : "";
|
|
89
|
+
return `./${bare === "" ? "__expected__/output.txt" : bare}`;
|
|
90
|
+
}
|
|
91
|
+
function requireRelativePath(call, path) {
|
|
92
|
+
if (typeof path !== "string" || !isRelativePath(path)) throw new TypeError(refusal("disk-named-by-relative-path-at-runtime", {
|
|
93
|
+
fix: `start it with ./ or ../, from the spec file: ${call}('${asRelative(path)}')`,
|
|
94
|
+
found: `${call}(${JSON.stringify(path)}), a path that is not relative to the spec file`
|
|
95
|
+
}));
|
|
96
|
+
}
|
|
97
|
+
//#endregion
|
|
98
|
+
//#region src/disk/resolved-spec-path.ts
|
|
99
|
+
function resolveSpecPath(verb, path, specDir) {
|
|
100
|
+
requireRelativePath(verb, path);
|
|
101
|
+
return resolve(specDir, path);
|
|
102
|
+
}
|
|
103
|
+
//#endregion
|
|
104
|
+
//#region src/expectations/in-place/render.ts
|
|
105
|
+
function escapeTemplate(text) {
|
|
106
|
+
return text.replaceAll("\\", String.raw`\\`).replaceAll("`", "\\`").replaceAll("${", "\\${");
|
|
107
|
+
}
|
|
108
|
+
function renderText(text, indent) {
|
|
109
|
+
if (!text.includes("\n")) return `\`${escapeTemplate(text)}\``;
|
|
110
|
+
const inner = `${indent} `;
|
|
111
|
+
const lines = text.split("\n");
|
|
112
|
+
const last = String(lines.pop());
|
|
113
|
+
const block = `\n${lines.map((line) => line === "" ? "" : `${inner}${line}`).join("\n")}\n${last === "" ? indent : `${inner}${last}`}`;
|
|
114
|
+
return dedent(block) === text ? `\`${escapeTemplate(block)}\`` : JSON.stringify(text);
|
|
115
|
+
}
|
|
116
|
+
const NUMBER_MARK = "@@in-place-number:";
|
|
117
|
+
const GROUPED_FROM = 1e4;
|
|
118
|
+
function grouped(value) {
|
|
119
|
+
return typeof value === "number" && Number.isSafeInteger(value) && Math.abs(value) >= GROUPED_FROM ? `${NUMBER_MARK}${String(value).replaceAll(/\B(?=(?:\d{3})+$)/gu, "_")}` : value;
|
|
120
|
+
}
|
|
121
|
+
const MARKED = new RegExp(`"${NUMBER_MARK}(?<digits>-?[\\d_]+)"`, "gu");
|
|
122
|
+
const literal = (value, space) => JSON.stringify(value, (_key, item) => grouped(item), space)?.replaceAll(MARKED, "$<digits>");
|
|
123
|
+
function renderValue(value, indent) {
|
|
124
|
+
return (literal(value, 4) ?? "undefined").split("\n").join(`\n${indent}`);
|
|
125
|
+
}
|
|
126
|
+
function renderRows(rows, indent) {
|
|
127
|
+
if (rows.length === 0) return "[]";
|
|
128
|
+
return `[\n${rows.map((row) => `${indent} [${row.map((cell) => literal(cell) ?? "null").join(", ")}],`).join("\n")}\n${indent}]`;
|
|
129
|
+
}
|
|
130
|
+
//#endregion
|
|
131
|
+
//#region src/specifications/cli/documents/literate-plugin.ts
|
|
132
|
+
const SPEC_FILE = /\.spec\.yaml(?:\?.*)?$/u;
|
|
133
|
+
function grammarRefusal(error, displayPath) {
|
|
134
|
+
return new Error(refusal({
|
|
135
|
+
fix: "write the key the grammar names; the reason prints the accepted set",
|
|
136
|
+
found: `${displayPath}:${error.line}: ${error.reason}`,
|
|
137
|
+
read: "docs/reference/linting.md#document-follows-its-grammar",
|
|
138
|
+
why: "A spec document follows one grammar, read by the parser the runner and the linter share: closed keys, `description:` and `runs:` required, `command:` and `exit:` in every run."
|
|
139
|
+
}), { cause: error });
|
|
140
|
+
}
|
|
141
|
+
function readDocument(content, displayPath) {
|
|
142
|
+
try {
|
|
143
|
+
return readSpecFile(content, displayPath);
|
|
144
|
+
} catch (error) {
|
|
145
|
+
throw error instanceof SpecSyntaxError ? grammarRefusal(error, displayPath) : error;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function literateModule(content, filePath, runnerPath) {
|
|
149
|
+
const { description } = readDocument(content, filePath).document;
|
|
150
|
+
return [
|
|
151
|
+
"import { test } from 'vitest';",
|
|
152
|
+
`import { cli } from ${JSON.stringify(runnerPath)};`,
|
|
153
|
+
"",
|
|
154
|
+
`test(${JSON.stringify(description)}, async () => {`,
|
|
155
|
+
` await cli.run({ [Symbol.for(${JSON.stringify(DISK_KEY)})]: 'file', path: ${JSON.stringify(`./${basename(filePath)}`)} });`,
|
|
156
|
+
"});",
|
|
157
|
+
""
|
|
158
|
+
].join("\n");
|
|
159
|
+
}
|
|
160
|
+
function cleanId(id) {
|
|
161
|
+
return id.split("?")[0] ?? id;
|
|
162
|
+
}
|
|
163
|
+
function literate(options) {
|
|
164
|
+
const include = options.include ?? ["**/*.spec.yaml"];
|
|
165
|
+
let root = process.cwd();
|
|
166
|
+
return {
|
|
167
|
+
config: () => ({ test: { include } }),
|
|
168
|
+
configResolved: (resolved) => {
|
|
169
|
+
({root} = resolved);
|
|
170
|
+
},
|
|
171
|
+
enforce: "pre",
|
|
172
|
+
load: (id) => {
|
|
173
|
+
if (!SPEC_FILE.test(id)) return null;
|
|
174
|
+
const filePath = cleanId(id);
|
|
175
|
+
const runnerPath = isAbsolute(options.runner) ? options.runner : resolve(root, options.runner);
|
|
176
|
+
return literateModule(readFileSync(filePath, "utf8"), filePath, runnerPath);
|
|
177
|
+
},
|
|
178
|
+
name: "jterrazz-test:literate"
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
//#endregion
|
|
182
|
+
export { renderValue as a, file as c, isDiskTarget as d, requireDiskFile as f, renderText as i, isDiskDirectory as l, literate as n, resolveSpecPath as o, requireDiskTarget as p, renderRows as r, directory as s, grammarRefusal as t, isDiskFile as u };
|
|
183
|
+
|
|
184
|
+
//# sourceMappingURL=shared-literate-plugin.js.map
|