@jterrazz/test 9.1.0 → 9.2.1

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
- Declarative testing framework for APIs, jobs, and CLIs. Three constructors — `specification.api()`, `specification.jobs()`, `specification.cli()` — and specs that read as sentences: given → action → assertions. The vitest test name is the spec's description; all assertions go through `expect()` with auto-registered, subject-typed matchers.
3
+ Declarative testing framework for APIs, jobs, CLIs, and websites. Four constructors — `specification.api()`, `specification.jobs()`, `specification.cli()`, `specification.website()` — and specs that read as sentences: given → action → assertions. The vitest test name is the spec's description; all assertions go through `expect()` with auto-registered, subject-typed matchers.
4
4
 
5
5
  ```bash
6
6
  npm install -D @jterrazz/test vitest
@@ -76,9 +76,38 @@ test('builds the project', async () => {
76
76
  });
77
77
  ```
78
78
 
79
- Actions are **terminal**: `.request()`, `.get()`, `.trigger()`, `.exec()` execute the spec and resolve to a precisely typed result. There is no `.run()`, no label, and no `.spawn()`.
79
+ ### Website testing (browser)
80
80
 
81
- ## The three constructors
81
+ ```typescript
82
+ // specs/website/website.specification.ts
83
+ import { specification } from '@jterrazz/test';
84
+ import { afterAll } from 'vitest';
85
+
86
+ export const { cleanup, website } = await specification.website({
87
+ server: { command: 'node specs/fixtures/website-app/server.mjs', ready: '/' },
88
+ });
89
+
90
+ afterAll(cleanup);
91
+ ```
92
+
93
+ ```typescript
94
+ // specs/website/visit/head.test.ts
95
+ import { expect, test } from 'vitest';
96
+ import { website } from '../website.specification.js';
97
+
98
+ test('captures the full head surface of a rendered page', async () => {
99
+ // Given - the fixture homepage
100
+ const result = await website.visit('/');
101
+
102
+ // Then - one golden covers title, canonical, alternates, and metas
103
+ expect(result.status).toBe(200);
104
+ expect(result.head).toMatch('home.head.json');
105
+ });
106
+ ```
107
+
108
+ Actions are **terminal**: `.request()`, `.get()`, `.trigger()`, `.exec()`, `.fetch()`, `.visit()` execute the spec and resolve to a precisely typed result. There is no `.run()`, no label, and no `.spawn()`.
109
+
110
+ ## The four constructors
82
111
 
83
112
  One constructor per tested interface, each returning a record destructured with its canonical name:
84
113
 
@@ -87,6 +116,7 @@ One constructor per tested interface, each returning a record destructured with
87
116
  | `specification.api(options)` | `{ api, cleanup, docker, orchestrator }` | `.request(file)`, `.get()`, `.post()`, `.put()`, `.delete()` |
88
117
  | `specification.jobs(options)` | `{ jobs, cleanup, orchestrator }` | `.trigger(name)` |
89
118
  | `specification.cli(bin, options)` | `{ cli, cleanup, docker, orchestrator }` | `.exec(args, { waitFor?, timeout? }?)` |
119
+ | `specification.website(options)` | `{ website, cleanup, url }` | `.fetch(path)`, `.visit(path, scenario?)` |
90
120
 
91
121
  ### `specification.api({ services, server, mode?, root? })`
92
122
 
