@jterrazz/test 18.0.4 → 18.2.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # @jterrazz/test
2
2
 
3
- A testing framework on vitest for whole products: HTTP APIs, background jobs, CLIs, websites, native mobile apps, rendered components, modules against real databases, and the lint rules a project ships. You declare what a test needs (Postgres, a built binary, a real Chromium, an iOS simulator), and the framework starts it, isolates it per worker and cleans it up. Each test is one sentence: a few setups, one action, and the whole result compared with `expect()`.
3
+ A testing framework on vitest for whole products: HTTP APIs, background jobs, CLIs, websites, running deployments, native mobile apps, rendered components, modules against real databases, and the lint rules a project ships. You declare what a test needs (Postgres, a built binary, a real Chromium, an iOS simulator), and the framework starts it, isolates it per worker and cleans it up. Each test is one sentence: a few setups, one action, and the whole result compared with `expect()`.
4
4
 
5
5
  ```typescript
6
6
  // specs/api/users/list/shape.spec.ts
@@ -52,6 +52,7 @@ An ES module project on Node 24, vitest 5 and `@jterrazz/typescript` 11 is requi
52
52
  | [Component](docs/guides/component-specs.md) | A React component, hook or DOM function in a real Chromium | `component.render()` |
53
53
  | [Website](docs/guides/website-specs.md) | A served page, fetched or driven in a browser | `specification.website()` |
54
54
  | [Mobile](docs/guides/mobile-specs.md) | A native app on an iOS simulator | `specification.mobile()` |
55
+ | [Deployment](docs/guides/deployment-specs.md) | A running deployment, probed over the network, no browser | `specification.deployment()` |
55
56
  | [API](docs/guides/api-specs.md) | An HTTP app, through complete requests | `specification.api()` |
56
57
  | [Job](docs/guides/job-specs.md) | A pipeline triggered by name | `specification.job()` |
57
58
  | [CLI](docs/guides/cli-specs.md) | A binary in a fresh working directory, often as a YAML document | `specification.cli()` |
@@ -3211,10 +3211,11 @@ async function waitUntil(predicate, options = {}) {
3211
3211
  }
3212
3212
  //#endregion
3213
3213
  //#region src/index.browser.ts
3214
- /** The seven constructors, present and refusing in a page. */
3214
+ /** The eight constructors, present and refusing in a page. */
3215
3215
  const specification = {
3216
3216
  api: () => refuse("specification.api()"),
3217
3217
  cli: () => refuse("specification.cli()"),
3218
+ deployment: () => refuse("specification.deployment()"),
3218
3219
  integration: () => refuse("specification.integration()"),
3219
3220
  job: () => refuse("specification.job()"),
3220
3221
  lint: () => refuse("specification.lint()"),
package/dist/checker.js CHANGED
@@ -1767,16 +1767,16 @@ function firstLine(text) {
1767
1767
  }
1768
1768
  const GRAMMAR = {
1769
1769
  request: {
1770
- line: /^(?<method>GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS) \/\S*/u,
1770
+ line: /^(?<method>GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS) (?:\/|https?:\/\/)/u,
1771
1771
  teaching: (name) => ({
1772
- fix: `start ${name} with its request line, \`GET /path\` or another method`,
1772
+ fix: `start ${name} with its request line, \`GET /path\`, \`GET https://host/path\` or another method`,
1773
1773
  found: `the first line of ${name} is no request line`
1774
1774
  })
1775
1775
  },
1776
1776
  response: {
1777
- line: /^HTTP\/\d(?:\.\d)? \d{3}\b/u,
1777
+ line: /^(?:HTTP\/\d(?:\.\d)? \d{3}\b|unreachable: \S)/u,
1778
1778
  teaching: (name) => ({
1779
- fix: `start ${name} with its status line, \`HTTP/1.1 200 OK\` or the status it expects`,
1779
+ fix: `start ${name} with its status line, \`HTTP/1.1 200 OK\` or the status it expects, or \`unreachable: <reason>\``,
1780
1780
  found: `the first line of ${name} is no status line`
1781
1781
  })
1782
1782
  }
