@shivaedev/test-story 0.15.0 → 0.17.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/CHANGELOG.md +18 -0
- package/README.md +32 -30
- package/dist/server/screenDriver.d.ts +3 -0
- package/dist/server/screenDriver.d.ts.map +1 -1
- package/dist/server/screenDriver.js +14 -1
- package/dist/server/screenDriver.js.map +1 -1
- package/package.json +7 -7
- package/src/server/screenDriver.ts +19 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.17.0 - 2026-10-19
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Quality's `manifests/exports-shape` rule checks exports at the package root and in workspace members by default. Modules use one file-preserving TypeScript wildcard with matching source, types and runtime targets; root entries, exact non-code assets, package metadata and null blocks are allowed.
|
|
8
|
+
|
|
9
|
+
### Breaking
|
|
10
|
+
|
|
11
|
+
- Quality's Biome preset moves from `@shivaedev/quality/biome` to `@shivaedev/quality/biome/preset.json`. Change the `extends` entry in your Biome config to the new path; the old alias is removed. TypeScript preset paths are unchanged.
|
|
12
|
+
- `@shivaedev/effect-pg-boss`, `@shivaedev/effect-test-sql`, `@shivaedev/local-postgres` and `@shivaedev/test-story` expose server modules at their file paths. Add `/server/` after the package name in imports, for example `@shivaedev/test-story/server/storyKit.ts`. The shortened paths are removed; internal server modules remain blocked.
|
|
13
|
+
- Work Board moves `@shivaedev/work-board/client/client.ts` to `@shivaedev/work-board/browserClient.ts`; change browser-client imports to that file path. `@shivaedev/work-board/client/document/View.ts` is removed: consume document queries with your own renderer, or use the bundled Board browser UI. The client bundle stays private and the old export overrides are removed.
|
|
14
|
+
|
|
15
|
+
## 0.16.0 - 2026-10-19
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- `screenDriver.find(target, { timeout }?)` waits for a unique element by selector or role and accessible name, narrates the lookup, and returns the element with the actions' candidate diagnostics when no unique target appears.
|
|
20
|
+
|
|
3
21
|
## 0.15.0 - 2026-10-19
|
|
4
22
|
|
|
5
23
|
### Added
|
package/README.md
CHANGED
|
@@ -77,7 +77,7 @@ The bakery below starts with a cold oven and no dough. Its traits declare heat a
|
|
|
77
77
|
|
|
78
78
|
### 1. Once per engine: the story kit
|
|
79
79
|
|
|
80
|
-
`storyKit(definition)` from `@shivaedev/test-story/storyKit.ts` turns one definition into the kit. Every type, the engine's, the stages', the verbs' and the services', is inferred from it.
|
|
80
|
+
`storyKit(definition)` from `@shivaedev/test-story/server/storyKit.ts` turns one definition into the kit. Every type, the engine's, the stages', the verbs' and the services', is inferred from it.
|
|
81
81
|
|
|
82
82
|
```ts
|
|
83
83
|
export const bakery = storyKit({
|
|
@@ -143,7 +143,7 @@ This is the bakery kit this package tests itself with. `emptyBakery`, `stillWait
|
|
|
143
143
|
Every test of the kit is named `Bakery Story: <its name>`, so `vitest run -t "Bakery Story: "` runs only the bakery's stories with no setup. A project that splits runs by Vitest tags can also tag them:
|
|
144
144
|
|
|
145
145
|
```ts
|
|
146
|
-
import { genreTags } from "@shivaedev/test-story/genreTags.ts";
|
|
146
|
+
import { genreTags } from "@shivaedev/test-story/server/genreTags.ts";
|
|
147
147
|
import { defineConfig } from "vitest/config";
|
|
148
148
|
|
|
149
149
|
export default defineConfig({
|
|
@@ -166,7 +166,7 @@ const app = storyKit({
|
|
|
166
166
|
|
|
167
167
|
`appDefinition` holds the application's kit hooks, and `fakeUrl` is the listening fake server's HTTP or HTTPS origin, such as `http://127.0.0.1:4321`. The map matches exact URL hostnames, across ports. It replaces the destination origin while keeping the request's path, query, method, headers and body. For example, a POST to `https://github.com/login/oauth/access_token` reaches the fake's `/login/oauth/access_token`. In Node stories, other hostnames pass through the previous dispatcher unchanged. In the DOM environment, only loopback hosts (`localhost`, IPv4 loopback addresses and `[::1]`) may pass through without a route; every other hostname must have an `outbound` fake. A refused request fails the story and names its hostname, even when application code catches the fetch error.
|
|
168
168
|
|
|
169
|
-
`routeOutbound(routes)` from `@shivaedev/test-story/routeOutbound.ts` installs the same routing in the current Effect Scope. Acquire a `fakeHttpService` in `create`, then yield `routeOutbound({ "api.example.com": api.url })` so the fake listener and its routing belong to that story. The starter's social sign-in story exercises this path through both document navigation and host-side requests.
|
|
169
|
+
`routeOutbound(routes)` from `@shivaedev/test-story/server/routeOutbound.ts` installs the same routing in the current Effect Scope. Acquire a `fakeHttpService` in `create`, then yield `routeOutbound({ "api.example.com": api.url })` so the fake listener and its routing belong to that story. The starter's social sign-in story exercises this path through both document navigation and host-side requests.
|
|
170
170
|
|
|
171
171
|
Routing uses the global dispatcher that Node's built-in `fetch` already uses. The kit's `outbound` option installs routing before `around`, creation, traits and the body, stays active during scoped cleanup, and restores the previous dispatcher after each story, including a failing story. Stories within one worker must run one at a time because that dispatcher is shared; parallelism belongs between workers or inside one story. The DOM environment uses the same routing for host-side Node fetch and document requests, including redirect destinations. It refuses external hosts even when the kit omits `outbound`.
|
|
172
172
|
|
|
@@ -182,7 +182,7 @@ A kit can collect spans with a native in-memory tracer, or start a fake OTLP rec
|
|
|
182
182
|
import { Effect, Tracer } from "effect";
|
|
183
183
|
import { HttpEffect, HttpServerResponse } from "effect/http";
|
|
184
184
|
import { expect } from "vitest";
|
|
185
|
-
import { storyKit } from "@shivaedev/test-story/storyKit.ts";
|
|
185
|
+
import { storyKit } from "@shivaedev/test-story/server/storyKit.ts";
|
|
186
186
|
|
|
187
187
|
const app = storyKit({
|
|
188
188
|
around: (effect) => effect.pipe(Effect.withSpan("story")),
|
|
@@ -235,10 +235,10 @@ Detachment only removes the incoming parent: spans that application startup expl
|
|
|
235
235
|
|
|
236
236
|
#### Forwarding requests to a host
|
|
237
237
|
|
|
238
|
-
Use `forwardingProxy` from `@shivaedev/test-story/forwardingProxy.ts` in the kit's scoped `create` or `after` hook after starting the host under test:
|
|
238
|
+
Use `forwardingProxy` from `@shivaedev/test-story/server/forwardingProxy.ts` in the kit's scoped `create` or `after` hook after starting the host under test:
|
|
239
239
|
|
|
240
240
|
```ts
|
|
241
|
-
import { forwardingProxy } from "@shivaedev/test-story/forwardingProxy.ts";
|
|
241
|
+
import { forwardingProxy } from "@shivaedev/test-story/server/forwardingProxy.ts";
|
|
242
242
|
|
|
243
243
|
const proxy = yield* forwardingProxy(upstream, {
|
|
244
244
|
localAddress: "::1",
|
|
@@ -257,7 +257,7 @@ Set `localAddress` to choose the source socket address of the proxy's connection
|
|
|
257
257
|
|
|
258
258
|
#### Reading OTLP exports
|
|
259
259
|
|
|
260
|
-
Use `otlpReceiver` from `@shivaedev/test-story/otlpReceiver.ts` in a kit's scoped `create` hook. It listens on an allocated loopback port and closes with the story scope. Point the application's real exporter at `${receiver.url}/v1/traces`, then flush the exporter before reading:
|
|
260
|
+
Use `otlpReceiver` from `@shivaedev/test-story/server/otlpReceiver.ts` in a kit's scoped `create` hook. It listens on an allocated loopback port and closes with the story scope. Point the application's real exporter at `${receiver.url}/v1/traces`, then flush the exporter before reading:
|
|
261
261
|
|
|
262
262
|
```ts
|
|
263
263
|
app.it("exports the work span", [], function* ({ runWork, disposeRuntime, spans, requests }) {
|
|
@@ -274,10 +274,10 @@ For timeout stories, return `undefined` from the `status` callback to accept an
|
|
|
274
274
|
|
|
275
275
|
#### Faking an outbound HTTP API
|
|
276
276
|
|
|
277
|
-
Use `fakeHttpService` from `@shivaedev/test-story/fakeHttpService.ts` in a kit's scoped `create` hook. Like the OTLP receiver, it listens on an allocated loopback port and closes with the scope. Routes have names for controls, a method (default `GET`), and a path whose `:segments` become decoded `request.params`. A reply receives the request and returns a JSON `body` (or a `Uint8Array`/Buffer sent as raw bytes), optional `status` (default 200), and optional response `headers`.
|
|
277
|
+
Use `fakeHttpService` from `@shivaedev/test-story/server/fakeHttpService.ts` in a kit's scoped `create` hook. Like the OTLP receiver, it listens on an allocated loopback port and closes with the scope. Routes have names for controls, a method (default `GET`), and a path whose `:segments` become decoded `request.params`. A reply receives the request and returns a JSON `body` (or a `Uint8Array`/Buffer sent as raw bytes), optional `status` (default 200), and optional response `headers`.
|
|
278
278
|
|
|
279
279
|
```ts
|
|
280
|
-
import { fakeHttpService } from "@shivaedev/test-story/fakeHttpService.ts";
|
|
280
|
+
import { fakeHttpService } from "@shivaedev/test-story/server/fakeHttpService.ts";
|
|
281
281
|
|
|
282
282
|
const api = yield* fakeHttpService({
|
|
283
283
|
guard: ({ headers }) => headers.authorization === "Bearer story-key",
|
|
@@ -494,9 +494,9 @@ Set `runIf: hasPostgres` in the kit definition to gate all its stories once, whe
|
|
|
494
494
|
|
|
495
495
|
The [order feature kit](https://github.com/ShivaeDev/platform/tree/main/packages/effect-react/src/test-support/order-example) is an example: it runs the feature’s services, repository, contracts, JSON serialization and authentication middleware over a loopback HTTP listener with bearer tokens.
|
|
496
496
|
|
|
497
|
-
For full-app stories, use `around: withIsolatedDatabase` from `@shivaedev/effect-test-sql/withIsolatedDatabase.ts` with a PostgreSQL test layer prepared from the application's migrations. Each story gets a database cloned from that template, and HTTP and worker pools commit into the same clone. The clone is dropped after scoped hosts and pools close. The starter's [application kit](https://github.com/ShivaeDev/platform/blob/main/examples/starter/src/test-support/application.ts) uses this isolation path.
|
|
497
|
+
For full-app stories, use `around: withIsolatedDatabase` from `@shivaedev/effect-test-sql/server/withIsolatedDatabase.ts` with a PostgreSQL test layer prepared from the application's migrations. Each story gets a database cloned from that template, and HTTP and worker pools commit into the same clone. The clone is dropped after scoped hosts and pools close. The starter's [application kit](https://github.com/ShivaeDev/platform/blob/main/examples/starter/src/test-support/application.ts) uses this isolation path.
|
|
498
498
|
|
|
499
|
-
For repository-level tests on a migrated template, `withTestTransaction` from `@shivaedev/effect-test-sql/withTestTransaction.ts` provides rollback isolation when all work uses its transaction connection. See the [database test helpers](https://github.com/ShivaeDev/platform/blob/main/packages/effect-test-sql/README.md) for template preparation and both wrappers.
|
|
499
|
+
For repository-level tests on a migrated template, `withTestTransaction` from `@shivaedev/effect-test-sql/server/withTestTransaction.ts` provides rollback isolation when all work uses its transaction connection. See the [database test helpers](https://github.com/ShivaeDev/platform/blob/main/packages/effect-test-sql/README.md) for template preparation and both wrappers.
|
|
500
500
|
|
|
501
501
|
React screen traits acquire roots in Scope and use a fresh native atom registry for each screen. Cleanup unmounts roots before disposing registries and servers. Screen verbs dispatch DOM events and use bounded observations of rendered output. Select a live clock once on an HTTP/DOM kit; keep TestClock for deterministic local stories. `runUntil` is for engines with a meaningful step hook, not HTTP or DOM polling. Creation forms can use the same pattern with the existing `useCreate` path.
|
|
502
502
|
|
|
@@ -504,7 +504,7 @@ For cookie sessions, use the DOM host setup below. Native `RpcTest` can exercise
|
|
|
504
504
|
|
|
505
505
|
### DOM stories with cookie sessions
|
|
506
506
|
|
|
507
|
-
Install `happy-dom@20.14.6` as a dev dependency. Select the `@shivaedev/test-story/domEnvironment.config.ts` Vitest environment for DOM tests that start their host in the same process:
|
|
507
|
+
Install `happy-dom@20.14.6` as a dev dependency. Select the `@shivaedev/test-story/server/domEnvironment.config.ts` Vitest environment for DOM tests that start their host in the same process:
|
|
508
508
|
|
|
509
509
|
```ts
|
|
510
510
|
import { fileURLToPath } from "node:url";
|
|
@@ -512,20 +512,20 @@ import { defineConfig } from "vitest/config";
|
|
|
512
512
|
|
|
513
513
|
export default defineConfig({
|
|
514
514
|
test: {
|
|
515
|
-
environment: fileURLToPath(import.meta.resolve("@shivaedev/test-story/domEnvironment.config.ts")),
|
|
515
|
+
environment: fileURLToPath(import.meta.resolve("@shivaedev/test-story/server/domEnvironment.config.ts")),
|
|
516
516
|
},
|
|
517
517
|
});
|
|
518
518
|
```
|
|
519
519
|
|
|
520
520
|
The environment keeps Node's global `fetch`, `Request`, `Response`, `Headers`, `AbortController` and `AbortSignal` for the in-process host. The separate `window` keeps happy-dom's constructors and cookie jar. Its asynchronous HTTP requests use Node fetch through the same `outbound` dispatcher, including XMLHttpRequest. Synchronous document HTTP requests are refused. The starter's [Vitest config](https://github.com/ShivaeDev/platform/blob/main/examples/starter/vitest.config.ts) selects this environment only for its DOM project while retaining the shared test projects. Use Vitest's standard forks or threads pool.
|
|
521
521
|
|
|
522
|
-
`domFetch(baseUrl)` from `@shivaedev/test-story/domFetch.ts` sets the document's location to the real HTTP host and returns a document fetch that resolves relative request strings against that URL. It translates Node `Request` and `Headers` inputs for happy-dom. Responses have happy-dom's status, headers and body methods; they are not instances of Node's global `Response`. The document keeps and sends cookies, including HttpOnly session cookies. Keep the default `same-origin` credentials policy for same-origin requests, and pass this fetch explicitly to browser clients, such as Better Auth's `fetchOptions.customFetchImpl`. The host continues to use global Node fetch without browser cookies.
|
|
522
|
+
`domFetch(baseUrl)` from `@shivaedev/test-story/server/domFetch.ts` sets the document's location to the real HTTP host and returns a document fetch that resolves relative request strings against that URL. It translates Node `Request` and `Headers` inputs for happy-dom. Responses have happy-dom's status, headers and body methods; they are not instances of Node's global `Response`. The document keeps and sends cookies, including HttpOnly session cookies. Keep the default `same-origin` credentials policy for same-origin requests, and pass this fetch explicitly to browser clients, such as Better Auth's `fetchOptions.customFetchImpl`. The host continues to use global Node fetch without browser cookies.
|
|
523
523
|
|
|
524
524
|
Call it in the kit's `create` hook after starting the host:
|
|
525
525
|
|
|
526
526
|
```ts
|
|
527
527
|
import { createAuthClient } from "better-auth/react";
|
|
528
|
-
import { domFetch } from "@shivaedev/test-story/domFetch.ts";
|
|
528
|
+
import { domFetch } from "@shivaedev/test-story/server/domFetch.ts";
|
|
529
529
|
|
|
530
530
|
const cookieApp = storyKit({
|
|
531
531
|
around: withIsolatedDatabase,
|
|
@@ -547,11 +547,11 @@ const cookieApp = storyKit({
|
|
|
547
547
|
|
|
548
548
|
#### Seeding a signed-in precondition
|
|
549
549
|
|
|
550
|
-
`seedCookies(cookies)` from `@shivaedev/test-story/seedCookies.ts` returns an Effect that seeds the current happy-dom window's cookie container, including HttpOnly cookies. Call `domFetch(baseUrl)` first so the document and requests use the same origin. Each cookie has `name` and `value`, with optional `domain`, `path`, `httpOnly`, `secure`, `sameSite` (`Lax`, `Strict` or `None`) and `expires` (Unix seconds). Better Auth's test-utils login cookies fit this shape directly.
|
|
550
|
+
`seedCookies(cookies)` from `@shivaedev/test-story/server/seedCookies.ts` returns an Effect that seeds the current happy-dom window's cookie container, including HttpOnly cookies. Call `domFetch(baseUrl)` first so the document and requests use the same origin. Each cookie has `name` and `value`, with optional `domain`, `path`, `httpOnly`, `secure`, `sameSite` (`Lax`, `Strict` or `None`) and `expires` (Unix seconds). Better Auth's test-utils login cookies fit this shape directly.
|
|
551
551
|
|
|
552
552
|
```ts
|
|
553
553
|
import { signedInUser } from "@shivaedev/platform/server/better-auth/testing.ts";
|
|
554
|
-
import { seedCookies } from "@shivaedev/test-story/seedCookies.ts";
|
|
554
|
+
import { seedCookies } from "@shivaedev/test-story/server/seedCookies.ts";
|
|
555
555
|
|
|
556
556
|
const { cookies } = yield* Effect.promise(() => signedInUser(testAuth, {
|
|
557
557
|
email: "alice@example.test",
|
|
@@ -564,7 +564,7 @@ yield* seedCookies(cookies);
|
|
|
564
564
|
|
|
565
565
|
#### Social sign-in through an outbound provider
|
|
566
566
|
|
|
567
|
-
Happy-dom's detached window records a location change without loading the next document. `domNavigate(url = window.location.href)` from `@shivaedev/test-story/domNavigate.ts` performs that load: it follows HTTP redirects through the document transport, keeps cookies for each destination, sets the final location, writes the returned HTML and returns the happy-dom response. Call it after the sign-in client has set the provider location, and before checking the returned page. An unrouted external redirect fails with its hostname.
|
|
567
|
+
Happy-dom's detached window records a location change without loading the next document. `domNavigate(url = window.location.href)` from `@shivaedev/test-story/server/domNavigate.ts` performs that load: it follows HTTP redirects through the document transport, keeps cookies for each destination, sets the final location, writes the returned HTML and returns the happy-dom response. Call it after the sign-in client has set the provider location, and before checking the returned page. An unrouted external redirect fails with its hostname.
|
|
568
568
|
|
|
569
569
|
The starter's [social application kit](https://github.com/ShivaeDev/platform/blob/main/examples/starter/src/test-support/socialApp.ts) runs a fixture Better Auth host on real PostgreSQL, with a rendered sign-in button and a fake GitHub provider. Its `create` hook acquires the provider and uses scoped `routeOutbound` routing for both `github.com` and `api.github.com`. The fake's authorize endpoint returns the application's callback URL with the received state and a test code. The real callback exchanges that code and fetches the user against the fake, creates the account and session in PostgreSQL, and redirects to the signed-in page.
|
|
570
570
|
|
|
@@ -589,7 +589,7 @@ socialApp.it("a social sign-in returns with a cookie session", [], function* ({
|
|
|
589
589
|
`screenDriver(options)` supplies DOM verbs and readouts for a kit's `verbs` hook. Give it the mounted `root` (or a getter for a root that navigation replaces), the story's `tell`, and a `pending()` function that reads the application's own request or atom state. `settle()` waits until that signal is false; repeated identical renders never count as completion. The application must keep the signal true until its resulting screen work is ready to render.
|
|
590
590
|
|
|
591
591
|
```ts
|
|
592
|
-
import { screenDriver } from "@shivaedev/test-story/screenDriver.ts";
|
|
592
|
+
import { screenDriver } from "@shivaedev/test-story/server/screenDriver.ts";
|
|
593
593
|
|
|
594
594
|
verbs: ({ view, requests }, story) => screenDriver({
|
|
595
595
|
root: view.container,
|
|
@@ -604,6 +604,8 @@ verbs: ({ view, requests }, story) => screenDriver({
|
|
|
604
604
|
|
|
605
605
|
`click`, `enter`, `submit`, `text` and `value` also accept a `ScreenTarget` object such as `{ role: "button", name: "Save" }` or `{ role: "textbox", name: /email/i }`. The role and computed accessible name must match exactly when strings are used; regular expressions match the name. Like Testing Library's role queries, these targets exclude hidden elements, including `aria-hidden` ancestors. Missing or ambiguous targets fail with the requested role, name and the visible candidates on screen. Narration names the role and name instead of a selector.
|
|
606
606
|
|
|
607
|
+
`find(target, { timeout }?)` returns an Effect that waits for exactly one matching rendered element and returns that `HTMLElement`. It accepts the same selectors and role/name targets as the actions, narrates the lookup, and uses the driver's live-time `timeoutMs` limit unless `timeout` overrides it in milliseconds. If no unique element appears before the limit, it fails with the same target diagnostics as the actions, including visible candidates for role/name targets.
|
|
608
|
+
|
|
607
609
|
`accessible(selector)` returns `{ name, role }` computed by `dom-accessibility-api`, including native roles and label references that take precedence over `aria-label`. It narrates the readout and uses the same single rendered target checks as other readouts. Assert on this result when checking a control's accessible name; reading its raw attributes does not compute that name. This DOM computation does not inspect a browser's accessibility tree or prove speech from a screen reader.
|
|
608
610
|
|
|
609
611
|
`accessible(element)` uses the same computation on an `Element` the kit already holds and tells no story line.
|
|
@@ -618,17 +620,17 @@ Import each module with the prefix `@shivaedev/test-story/`; there is no root en
|
|
|
618
620
|
|
|
619
621
|
| Module | Exports |
|
|
620
622
|
| --- | --- |
|
|
621
|
-
| `storyKit.ts` | `storyKit`, `Trait`, `RunUntilOptions`, `Story`, `RunHooks`, `StoryKitDefinition`, `Told`, `StoryBody`, `StoryTest`, `StoryIt`, `StoryKit` |
|
|
622
|
-
| `domFetch.ts` | `domFetch(baseUrl)` |
|
|
623
|
-
| `seedCookies.ts` | `seedCookies(cookies)`, `DomCookie` |
|
|
624
|
-
| `routeOutbound.ts` | `routeOutbound(routes)` |
|
|
625
|
-
| `screenDriver.ts` | `screenDriver(options)`, `ScreenDriverOptions`, `ScreenDriver`, `ScreenTarget` |
|
|
626
|
-
| `domNavigate.ts` | `domNavigate(url?)` |
|
|
627
|
-
| `genreTags.ts` | `genreTags(...kitNames)`, `GenreTag` |
|
|
628
|
-
| `domEnvironment.config.ts` | Default Vitest environment preserving Node host globals and happy-dom document transport |
|
|
629
|
-
| `fakeHttpService.ts` | `fakeHttpService(options)`, `FakeHttpRoute`, `FakeHttpRequest`, `FakeHttpReply`, `FakeHttpFault`, `FakeHttpHold`, `FakeHttpScope`, `FakeHttpScriptOptions`, `FakeHttpScript`, `FakeHttpServiceOptions` |
|
|
630
|
-
| `forwardingProxy.ts` | `forwardingProxy(upstream, options?)`, `ForwardingProxyOptions` |
|
|
631
|
-
| `otlpReceiver.ts` | `otlpReceiver(options?)`, `OtlpSpan`, `OtlpExport`, `OtlpReceiverOptions` |
|
|
623
|
+
| `server/storyKit.ts` | `storyKit`, `Trait`, `RunUntilOptions`, `Story`, `RunHooks`, `StoryKitDefinition`, `Told`, `StoryBody`, `StoryTest`, `StoryIt`, `StoryKit` |
|
|
624
|
+
| `server/domFetch.ts` | `domFetch(baseUrl)` |
|
|
625
|
+
| `server/seedCookies.ts` | `seedCookies(cookies)`, `DomCookie` |
|
|
626
|
+
| `server/routeOutbound.ts` | `routeOutbound(routes)` |
|
|
627
|
+
| `server/screenDriver.ts` | `screenDriver(options)`, `ScreenDriverOptions`, `ScreenDriver`, `ScreenTarget` |
|
|
628
|
+
| `server/domNavigate.ts` | `domNavigate(url?)` |
|
|
629
|
+
| `server/genreTags.ts` | `genreTags(...kitNames)`, `GenreTag` |
|
|
630
|
+
| `server/domEnvironment.config.ts` | Default Vitest environment preserving Node host globals and happy-dom document transport |
|
|
631
|
+
| `server/fakeHttpService.ts` | `fakeHttpService(options)`, `FakeHttpRoute`, `FakeHttpRequest`, `FakeHttpReply`, `FakeHttpFault`, `FakeHttpHold`, `FakeHttpScope`, `FakeHttpScriptOptions`, `FakeHttpScript`, `FakeHttpServiceOptions` |
|
|
632
|
+
| `server/forwardingProxy.ts` | `forwardingProxy(upstream, options?)`, `ForwardingProxyOptions` |
|
|
633
|
+
| `server/otlpReceiver.ts` | `otlpReceiver(options?)`, `OtlpSpan`, `OtlpExport`, `OtlpReceiverOptions` |
|
|
632
634
|
|
|
633
635
|
### Install and limits
|
|
634
636
|
|
|
@@ -18,6 +18,9 @@ export declare function screenDriver({ root, pending, tell, act, timeoutMs }: Sc
|
|
|
18
18
|
};
|
|
19
19
|
click: (selector: ScreenTarget) => Effect.Effect<void, never, never>;
|
|
20
20
|
enter: (selector: ScreenTarget, value: string) => Effect.Effect<void, never, never>;
|
|
21
|
+
find: (target: ScreenTarget, options?: {
|
|
22
|
+
readonly timeout?: number;
|
|
23
|
+
}) => Effect.Effect<HTMLElement, import("effect/Cause").UnknownError, never>;
|
|
21
24
|
settle: (options?: {
|
|
22
25
|
readonly timeoutMs?: number;
|
|
23
26
|
}) => Effect.Effect<void, never, never>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"screenDriver.d.ts","sourceRoot":"","sources":["../../src/server/screenDriver.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"screenDriver.d.ts","sourceRoot":"","sources":["../../src/server/screenDriver.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAY,MAAM,QAAQ,CAAC;AAK1C,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAA;CAAE,CAAC;AAE9F,MAAM,WAAW,mBAAmB;IACnC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,KAAK,WAAW,CAAC,IAAI,CAAC,CAAC;IAClE,QAAQ,CAAC,OAAO,EAAE,MAAM,OAAO,CAAC;IAChC,QAAQ,CAAC,IAAI,EAAE,WAAW,GAAG,CAAC,MAAM,WAAW,CAAC,CAAC;IACjD,QAAQ,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACtC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,MAAM,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AAE3D,wBAAgB,YAAY,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,GAA0B,EAAE,SAAgB,EAAE,EAAE,mBAAmB;2BAiD9F,MAAM,GAAG,OAAO;;;;sBASrB,YAAY;sBAOZ,YAAY,SAAS,MAAM;mBAlCxB,YAAY,YAAW;QAAE,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE;uBAtBjD;QAAE,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE;wBA2DpC,YAAY;sBACb,YAAY;sBAKZ,YAAY;EAM/B"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Effect } from "effect";
|
|
1
|
+
import { Effect, Schedule } from "effect";
|
|
2
2
|
import { withLive } from "@shivaedev/effect-test/withLive.ts";
|
|
3
3
|
import { accessibility, targetName } from "#internal/screenAccessibility.ts";
|
|
4
4
|
import { screenElements } from "#internal/screenElements.ts";
|
|
@@ -29,6 +29,18 @@ export function screenDriver({ root, pending, tell, act = (action) => action(),
|
|
|
29
29
|
}
|
|
30
30
|
return withLive(wait.pipe(Effect.timeoutOrElse({ duration: limit, orElse: timedOut })));
|
|
31
31
|
}
|
|
32
|
+
function find(target, options = {}) {
|
|
33
|
+
return withLive(Effect.gen(function* () {
|
|
34
|
+
tell(`find ${targetName(target)}`);
|
|
35
|
+
return yield* Effect.tryPromise(async () => {
|
|
36
|
+
await act(async () => undefined);
|
|
37
|
+
return elements.element(target);
|
|
38
|
+
}).pipe(Effect.retry(Schedule.spaced(10)), Effect.timeoutOrElse({
|
|
39
|
+
duration: options.timeout ?? timeoutMs,
|
|
40
|
+
orElse: () => Effect.sync(() => elements.element(target)),
|
|
41
|
+
}));
|
|
42
|
+
}));
|
|
43
|
+
}
|
|
32
44
|
return {
|
|
33
45
|
accessible: (selector) => {
|
|
34
46
|
if (typeof selector !== "string") {
|
|
@@ -46,6 +58,7 @@ export function screenDriver({ root, pending, tell, act = (action) => action(),
|
|
|
46
58
|
element.click();
|
|
47
59
|
}),
|
|
48
60
|
enter: (selector, value) => perform(`enter a value in ${targetName(selector)}`, () => elements.enter(selector, value)),
|
|
61
|
+
find,
|
|
49
62
|
settle,
|
|
50
63
|
submit: (selector = "form") => perform(`submit ${targetName(selector)}`, () => elements.form(selector).requestSubmit()),
|
|
51
64
|
text: (selector) => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"screenDriver.js","sourceRoot":"","sources":["../../src/server/screenDriver.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;
|
|
1
|
+
{"version":3,"file":"screenDriver.js","sourceRoot":"","sources":["../../src/server/screenDriver.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shivaedev/test-story",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"description": "Tests that read like stories over the real engine: a story kit per engine with its own it, traits per feature, and a failure that prints the story, where it stopped and the engine's state",
|
|
5
5
|
"homepage": "https://github.com/ShivaeDev/platform/tree/main/packages/test-story#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -24,12 +24,12 @@
|
|
|
24
24
|
},
|
|
25
25
|
"exports": {
|
|
26
26
|
"./*.ts": {
|
|
27
|
-
"source": "./src
|
|
28
|
-
"types": "./dist
|
|
29
|
-
"default": "./dist
|
|
27
|
+
"source": "./src/*.ts",
|
|
28
|
+
"types": "./dist/*.d.ts",
|
|
29
|
+
"default": "./dist/*.js"
|
|
30
30
|
},
|
|
31
|
-
"./
|
|
32
|
-
"./
|
|
31
|
+
"./package.json": "./package.json",
|
|
32
|
+
"./server/internal/*": null
|
|
33
33
|
},
|
|
34
34
|
"files": [
|
|
35
35
|
"dist",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@effect/vitest": "4.0.4",
|
|
49
|
-
"@shivaedev/quality": "0.
|
|
49
|
+
"@shivaedev/quality": "0.30.0",
|
|
50
50
|
"@types/node": "24.19.2",
|
|
51
51
|
"effect": "4.0.4",
|
|
52
52
|
"happy-dom": "20.14.6",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Effect } from "effect";
|
|
1
|
+
import { Effect, Schedule } from "effect";
|
|
2
2
|
import { withLive } from "@shivaedev/effect-test/withLive.ts";
|
|
3
3
|
import { accessibility, targetName } from "#internal/screenAccessibility.ts";
|
|
4
4
|
import { screenElements } from "#internal/screenElements.ts";
|
|
@@ -46,6 +46,23 @@ export function screenDriver({ root, pending, tell, act = (action) => action(),
|
|
|
46
46
|
}
|
|
47
47
|
return withLive(wait.pipe(Effect.timeoutOrElse({ duration: limit, orElse: timedOut })));
|
|
48
48
|
}
|
|
49
|
+
function find(target: ScreenTarget, options: { readonly timeout?: number } = {}) {
|
|
50
|
+
return withLive(
|
|
51
|
+
Effect.gen(function* () {
|
|
52
|
+
tell(`find ${targetName(target)}`);
|
|
53
|
+
return yield* Effect.tryPromise(async () => {
|
|
54
|
+
await act(async () => undefined);
|
|
55
|
+
return elements.element(target);
|
|
56
|
+
}).pipe(
|
|
57
|
+
Effect.retry(Schedule.spaced(10)),
|
|
58
|
+
Effect.timeoutOrElse({
|
|
59
|
+
duration: options.timeout ?? timeoutMs,
|
|
60
|
+
orElse: () => Effect.sync(() => elements.element(target)),
|
|
61
|
+
}),
|
|
62
|
+
);
|
|
63
|
+
}),
|
|
64
|
+
);
|
|
65
|
+
}
|
|
49
66
|
return {
|
|
50
67
|
accessible: (selector: string | Element) => {
|
|
51
68
|
if (typeof selector !== "string") {
|
|
@@ -64,6 +81,7 @@ export function screenDriver({ root, pending, tell, act = (action) => action(),
|
|
|
64
81
|
element.click();
|
|
65
82
|
}),
|
|
66
83
|
enter: (selector: ScreenTarget, value: string) => perform(`enter a value in ${targetName(selector)}`, () => elements.enter(selector, value)),
|
|
84
|
+
find,
|
|
67
85
|
settle,
|
|
68
86
|
submit: (selector: ScreenTarget = "form") => perform(`submit ${targetName(selector)}`, () => elements.form(selector).requestSubmit()),
|
|
69
87
|
text: (selector?: ScreenTarget) => {
|