@@ -146,35 +176,57 @@ export const { cli, cleanup } = await specification.cli('my-migrate-tool', {
146
176
  const result = await cli.seed('legacy-schema.sql').exec('up');
147
177
  ```
148
178
 
179
+ ### `specification.website({ server?, url?, external?, root? })`
180
+
181
+ Tests a rendered website: `.fetch(path)` for a raw HTTP exchange (redirects never followed), `.visit(path, scenario?)` for a page rendered in a real chromium. Exactly one of `server` (start the site locally — a free port injected as `PORT`, polled on `ready`) or `url` (target a running site) is required.
182
+
183
+ ```typescript
184
+ export const { website, cleanup } = await specification.website({
185
+ server: { command: 'node specs/fixtures/website-app/server.mjs', ready: '/' },
186
+ });
187
+
188
+ // Raw exchange — status + headers, redirects surface as 3xx
189
+ const redirect = await website.fetch('/old');
190
+
191
+ // Rendered page, optionally driven by a scenario (the When)
192
+ const page = await website.visit('/', async (visitor) => {
193
+ await visitor.click(link('Articles'));
194
+ });
195
+ ```
196
+
197
+ The handle destructures to `{ website, cleanup, url }` — no `docker`, no `orchestrator`. `.visit()` needs playwright (`npm install -D playwright && npx playwright install chromium`) — an optional peer dependency, only loaded when a spec actually renders a page. Full reference: [docs/11-website.md](docs/11-website.md).
198
+
149
199
  ### Root auto-discovery
150
200
 
151
- When `root` is absent, the framework walks up from the specification file to the first directory containing `docker/compose.test.yaml`, else the first containing `package.json`. Pass `root` only when the convention does not fit. `root` is strictly the **project root** (compose detection + local-bin resolution) — it is not a fixtures root; `.fixture()` resolves its own paths.
201
+ When `root` is absent, the framework walks up from the specification file to the first directory containing `docker/compose.test.yaml`, else the first containing `package.json`. Pass `root` only when the convention does not fit. `root` is strictly the **project root** (compose detection + local-bin resolution, or the cwd of a `specification.website()` server command) — it is not a fixtures root; `.fixture()` resolves its own paths.
152
202
 
153
203
  ## Builder API
154
204
 
155
205
  ### Setup (chainable)
156
206
 
157
- | Method | Facets | Description |
158
- | --------------------------------------- | --------- | ------------------------------------------------------------------------------------------------------ |
159
- | `.seed("file.sql", { database? })` | all | Load SQL from `seeds/` — `database` is the record key (mandatory with ≥ 2 databases, forbidden with 1) |
160
- | `.fixture("file")` | cli | Copy the feature-local `fixtures/file` into the working directory |
161
- | `.fixture("$FIXTURES/name/")` | cli | Spread the shared `specs/fixtures/name/` project into the cwd (trailing `/` = contents; layers) |
162
- | `.env({ KEY: "value" })` | cli | Set env vars on the child (`null` unsets, `$WORKDIR` expands, calls merge) |
163
- | `.headers({ "Accept-Language": "fr" })` | api | Set HTTP request headers (merge on top of `.http` file headers) |
164
- | `.intercept(contract)` | api, jobs | Intercept an outgoing HTTP call with a declared contract |
165
- | `.intercept(trigger, response)` | api, jobs | Inline intercept for one-off cases |
207
+ | Method | Facets | Description |
208
+ | --------------------------------------- | ------------ | ------------------------------------------------------------------------------------------------------ |
209
+ | `.seed("file.sql", { database? })` | all | Load SQL from `seeds/` — `database` is the record key (mandatory with ≥ 2 databases, forbidden with 1) |
210
+ | `.fixture("file")` | cli | Copy the feature-local `fixtures/file` into the working directory |
211
+ | `.fixture("$FIXTURES/name/")` | cli | Spread the shared `specs/fixtures/name/` project into the cwd (trailing `/` = contents; layers) |
212
+ | `.env({ KEY: "value" })` | cli | Set env vars on the child (`null` unsets, `$WORKDIR` expands, calls merge) |
213
+ | `.headers({ "Accept-Language": "fr" })` | api, website | Set HTTP request headers (merge on top of `.http` file headers, or on the browser context) |
214
+ | `.intercept(contract)` | api, jobs | Intercept an outgoing HTTP call with a declared contract |
215
+ | `.intercept(trigger, response)` | api, jobs | Inline intercept for one-off cases |
166
216
 
167
217
  ### Actions (terminal)
168
218
 
169
- | Method | Facet | Resolves to | Description |
170
- | ------------------------------------------ | ----- | ------------ | ---------------------------------------------------------------------------------- |
171
- | `.request("create-user.http")` | api | `HttpResult` | Send the COMPLETE request from `requests/<file>` (method, path, headers, raw body) |
172
- | `.get(path)` / `.delete(path)` | api | `HttpResult` | Inline requests for simple cases |
173
- | `.post(path, body?)` / `.put(path, body?)` | api | `HttpResult` | Inline body: plain object, JSON-serialized |
174
- | `.trigger("name")` | jobs | `BaseResult` | Execute a registered job |
175
- | `.exec("args")` | cli | `CliResult` | Run the command |
176
- | `.exec(["build", "start"])` | cli | `CliResult` | Sequence in the same cwd; stops on first non-zero exit |
177
- | `.exec("dev", { waitFor, timeout? })` | cli | `CliResult` | Long-running: resolves at the pattern, killed at `timeout` (default 10 s) |
219
+ | Method | Facet | Resolves to | Description |
220
+ | ------------------------------------------ | ------- | ------------- | ------------------------------------------------------------------------------------- |
221
+ | `.request("create-user.http")` | api | `HttpResult` | Send the COMPLETE request from `requests/<file>` (method, path, headers, raw body) |
222
+ | `.get(path)` / `.delete(path)` | api | `HttpResult` | Inline requests for simple cases |
223
+ | `.post(path, body?)` / `.put(path, body?)` | api | `HttpResult` | Inline body: plain object, JSON-serialized |
224
+ | `.trigger("name")` | jobs | `BaseResult` | Execute a registered job |
225
+ | `.exec("args")` | cli | `CliResult` | Run the command |
226
+ | `.exec(["build", "start"])` | cli | `CliResult` | Sequence in the same cwd; stops on first non-zero exit |
227
+ | `.exec("dev", { waitFor, timeout? })` | cli | `CliResult` | Long-running: resolves at the pattern, killed at `timeout` (default 10 s) |
228
+ | `.fetch(path)` | website | `FetchResult` | One raw HTTP exchange — redirects surface as 3xx, never followed |
229
+ | `.visit(path, scenario?)` | website | `PageResult` | Render the page in a shared chromium; with a scenario, the capture is the final state |
178
230
 
179
231
  One chain = one terminal action; databases reset at the start of every chain. Every cli spec runs in a fresh, empty temp directory.
180
232
 
@@ -322,13 +374,14 @@ These conventions are not just prose: the package ships an oxlint plugin (`@jter
322
374
 
323
375
  ## Requirements
324
376
 
325
- - **Docker** - testcontainers for node mode, docker compose for compose mode; not needed for `sqlite()` or plain cli specs
377
+ - **Docker** - testcontainers for node mode, docker compose for compose mode; not needed for `sqlite()`, plain cli specs, or website specs
326
378
  - **vitest** - peer dependency
379
+ - **playwright** - optional peer dependency, only needed for `.visit()`: `npm install -D playwright && npx playwright install chromium`
327
380
  - **msw** - bundled as a direct dependency (powers `.intercept()`); no separate install
328
381
  - **hono** (or any web framework) - supplied by your project for in-process apps; the adapter only needs an object with a `request()` method, so it is not a peer
329
382
 
330
383
  ## Docs
331
384
 
332
- - Guide (chapters): [docs/README.md](docs/README.md) — getting started, API/jobs/CLI specs, assertions, tokens, contracts, services, conventions, linting
385
+ - Guide (chapters): [docs/README.md](docs/README.md) — getting started, API/jobs/CLI/website specs, assertions, tokens, contracts, services, conventions, linting
333
386
  - API reference: committed under [docs/reference/](docs/reference/) — compiled from source by `npm run docs`
334
387
  - Agent skill: [skills/jterrazz-test/](skills/jterrazz-test/) — mental model, per-facet references, generated rule reference
package/dist/index.d.ts CHANGED
@@ -496,6 +496,116 @@ interface InterceptContract {
496
496
  */
497
497
  declare function defineContract(contract: InterceptContract): InterceptContract;
498
498
  //#endregion
499
+ //#region src/core/ports/browser.port.d.ts
500
+ /**
501
+ * A user-facing element descriptor — pure data, built by the element
502
+ * vocabulary (`button()`, `link()`, `field()`, …) and translated into
503
+ * concrete locators by the browser integration. CSS/XPath selectors are
504
+ * deliberately not expressible: user-facing elements are the only surface.
505
+ */
506
+ interface ElementRef {
507
+ kind: 'button' | 'field' | 'heading' | 'link' | 'testId' | 'text';
508
+ name: string;
509
+ }
510
+ /** A `<link>` element captured from the rendered document's head. */
511
+ interface BrowserLinkElement {
512
+ href: string;
513
+ hreflang?: string;
514
+ rel: string;
515
+ type?: string;
516
+ }
517
+ /** A `<meta>` element captured from the rendered document's head. */
518
+ interface BrowserMetaElement {
519
+ content: string;
520
+ name?: string;
521
+ property?: string;
522
+ }
523
+ /** A console message emitted while the page loaded or the scenario ran. */
524
+ interface BrowserConsoleMessage {
525
+ text: string;
526
+ type: string;
527
+ }
528
+ /**
529
+ * The visitor — the interaction vocabulary handed to a visit scenario.
530
+ * Every action auto-waits (playwright actionability); `see()` is the single
531
+ * synchronization primitive: it retries until the element is visible and
532
+ * fails at the timeout. There is no sleep and no conditional helper.
533
+ */
534
+ interface Visitor {
535
+ /** Check a checkbox or radio. */
536
+ check: (element: ElementRef) => Promise<void>;
537
+ /** Click the element. */
538
+ click: (element: ElementRef) => Promise<void>;
539
+ /** Fill a form field with a value. */
540
+ fill: (element: ElementRef, value: string) => Promise<void>;
541
+ /** Navigate to a path of the site under test. */
542
+ goto: (path: string) => Promise<void>;
543
+ /** Hover the element. */
544
+ hover: (element: ElementRef) => Promise<void>;
545
+ /** Press a key (e.g. `Enter`). */
546
+ press: (key: string) => Promise<void>;
547
+ /** Wait until the element is visible — the only synchronization primitive. */
548
+ see: (element: ElementRef) => Promise<void>;
549
+ /** Select an option in a select field. */
550
+ select: (element: ElementRef, option: string) => Promise<void>;
551
+ }
552
+ /** The behavior of a visit — the When of the spec; assertions stay in the Then. */
553
+ type VisitScenario = (visitor: Visitor) => Promise<void>;
554
+ /** Per-visit options forwarded to the browser context. */
555
+ interface BrowserOpenOptions {
556
+ /**
557
+ * Base URL of the site under test — the origin `goto()` resolves against
558
+ * and the boundary of the `external` policy.
559
+ */
560
+ baseUrl: string;
561
+ /**
562
+ * Cross-origin request policy. `'block'` aborts every request leaving
563
+ * the site under test (analytics, CDNs) — the browser-side analog of
564
+ * strict intercepts. `'allow'` lets them through (deployed-site mode).
565
+ */
566
+ external: 'allow' | 'block';
567
+ /** Extra HTTP headers sent with every request of the visit (incl. User-Agent overrides). */
568
+ headers?: Record<string, string>;
569
+ /** The interaction scenario to run after load; the capture reflects the final state. */
570
+ scenario?: VisitScenario;
571
+ }
572
+ /**
573
+ * The rendered page captured by a browser visit — the FINAL state when a
574
+ * scenario ran. Extraction happens in-page (the browser IS the HTML
575
+ * parser); interpretation of the raw elements belongs to core.
576
+ */
577
+ interface BrowserPage {
578
+ /** Console messages emitted while loading and interacting, in order. */
579
+ consoleMessages: BrowserConsoleMessage[];
580
+ /** Serialized DOM after rendering (`document.documentElement.outerHTML`). */
581
+ html: string;
582
+ /** Raw text content of every `application/ld+json` script, in DOM order. */
583
+ jsonLdBlocks: string[];
584
+ /** `<link>` elements of the head, in DOM order. */
585
+ links: BrowserLinkElement[];
586
+ /** `<meta>` elements of the head, in DOM order. */
587
+ metas: BrowserMetaElement[];
588
+ /** HTTP status of the main document response (0 when unavailable). */
589
+ status: number;
590
+ /** Rendered `document.body.innerText`. */
591
+ text: string;
592
+ /** `document.title` after rendering. */
593
+ title: string;
594
+ /** Final URL after redirects and scenario navigation. */
595
+ url: string;
596
+ }
597
+ /**
598
+ * Abstract browser interface for the website specification runner.
599
+ * One implementation lives in `integrations/playwright/` — a single shared
600
+ * browser instance per runner; each `open()` gets a fresh, isolated context.
601
+ */
602
+ interface BrowserPort {
603
+ /** Close the shared browser instance (idempotent). */
604
+ close: () => Promise<void>;
605
+ /** Load `url` in a fresh context, run the scenario, capture the final page. */
606
+ open: (url: string, options: BrowserOpenOptions) => Promise<BrowserPage>;
607
+ }
608
+ //#endregion
499
609
  //#region src/core/ports/isolation.port.d.ts
500
610
  /**
501
611
  * Strategy for isolating service state across parallel test workers.
@@ -623,6 +733,83 @@ declare class HttpResult extends BaseResult {
623
733
  get status(): number;
624
734
  }
625
735
  //#endregion
736
+ //#region src/core/specification/website/result.d.ts
737
+ /** A raw HTTP exchange captured by `.fetch()` — redirects are NOT followed. */
738
+ interface FetchExchange {
739
+ body: string;
740
+ headers: Record<string, string>;
741
+ status: number;
742
+ }
743
+ /** Result from a raw `.fetch()` action (robots.txt, sitemaps, redirects). */
744
+ declare class FetchResult extends BaseResult {
745
+ private readonly exchange;
746
+ constructor(options: {
747
+ config: SpecificationConfig;
748
+ exchange: FetchExchange;
749
+ testDir: string;
750
+ });
751
+ /** The response body as a text accessor (`toMatch('robots.txt')`, `.grep()`). */
752
+ get body(): TextAccessor;
753
+ /** Response headers as a flat, lower-cased key-value map. */
754
+ get headers(): Record<string, string>;
755
+ /** The response body parsed as JSON. */
756
+ get json(): JsonAccessor;
757
+ /** The `location` header of a redirect response, or undefined. */
758
+ get location(): string | undefined;
759
+ /** The HTTP response status code — redirects surface as 3xx, never followed. */
760
+ get status(): number;
761
+ }
762
+ /** Result from a rendered `.visit()` action — the page as a browser saw it. */
763
+ declare class PageResult extends BaseResult {
764
+ private readonly page;
765
+ constructor(options: {
766
+ config: SpecificationConfig;
767
+ page: BrowserPage;
768
+ testDir: string;
769
+ });
770
+ /** Hreflang alternates declared in the head, keyed by language code. */
771
+ get alternates(): Record<string, string>;
772
+ /** The canonical URL declared in the head, or null. */
773
+ get canonical(): null | string;
774
+ /**
775
+ * The browser console as a stream — one message per line, prefixed by
776
+ * its type — same shape as `stdout` on a cli result. Assert emptiness
777
+ * with `toBeEmpty()`, or snapshot with `toMatch('home.console.txt')`.
778
+ */
779
+ get console(): TextAccessor;
780
+ /**
781
+ * Console messages of type `error` only — the stream you usually want
782
+ * empty: `expect(result.errors).toBeEmpty()`. Same shape as `stderr` on
783
+ * a cli result.
784
+ */
785
+ get errors(): TextAccessor;
786
+ /**
787
+ * The document-head projection as a JSON accessor — title, canonical,
788
+ * hreflang alternates, and named metas — for one golden per page:
789
+ * `expect(result.head).toMatch('home.head.json')`.
790
+ */
791
+ get head(): JsonAccessor;
792
+ /** The rendered DOM serialization as a text accessor. */
793
+ get html(): TextAccessor;
794
+ /**
795
+ * Every `application/ld+json` block of the page, parsed, as one JSON
796
+ * array — `expect(result.jsonLd).toMatch('article.jsonld.json')`.
797
+ */
798
+ get jsonLd(): JsonAccessor;
799
+ /** `<link>` elements of the head, in DOM order. */
800
+ get links(): BrowserLinkElement[];
801
+ /** The content of a named meta — `meta('description')`, `meta('og:image')`. */
802
+ meta(name: string): string | undefined;
803
+ /** HTTP status of the main document response. */
804
+ get status(): number;
805
+ /** Rendered body text as a text accessor. */
806
+ get content(): TextAccessor;
807
+ /** The document title as a text accessor. */
808
+ get title(): TextAccessor;
809
+ /** Final URL after redirects. */
810
+ get url(): string;
811
+ }
812
+ //#endregion
626
813
  //#region src/core/specification/shared/builder.d.ts
627
814
  /** A named job that can be triggered via jobs.trigger(). */
628
815
  interface JobHandle {
@@ -642,6 +829,20 @@ interface DockerSpecConfig {
642
829
  }
643
830
  /** Adapter configuration passed to the specification facets at setup time. */
644
831
  interface SpecificationConfig {
832
+ /** Base URL of the website under test (website facet only). */
833
+ baseUrl?: string;
834
+ /**
835
+ * Lazy browser accessor (website facet only). The first `.visit()`
836
+ * launches the shared browser instance; `.fetch()`-only spec files never
837
+ * pay the browser cost.
838
+ */
839
+ browser?: () => Promise<BrowserPort>;
840
+ /**
841
+ * Cross-origin request policy for visits (website facet only): `'block'`
842
+ * aborts requests leaving the site under test — the browser-side analog
843
+ * of strict intercepts.
844
+ */
845
+ external?: 'allow' | 'block';
645
846
  command?: CliPort;
646
847
  database?: DatabasePort;
647
848
  /**
@@ -750,6 +951,24 @@ interface CliSpecification<DatabaseKey extends string = string> {
750
951
  */
751
952
  exec: (args?: string | string[], options?: ExecOptions) => Promise<CliResult>;
752
953
  }
954
+ /**
955
+ * The `website` facet — page chain entry handed out by
956
+ * `specification.website()`. Setup methods chain; action methods are
957
+ * terminal. `.visit()` renders the page in the shared browser; `.fetch()`
958
+ * performs one raw HTTP exchange and never follows redirects.
959
+ */
960
+ interface WebsiteSpecification {
961
+ /** Set HTTP headers for the exchange (incl. User-Agent overrides). Multiple calls merge. */
962
+ headers: (headers: Record<string, string>) => WebsiteSpecification;
963
+ /** Perform one raw HTTP GET — redirects surface as 3xx results, never followed. */
964
+ fetch: (path: string) => Promise<FetchResult>;
965
+ /**
966
+ * Render the page in the shared browser and resolve with the captured
967
+ * document. With a scenario, the visitor interacts first (the When) and
968
+ * the capture reflects the FINAL page state.
969
+ */
970
+ visit: (path: string, scenario?: VisitScenario) => Promise<PageResult>;
971
+ }
753
972
  //#endregion
754
973
  //#region src/core/specification/cli/result.d.ts
755
974
  /**
@@ -1101,6 +1320,65 @@ interface JobsHandle<DatabaseKey extends string = string> {
1101
1320
  }
1102
1321
  declare function startJobs<Services extends ServiceRecord>(options: JobsSpecificationOptions<Services>): Promise<JobsHandle<DatabaseKeys<Services>>>;
1103
1322
  //#endregion
1323
+ //#region src/core/specification/website/serve.adapter.d.ts
1324
+ /** Options for the local server started by `specification.website()`. */
1325
+ interface ServeOptions {
1326
+ /** Shell command that starts the site. Receives the chosen port as `PORT`. */
1327
+ command: string;
1328
+ /** Fixed port. Default: an OS-assigned free port, injected as `PORT`. */
1329
+ port?: number;
1330
+ /** Path polled until it answers with any HTTP status. Default `/`. */
1331
+ ready?: string;
1332
+ /** Readiness budget in milliseconds. Default 30 000. */
1333
+ timeout?: number;
1334
+ }
1335
+ //#endregion
1336
+ //#region src/core/specification/website/start-website.d.ts
1337
+ /**
1338
+ * Options for {@link startWebsite | specification.website}. `server` (start
1339
+ * the site locally) and `url` (target a running site) are mutually
1340
+ * exclusive BY TYPE — the union makes the invalid combinations
1341
+ * inexpressible rather than runtime-checked.
1342
+ */
1343
+ type WebsiteSpecificationOptions = {
1344
+ /**
1345
+ * Cross-origin request policy for visits. Default: `'block'` with a
1346
+ * local `server` (deterministic — analytics and CDNs never leave the
1347
+ * machine), `'allow'` with a deployed `url`.
1348
+ */
1349
+ external?: 'allow' | 'block';
1350
+ /**
1351
+ * Project-root override (CONVENTIONS A9): the working directory of the
1352
+ * `server` command. Auto-discovered from the calling file when absent.
1353
+ */
1354
+ root?: string;
1355
+ } & ({
1356
+ /**
1357
+ * Start the site locally: a shell command receiving a free port
1358
+ * as `PORT`, polled on `ready` (default `/`) until it answers.
1359
+ */
1360
+ server: ServeOptions;
1361
+ url?: never;
1362
+ } | {
1363
+ server?: never;
1364
+ /** Target an already-running site (a deployed or preview URL). */
1365
+ url: string;
1366
+ });
1367
+ /**
1368
+ * The record returned by {@link startWebsite | specification.website}.
1369
+ * Destructure with the canonical names (CONVENTIONS A3):
1370
+ *
1371
+ * const { website, cleanup } = await specification.website(…);
1372
+ */
1373
+ interface WebsiteHandle {
1374
+ /** Stop the server process and the shared browser instance. */
1375
+ cleanup: () => Promise<void>;
1376
+ /** The base URL the specs run against. */
1377
+ url: string;
1378
+ website: WebsiteSpecification;
1379
+ }
1380
+ declare function startWebsite(options: WebsiteSpecificationOptions): Promise<WebsiteHandle>;
1381
+ //#endregion
1104
1382
  //#region src/core/specification/shared/specification.d.ts
1105
1383
  /**
1106
1384
  * The three specification constructors (CONVENTIONS A2) — created in a
@@ -1146,6 +1424,14 @@ declare const specification: {
1146
1424
  * server, no mode. `.trigger(name)` is the terminal action.
1147
1425
  */
1148
1426
  jobs: typeof startJobs;
1427
+ /**
1428
+ * Test a deployed or locally-served website. `server` starts the site
1429
+ * (a shell command receiving `PORT`); `url` targets a running one.
1430
+ * `.visit(path)` renders the page in a single shared browser instance;
1431
+ * `.fetch(path)` performs one raw HTTP exchange (redirects never
1432
+ * followed).
1433
+ */
1434
+ website: typeof startWebsite;
1149
1435
  };
1150
1436
  //#endregion
1151
1437
  //#region src/integrations/docker/docker-lookup.d.ts
@@ -1176,6 +1462,26 @@ interface ContainerPort {
1176
1462
  getLogs: () => Promise<string>;
1177
1463
  }
1178
1464
  //#endregion
1465
+ //#region src/core/specification/website/elements.d.ts
1466
+ /**
1467
+ * The element vocabulary — user-facing descriptors for visit scenarios.
1468
+ * Reads like English (`click(link('Articles'))`), translates to
1469
+ * accessibility-first locators in the browser integration. CSS/XPath is
1470
+ * deliberately not expressible; `testId()` is the single escape hatch.
1471
+ */
1472
+ /** A button (or element with the button role), by accessible name. */
1473
+ declare const button: (name: string) => ElementRef;
1474
+ /** A form field, by label. */
1475
+ declare const field: (label: string) => ElementRef;
1476
+ /** A heading, by accessible name. */
1477
+ declare const heading: (name: string) => ElementRef;
1478
+ /** A link, by accessible name. */
1479
+ declare const link: (name: string) => ElementRef;
1480
+ /** An element containing the given text. */
1481
+ declare const content: (value: string) => ElementRef;
1482
+ /** The escape hatch: an element by `data-testid`. Prefer user-facing elements. */
1483
+ declare const testId: (id: string) => ElementRef;
1484
+ //#endregion
1179
1485
  //#region src/integrations/postgres/postgres.d.ts
1180
1486
  interface PostgresOptions {
1181
1487
  /**
@@ -1466,8 +1772,13 @@ declare const mockOfDate: MockDatePort;
1466
1772
  //#region src/index.d.ts
1467
1773
  declare module 'vitest' {
1468
1774
  interface Matchers<T = any> {
1469
- /** Assert the table has zero rows. Async — queries the database. */
1470
- toBeEmpty: T extends TableAccessor ? () => Promise<void> : never;
1775
+ /**
1776
+ * Assert the subject is empty zero rows for a table (async), an
1777
+ * empty stream for a text accessor (console, errors, stdout).
1778
+ */
1779
+ toBeEmpty: T extends TableAccessor ? () => Promise<void> : T extends {
1780
+ readonly comparableText: string;
1781
+ } ? () => Promise<void> : never;
1471
1782
  /** Assert the container is running. Async — docker-backed subject. */
1472
1783
  toBeRunning: T extends ContainerAccessor ? () => Promise<void> : never;
1473
1784
  /**
@@ -1498,4 +1809,4 @@ declare module 'vitest' {
1498
1809
  }
1499
1810
  }
1500
1811
  //#endregion
1501
- export { type ApiHandle, type ApiSpecification, type ApiSpecificationOptions, BaseResult, type CaptureScope, type CliEnv, type CliHandle, type CliOutput, type CliPort, CliResult, type CliSpecification, type CliSpecificationOptions, ContainerAccessor, type ContainerPort, type DatabaseKeys, type DatabasePort, DirectoryAccessor, type DockerSpecConfig, type ExecOptions, type FileAccessor, FilesystemAccessor, type HonoApp, HttpResult, type InterceptContract, type InterceptEntry, type InterceptResponder, type InterceptResponse, type InterceptResponseValue, type InterceptTrigger, type IsolationStrategy, type JobHandle, type JobsHandle, type JobsSpecification, type JobsSpecificationOptions, JsonAccessor, type MatchFixtureOptions, type MatchableRequest, Matcher, type MatcherKind, type MockDatePort, type MockPort, Orchestrator, type PostgresOptions, type RedisOptions, ResponseAccessor, type ServerPort, type ServerResponse, type ServiceHandle, type ServiceRecord, type SpecificationConfig, type SpecificationMode, type SqliteOptions, TableAccessor, TextAccessor, anthropic, defineContract, findContainersByLabel, http, inspectContainer, match, mockOf, mockOfDate, openai, postgres, redis, removeContainers, specification, sqlite, text };
1812
+ export { type ApiHandle, type ApiSpecification, type ApiSpecificationOptions, BaseResult, type BrowserConsoleMessage, type BrowserLinkElement, type BrowserMetaElement, type BrowserOpenOptions, type BrowserPage, type BrowserPort, type CaptureScope, type CliEnv, type CliHandle, type CliOutput, type CliPort, CliResult, type CliSpecification, type CliSpecificationOptions, ContainerAccessor, type ContainerPort, type DatabaseKeys, type DatabasePort, DirectoryAccessor, type DockerSpecConfig, type ElementRef, type ExecOptions, FetchResult, type FileAccessor, FilesystemAccessor, type HonoApp, HttpResult, type InterceptContract, type InterceptEntry, type InterceptResponder, type InterceptResponse, type InterceptResponseValue, type InterceptTrigger, type IsolationStrategy, type JobHandle, type JobsHandle, type JobsSpecification, type JobsSpecificationOptions, JsonAccessor, type MatchFixtureOptions, type MatchableRequest, Matcher, type MatcherKind, type MockDatePort, type MockPort, Orchestrator, PageResult, type PostgresOptions, type RedisOptions, ResponseAccessor, type ServeOptions, type ServerPort, type ServerResponse, type ServiceHandle, type ServiceRecord, type SpecificationConfig, type SpecificationMode, type SqliteOptions, TableAccessor, TextAccessor, type VisitScenario, type Visitor, type WebsiteHandle, type WebsiteSpecification, type WebsiteSpecificationOptions, anthropic, button, content, defineContract, field, findContainersByLabel, heading, http, inspectContainer, link, match, mockOf, mockOfDate, openai, postgres, redis, removeContainers, specification, sqlite, testId, text };