package/dist/config.d.ts CHANGED
@@ -22,7 +22,7 @@ type TestSettings = {
22
22
  specifications?: readonly string[];
23
23
  };
24
24
  /** The helpers a project is built by, named by their specification. */
25
- type TestSpecification = 'api' | 'cli' | 'component' | 'integration' | 'job' | 'lint' | 'mobile' | 'unit' | 'website';
25
+ type TestSpecification = 'api' | 'cli' | 'component' | 'deployment' | 'integration' | 'job' | 'lint' | 'mobile' | 'unit' | 'website';
26
26
  /** What a declared project states beyond its derived one: the global setups it runs, the options its helper takes, and why. */
27
27
  type TestProject = {
28
28
  readonly reason: string;
package/dist/config.js CHANGED
@@ -13,16 +13,23 @@ var standard_default = defineStandard({ test: {
13
13
  "api",
14
14
  "cli",
15
15
  "component",
16
+ "deployment",
16
17
  "integration",
17
18
  "job",
18
19
  "lint",
19
20
  "mobile",
20
21
  "website"
21
22
  ],
22
- projects: { unit: {
23
- isolate: false,
24
- reason: "a module test leaves no state behind in a module, so a worker runs its files in one module graph"
25
- } }
23
+ projects: {
24
+ lint: {
25
+ isolate: false,
26
+ reason: "a lint spec lays its own fixture tree and runs the linters as processes of their own, so a worker runs its files in one module graph"
27
+ },
28
+ unit: {
29
+ isolate: false,
30
+ reason: "a module test leaves no state behind in a module, so a worker runs its files in one module graph"
31
+ }
32
+ }
26
33
  } });
27
34
  //#endregion
28
35
  //#region lint/kit/project/loaded-packages.ts
package/dist/index.d.ts CHANGED
@@ -279,6 +279,15 @@ type ServerResponse = {
279
279
  body: unknown;
280
280
  /** Response headers as a flat key-value map. */
281
281
  headers: Record<string, string>;
282
+ /** Why no response came back (`refused`, `timeout`, `dns`, `tls CERT_HAS_EXPIRED` ...): set only when the exchange never answered, and then the status is 0, the headers empty and the body null. */
283
+ unreachable?: string | undefined;
284
+ };
285
+ /** What update mode writes into a golden that states nothing yet: which headers it records, and whether a golden stating no body keeps stating none. */
286
+ type ResponseRecording = {
287
+ /** A golden that states no body keeps stating none: the body is claimed by writing it. */
288
+ readonly bodyKeptAbsent: boolean;
289
+ /** The header names a fresh golden records, those the response carries. */
290
+ readonly headers: readonly string[];
282
291
  };
283
292
  /** Read-only accessor for an HTTP response, compared by `expect(...).toMatch(...)`. */
284
293
  declare class ResponseAccessor {
@@ -288,10 +297,14 @@ declare class ResponseAccessor {
288
297
  readonly captures: CaptureScope;
289
298
  /** Response headers as a flat, lower-cased key-value map. */
290
299
  readonly headers: Record<string, string>;
300
+ /** @internal */
301
+ readonly recording: ResponseRecording;
291
302
  readonly status: number;
292
303
  /** @internal */
293
304
  readonly testDir: string;
294
- constructor(response: ServerResponse, testDir: string, captures?: CaptureScope);
305
+ /** Why the exchange never answered, a golden's `unreachable: <reason>` line; undefined when it answered. */
306
+ readonly unreachable: string | undefined;
307
+ constructor(response: ServerResponse, testDir: string, captures?: CaptureScope, recording?: ResponseRecording);
295
308
  }
296
309
  //#endregion
297
310
  //#region src/specifications/api/server/server.port.d.ts
@@ -465,7 +478,7 @@ type Methods<C extends Composition, P extends SpecificationParams, Self> = Inter
465
478
  type ChainOf<C extends Composition, P extends SpecificationParams, Self> = Pick<Methods<C, P, Self>, Extract<Names<C['actions']> | Names<C['capabilities']>, keyof Methods<C, P, Self>>> & SharingOf<C, Self>;
466
479
  //#endregion
467
480
  //#region src/specifications/api/api.composition.d.ts
468
- declare const composition$7: {
481
+ declare const composition$8: {
469
482
  readonly actions: readonly [Action<"request", [source: string | DiskFile<".http">], HttpResult, RequestSignature, {
470
483
  server: ServerPort;
471
484
  }, ChainConfig>, Action<"get", [path: string, options?: RouteOptionsAny], HttpResult, RequestSignature, {
@@ -508,7 +521,7 @@ declare const composition$7: {
508
521
  };
509
522
  };
510
523
  /** The chain `specification.api()` hands out; the app answers in this process (`server-sees-declared-services`). */
511
- interface ApiSpecification<DatabaseKey extends string = string> extends ChainOf<typeof composition$7, {
524
+ interface ApiSpecification<DatabaseKey extends string = string> extends ChainOf<typeof composition$8, {
512
525
  databaseKey: DatabaseKey;
513
526
  services: Record<string, unknown>;
514
527
  }, ApiSpecification<DatabaseKey>> {}
@@ -720,7 +733,7 @@ interface PrepareSignature extends ChainSignature {
720
733
  }
721
734
  //#endregion
722
735
  //#region src/specifications/cli/cli.composition.d.ts
723
- declare const composition$6: {
736
+ declare const composition$7: {
724
737
  readonly actions: readonly [Action<"exec", [args?: string | string[], options?: ExecOptions], CliResult, ExecSignature, {
725
738
  command: CliPort;
726
739
  }, CliConfig>, Action<"run", [document: DiskFile<".spec.yaml">, options?: LiterateRunFlags], CliResult, RunSignature$1, Record<never, never>, CliConfig>];
@@ -793,7 +806,7 @@ declare const composition$6: {
793
806
  };
794
807
  };
795
808
  /** The chain `specification.cli()` hands out: a binary run in a fresh, empty working directory, in its own process. */
796
- interface CliSpecification<DatabaseKey extends string = string> extends ChainOf<typeof composition$6, {
809
+ interface CliSpecification<DatabaseKey extends string = string> extends ChainOf<typeof composition$7, {
797
810
  databaseKey: DatabaseKey;
798
811
  services: Record<string, unknown>;
799
812
  }, CliSpecification<DatabaseKey>> {}
@@ -861,6 +874,131 @@ type CliHandle<DatabaseKey extends string = string> = {
861
874
  };
862
875
  declare function startCli<Services extends ServiceRecord>(bin: string, options?: CliSpecificationOptions<Services>): Promise<CliHandle<DatabaseKeys<Services>>>;
863
876
  //#endregion
877
+ //#region src/specifications/deployment/exchange/tls-facts.d.ts
878
+ /** The certificate an HTTPS exchange was presented, and what the client made of it. */
879
+ type TlsFacts = {
880
+ /** The names the certificate is valid for (its subjectAltName), each without its `DNS:` or `IP Address:` prefix. */
881
+ readonly altNames: readonly string[];
882
+ /** The code the trust check refused with (`DEPTH_ZERO_SELF_SIGNED_CERT`, `CERT_HAS_EXPIRED`, `ERR_TLS_CERT_ALTNAME_INVALID` ...), undefined when trusted. */
883
+ readonly authorizationError: string | undefined;
884
+ /** Whether the certificate chains to a trusted authority and covers the URL's host. */
885
+ readonly authorized: boolean;
886
+ /** Whole days from the exchange to `validTo`, negative once it has expired. */
887
+ readonly daysRemaining: number;
888
+ /** The issuer's common name, or its organisation when it states none. */
889
+ readonly issuer: string;
890
+ /** The TLS version negotiated (`TLSv1.3`). */
891
+ readonly protocol: string | undefined;
892
+ /** The subject's common name; empty when it states none. */
893
+ readonly subject: string;
894
+ readonly validFrom: Date;
895
+ readonly validTo: Date;
896
+ /** Whether the certificate is valid for `host`, wildcards read as a client reads them. */
897
+ covers: (host: string) => boolean;
898
+ };
899
+ //#endregion
900
+ //#region src/specifications/deployment/exchange/exchange.result.d.ts
901
+ /** What one exchange with a deployment hands back: the response, or why none came, and the certificate it was presented. */
902
+ declare class ExchangeResult extends BaseResult {
903
+ private readonly answer;
904
+ private readonly recording;
905
+ /** The certificate an https exchange was presented and the trust check's verdict; undefined over http, or when no handshake completed. */
906
+ readonly tls: TlsFacts | undefined;
907
+ constructor(options: BaseResultOptions & {
908
+ recording: ResponseRecording;
909
+ response: ServerResponse;
910
+ tls: TlsFacts | undefined;
911
+ });
912
+ /** The whole response (status, headers, body), or `unreachable: <reason>`, compared whole: `toMatch(<.http text>)` or `toMatch(file(...))`. */
913
+ get response(): ResponseAccessor;
914
+ }
915
+ //#endregion
916
+ //#region src/specifications/deployment/exchange/request.action.d.ts
917
+ type ExchangeSignature = ChainSignature & {
918
+ readonly chain: {
919
+ /** Send a GET to a URL, written whole or as a path from the specification's `url`, and resolve with what came back. */
920
+ get: (target: string) => Promise<ExchangeResult>;
921
+ /** Send a complete request: its `.http` text written in the test, or `file('./__requests__/<name>.http')`; its first line names the URL whole or a path. */
922
+ request: (source: DiskFile<'.http'> | string) => Promise<ExchangeResult>;
923
+ };
924
+ };
925
+ //#endregion
926
+ //#region src/specifications/deployment/exchange/connect.capability.d.ts
927
+ type ConnectChain<Self> = {
928
+ /** Open the socket on this address instead of the URL's host and port, as curl's `--connect-to`: the Host header and the TLS server name stay the URL's; a later call wins. */
929
+ connect: (address: string) => Self;
930
+ };
931
+ interface ConnectSignature extends ChainSignature {
932
+ readonly chain: ConnectChain<this['self']>;
933
+ }
934
+ //#endregion
935
+ //#region src/specifications/deployment/deployment.composition.d.ts
936
+ declare const composition$6: {
937
+ readonly actions: readonly [Action<"request", [source: string | DiskFile<".http">], ExchangeResult, ExchangeSignature, Record<never, never>, DeploymentConfig>, Action<"get", [target: string], ExchangeResult, ExchangeSignature, Record<never, never>, DeploymentConfig>];
938
+ readonly capabilities: readonly [Capability<"connect", string | null, [address: string], ConnectSignature, Record<never, never>, never, ChainConfig>, Capability<"headers", Readonly<Record<string, string>>, [headers: Record<string, string>], HeadersSignature, Record<never, never>, "headers", ChainConfig>, Sharing];
939
+ readonly options: {
940
+ ca: {
941
+ group: "Constructor option";
942
+ name: string;
943
+ probe: string;
944
+ };
945
+ insecure: {
946
+ group: "Constructor option";
947
+ name: string;
948
+ probe: string;
949
+ };
950
+ recordedHeaders: {
951
+ group: "Constructor option";
952
+ name: string;
953
+ probe: string;
954
+ };
955
+ timeout: {
956
+ group: "Constructor option";
957
+ name: string;
958
+ probe: string;
959
+ };
960
+ url: {
961
+ group: "Constructor option";
962
+ name: string;
963
+ probe: string;
964
+ };
965
+ };
966
+ };
967
+ /** The chain `specification.deployment()` hands out: one HTTP exchange with a running deployment, over the network. */
968
+ interface DeploymentSpecification extends ChainOf<typeof composition$6, {
969
+ databaseKey: string;
970
+ services: Record<string, unknown>;
971
+ }, DeploymentSpecification> {}
972
+ //#endregion
973
+ //#region src/specifications/deployment/deployment.handle.d.ts
974
+ /** Options for {@link startDeployment | specification.deployment}. */
975
+ type DeploymentSpecificationOptions = {
976
+ /** A certificate authority trusted beside the ones node trusts: its PEM text, a value like any string. */
977
+ ca?: string;
978
+ /** Accept a certificate the trust check refuses (self-signed, expired, another host's); `result.tls` still says why. Default `false`. */
979
+ insecure?: boolean;
980
+ /** The headers update mode records into a golden that states none yet, those the response carries. Default `['content-type', 'location']`. */
981
+ recordedHeaders?: readonly string[];
982
+ /** The milliseconds one exchange may take, connection to last byte, before it reads `unreachable: timeout`. Default `5000`. */
983
+ timeout?: number;
984
+ /** The base URL a path is read from (`.get('/health')`); a URL written whole needs none. */
985
+ url?: string;
986
+ };
987
+ type DeploymentConfig = ChainConfig & {
988
+ baseUrl: string | undefined;
989
+ ca: string | undefined;
990
+ insecure: boolean;
991
+ recording: ResponseRecording;
992
+ timeout: number;
993
+ };
994
+ /** What `specification.deployment()` returns: `const { deployment, cleanup } = ...` (`no-runner-alias`). */
995
+ type DeploymentHandle = {
996
+ /** Starts nothing, so releases nothing: passed to `afterAll` like every specification's. */
997
+ cleanup: () => Promise<void>;
998
+ deployment: DeploymentSpecification;
999
+ };
1000
+ declare function startDeployment(options?: DeploymentSpecificationOptions): Promise<DeploymentHandle>;
1001
+ //#endregion
864
1002
  //#region src/specifications/integration/call.result.d.ts
865
1003
  type CallOutcome = {
866
1004
  error: unknown;
@@ -1552,10 +1690,11 @@ type WebsiteHandle = {
1552
1690
  declare function startWebsite<Services extends ServiceRecord>(options: WebsiteSpecificationOptions<Services>): Promise<WebsiteHandle>;
1553
1691
  //#endregion
1554
1692
  //#region src/specifications/specification.d.ts
1555
- /** The seven constructors, and only seven: the framework's whole entry surface. */
1693
+ /** The eight constructors, and only eight: the framework's whole entry surface. */
1556
1694
  type Specification = {
1557
1695
  api: typeof startApi;
1558
1696
  cli: typeof startCli;
1697
+ deployment: typeof startDeployment;
1559
1698
  integration: typeof startIntegration;
1560
1699
  job: typeof startJob;
1561
1700
  lint: typeof startLint;
@@ -1912,5 +2051,5 @@ declare module 'vitest' {
1912
2051
  }
1913
2052
  }
1914
2053
  //#endregion
1915
- export { type ApiHandle, type ApiSpecification, type ApiSpecificationOptions, type BaseResult, type BrowserConsoleMessage, type BrowserLinkElement, type BrowserMetaElement, type BrowserOpenOptions, type BrowserPage, type BrowserPort, type CallResult, type CaptureScope, type CliEnv, type CliHandle, type CliInput, type CliOutput, type CliPort, type CliResult, type CliSpecification, type CliSpecificationOptions, type ComponentChain, type ComponentScenario, type ComponentUi, type ComponentVisitor, type ContainerAccessor, type DeviceOpenOptions, type DevicePort, type DeviceScreen, type DeviceTimeouts, type DirectoryAccessor, type DiskDirectory, type DiskFile, type DiskTarget, type DockerSpecConfig, type DomMount, type ElementKind, type ElementMatch, type ExecOptions, type FetchResult, type FileAccessor, type FileAt, type FilesystemAccessor, type HonoApp, type HttpResult, type IntegrationHandle, type IntegrationSpecification, type IntegrationSpecificationOptions, type Job, type JobHandle, type JobResult, type JobSpecification, type JobSpecificationOptions, type JsonAccessor, type LandmarkKind, type LintConfig, type LintHandle, type LintResult, type LintRules, type LintRunOptions, type LintSpecification, type LintSpecificationOptions, type LiterateRunFlags, type LiterateServeRegistration, type MatchFixtureOptions, Matcher, type MatcherKind, type MobileBackendOptions, type MobileElementKind, type MobileElementMatch, type MobileHandle, type MobileScenario, type MobileSpecification, type MobileSpecificationOptions, type MobileVisitor, type MockOfOptions, type MockPort, type PackedPackage, type PageResult, type PinnedClock, type PositionedCases, type PositionedRuleTester, type PrepareStep, type RelativePath, type RenderResult, type RenderSubject, type RenderedText, type ResponseAccessor, type ScreenNode, type ScreenResult, type ServerPort, type ServerRequestOptions, type ServerResponse, type ServerSpec, type SpecDocument, type SpecEnvToken, type SpecFileAssertion, type SpecFixture, type SpecRun, type SpecServeEntry, type SpecStream, type TableAccessor, type Target, type TargetOptions, type TextAccessor, type VisitScenario, type WaitUntilOptions, type WebsiteBackendOptions, type WebsiteHandle, type WebsiteSpecification, type WebsiteSpecificationOptions, type WebsiteVisitor, directory, file, match };
2054
+ export { type ApiHandle, type ApiSpecification, type ApiSpecificationOptions, type BaseResult, type BrowserConsoleMessage, type BrowserLinkElement, type BrowserMetaElement, type BrowserOpenOptions, type BrowserPage, type BrowserPort, type CallResult, type CaptureScope, type CliEnv, type CliHandle, type CliInput, type CliOutput, type CliPort, type CliResult, type CliSpecification, type CliSpecificationOptions, type ComponentChain, type ComponentScenario, type ComponentUi, type ComponentVisitor, type ContainerAccessor, type DeploymentHandle, type DeploymentSpecification, type DeploymentSpecificationOptions, type DeviceOpenOptions, type DevicePort, type DeviceScreen, type DeviceTimeouts, type DirectoryAccessor, type DiskDirectory, type DiskFile, type DiskTarget, type DockerSpecConfig, type DomMount, type ElementKind, type ElementMatch, type ExchangeResult, type ExecOptions, type FetchResult, type FileAccessor, type FileAt, type FilesystemAccessor, type HonoApp, type HttpResult, type IntegrationHandle, type IntegrationSpecification, type IntegrationSpecificationOptions, type Job, type JobHandle, type JobResult, type JobSpecification, type JobSpecificationOptions, type JsonAccessor, type LandmarkKind, type LintConfig, type LintHandle, type LintResult, type LintRules, type LintRunOptions, type LintSpecification, type LintSpecificationOptions, type LiterateRunFlags, type LiterateServeRegistration, type MatchFixtureOptions, Matcher, type MatcherKind, type MobileBackendOptions, type MobileElementKind, type MobileElementMatch, type MobileHandle, type MobileScenario, type MobileSpecification, type MobileSpecificationOptions, type MobileVisitor, type MockOfOptions, type MockPort, type PackedPackage, type PageResult, type PinnedClock, type PositionedCases, type PositionedRuleTester, type PrepareStep, type RelativePath, type RenderResult, type RenderSubject, type RenderedText, type ResponseAccessor, type ScreenNode, type ScreenResult, type ServerPort, type ServerRequestOptions, type ServerResponse, type ServerSpec, type SpecDocument, type SpecEnvToken, type SpecFileAssertion, type SpecFixture, type SpecRun, type SpecServeEntry, type SpecStream, type TableAccessor, type Target, type TargetOptions, type TextAccessor, type TlsFacts, type VisitScenario, type WaitUntilOptions, type WebsiteBackendOptions, type WebsiteHandle, type WebsiteSpecification, type WebsiteSpecificationOptions, type WebsiteVisitor, directory, file, match };
1916
2055
  //# sourceMappingURL=index.d.ts.map