@shivaedev/test-story 0.7.0 → 0.8.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 +17 -0
- package/README.md +103 -69
- package/dist/server/domEnvironment.config.js +1 -1
- package/dist/server/domEnvironment.config.js.map +1 -1
- package/dist/server/internal/installOutbound.d.ts +2 -0
- package/dist/server/internal/installOutbound.d.ts.map +1 -0
- package/dist/server/internal/{routeOutbound.js → installOutbound.js} +1 -5
- package/dist/server/internal/installOutbound.js.map +1 -0
- package/dist/server/internal/screenElements.d.ts +9 -0
- package/dist/server/internal/screenElements.d.ts.map +1 -0
- package/dist/server/internal/screenElements.js +61 -0
- package/dist/server/internal/screenElements.js.map +1 -0
- package/dist/server/internal/seed.d.ts +1 -1
- package/dist/server/internal/seed.d.ts.map +1 -1
- package/dist/server/internal/start.d.ts.map +1 -1
- package/dist/server/internal/start.js +4 -1
- package/dist/server/internal/start.js.map +1 -1
- package/dist/server/internal/storyIt.d.ts.map +1 -1
- package/dist/server/internal/storyIt.js +1 -1
- package/dist/server/internal/storyIt.js.map +1 -1
- package/dist/server/{internal/routeOutbound.d.ts → routeOutbound.d.ts} +0 -1
- package/dist/server/routeOutbound.d.ts.map +1 -0
- package/dist/server/routeOutbound.js +6 -0
- package/dist/server/routeOutbound.js.map +1 -0
- package/dist/server/screenDriver.d.ts +24 -0
- package/dist/server/screenDriver.d.ts.map +1 -0
- package/dist/server/screenDriver.js +60 -0
- package/dist/server/screenDriver.js.map +1 -0
- package/dist/server/storyKit.d.ts +3 -3
- package/dist/server/storyKit.d.ts.map +1 -1
- package/package.json +6 -8
- package/src/server/domEnvironment.config.ts +1 -1
- package/src/server/internal/{routeOutbound.ts → installOutbound.ts} +0 -8
- package/src/server/internal/screenElements.ts +62 -0
- package/src/server/internal/seed.ts +1 -1
- package/src/server/internal/start.ts +9 -1
- package/src/server/internal/storyIt.ts +1 -1
- package/src/server/routeOutbound.ts +9 -0
- package/src/server/screenDriver.ts +75 -0
- package/src/server/storyKit.ts +5 -5
- package/dist/server/internal/routeOutbound.d.ts.map +0 -1
- package/dist/server/internal/routeOutbound.js.map +0 -1
- package/docs/north-star.md +0 -35
- package/docs/roadmap.md +0 -26
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.8.0 - 2026-10-18
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- `screenDriver(options).accessible(selector)` returns and narrates an element's computed accessible name and role, including label references that take precedence over `aria-label`.
|
|
8
|
+
- Export `screenDriver(options)`, `ScreenDriverOptions` and `ScreenDriver` from `screenDriver.ts`: narrated DOM clicks, text entry, form submission and screen readouts, with settlement driven by the app's pending signal and an optional renderer `act` adapter. Screens use the existing DOM transport to reach fake HTTP services, including identity providers.
|
|
9
|
+
- Screen drivers accept a root getter for document navigation and a per-call `settle({ timeoutMs })` limit.
|
|
10
|
+
- Export `routeOutbound(routes)` from `routeOutbound.ts` to scope outbound routing alongside a story's fake HTTP service.
|
|
11
|
+
- Screen actions and readouts reject hidden targets and targets inside hidden ancestors; form submission also refuses inert ancestors.
|
|
12
|
+
- Accept `stages: []` in `storyKit` definitions for engines initialized entirely in `create`, preserving inferred engine and verb types with no available trait stages.
|
|
13
|
+
|
|
14
|
+
## 0.7.1 - 2026-10-18
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- Publish against `@shivaedev/effect-test` 0.6.0 so framework consumers share the current test runner.
|
|
19
|
+
|
|
3
20
|
## 0.7.0 - 2026-10-18
|
|
4
21
|
|
|
5
22
|
### Added
|
package/README.md
CHANGED
|
@@ -8,11 +8,13 @@ When agents write most of the code, test files grow into hundreds of lines nobod
|
|
|
8
8
|
|
|
9
9
|
```ts
|
|
10
10
|
bakery.it("steps until the condition holds", [ovenIsLit(), hasDough(3)], function* ({ oven }) {
|
|
11
|
-
|
|
11
|
+
yield* oven.bakesEverything();
|
|
12
|
+
expect(oven.loaves()).toBe(3);
|
|
13
|
+
expect(oven.minutes()).toBe(3);
|
|
12
14
|
});
|
|
13
15
|
```
|
|
14
16
|
|
|
15
|
-
That is a whole test. `bakery.it` comes from the bakery's story kit. The list holds its traits, `ovenIsLit()` and `hasDough(3)`, which are written once and shared by every spec. `oven.bakesEverything()`
|
|
17
|
+
That is a whole test. `bakery.it` comes from the bakery's story kit. The list holds its traits, `ovenIsLit()` and `hasDough(3)`, which are written once and shared by every spec. `oven.bakesEverything()` performs the baking action until the dough is gone; `oven.loaves()` and `oven.minutes()` read separate observations. The spec states both expectations. Every story runs the whole engine, so each test also checks the parts its author never thought to mock, and because a spec reads as English, a sloppy test stands out in review.
|
|
16
18
|
|
|
17
19
|
When a story fails, it prints itself. One of this package's own failing stories expects 2 loaves from one ball of dough, and the test fails with this message:
|
|
18
20
|
|
|
@@ -28,7 +30,7 @@ loaves: expected 1 to be 2 // Object.is equality
|
|
|
28
30
|
given the baker has 1 balls of dough
|
|
29
31
|
1m a loaf comes out of the oven
|
|
30
32
|
✗ the test failed after the line above
|
|
31
|
-
at src/test-support/failingStories.ts:
|
|
33
|
+
at src/test-support/failingStories.ts:12:34
|
|
32
34
|
|
|
33
35
|
The bakery when the test failed:
|
|
34
36
|
{"bowls":1,"capacity":3,"dough":0,"flour":0,"loaves":1,"minute":1,"orders":[],"ovenLit":true,"overfired":false,
|
|
@@ -57,6 +59,22 @@ The work splits into three layers, and each is written by a different hand at a
|
|
|
57
59
|
2. **Once per feature**, traits and verbs in the kit name that feature in domain words.
|
|
58
60
|
3. **In every test**, a spec tells a story with the kit's `it` and checks what a user would see.
|
|
59
61
|
|
|
62
|
+
### The verb shape every kit follows
|
|
63
|
+
|
|
64
|
+
A verb performs one action on the real engine or reads one observation. An action may return its real response, such as an HTTP response; it does not orchestrate a scenario and package the measurements for the spec. A readout reports what happened without deciding whether it was correct. The spec shows each action in order and states every expectation.
|
|
65
|
+
|
|
66
|
+
Acquire real services in `create` or a trait inside the story's per-test Scope, with dependencies provided by `layer`. Keep those services on the engine so verbs use the same running instance; scoped cleanup releases them after the story. Do not build and tear down services inside each verb. When closing a service is the action under test, expose that action separately from acquisition and observation.
|
|
67
|
+
|
|
68
|
+
| Shape to avoid | What to do instead |
|
|
69
|
+
| --- | --- |
|
|
70
|
+
| A verb runs a whole scenario and returns the measurements the spec asserts. | Give each action and observation its own verb; let the spec compose them and compare the observations. |
|
|
71
|
+
| A verb hard-codes the expected outcome or hides assertions. | Return the real response or observation, and put expected values and all assertions in the spec. |
|
|
72
|
+
| A verb does two jobs and returns both results. | Split the jobs into two verbs and show both calls in the spec. |
|
|
73
|
+
| A verb silently does nothing when its target is missing, such as `button?.click()`. | Fail with a message naming the missing target, then perform the action only once the target exists. |
|
|
74
|
+
| An engine default hides what the story declares, such as a lit oven, signed-in user or authorization header. | Start with a neutral engine and name the precondition in a trait or pass the input explicitly in the action. |
|
|
75
|
+
|
|
76
|
+
The bakery below starts with a cold oven and no dough. Its traits declare heat and dough, its baking action returns no report, and its readouts expose loaves and elapsed minutes separately. `runUntil` waits for that one engine action to complete; it does not replace a spec's sequence of actions.
|
|
77
|
+
|
|
60
78
|
### 1. Once per engine: the story kit
|
|
61
79
|
|
|
62
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.
|
|
@@ -97,8 +115,10 @@ export const bakery = storyKit({
|
|
|
97
115
|
},
|
|
98
116
|
},
|
|
99
117
|
oven: {
|
|
100
|
-
bakesEverything: (maxSteps?: number): Effect.Effect<
|
|
101
|
-
story.runUntil((current) => current.dough === 0, { maxSteps })
|
|
118
|
+
bakesEverything: (maxSteps?: number): Effect.Effect<void, BurntLoaf> =>
|
|
119
|
+
story.runUntil((current) => current.dough === 0, { maxSteps }),
|
|
120
|
+
loaves: () => state.loaves,
|
|
121
|
+
minutes: () => state.minute,
|
|
102
122
|
},
|
|
103
123
|
}),
|
|
104
124
|
});
|
|
@@ -118,7 +138,7 @@ This is the bakery kit this package tests itself with. `emptyBakery`, `stillWait
|
|
|
118
138
|
| `after` | Optional. A hook per stage that runs once that stage's traits are applied, even when no trait names the stage, so a value it derives is ready before a later stage reads it. Its typed failure fails the test. |
|
|
119
139
|
| `run` | Optional. What `story.runUntil` needs: `step(engine, tell)` advances the engine once and tells what happened, `maxSteps` caps one run, `failed(engine)` returns a sentence when the engine is in a state it cannot recover from, and `diagnose(engine)` says why it has not finished. |
|
|
120
140
|
| `inspect` | Optional. The part of the engine a failure prints. Without it, the failure prints the whole engine. |
|
|
121
|
-
| `verbs(engine, story)` |
|
|
141
|
+
| `verbs(engine, story)` | One action or one observation per verb. A verb that waits for a stepped engine returns the Effect from `story.runUntil`; the spec reads observations and asserts separately. |
|
|
122
142
|
|
|
123
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:
|
|
124
144
|
|
|
@@ -146,7 +166,9 @@ const app = storyKit({
|
|
|
146
166
|
|
|
147
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.
|
|
148
168
|
|
|
149
|
-
|
|
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.
|
|
170
|
+
|
|
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`.
|
|
150
172
|
|
|
151
173
|
#### Asserting on application traces
|
|
152
174
|
|
|
@@ -154,7 +176,7 @@ Start an in-process host in `create` or a stage's `after` hook, such as `after:
|
|
|
154
176
|
|
|
155
177
|
This lets an application's story assert that a request starts its own trace: a request without trace headers starts a root trace, and a request with `traceparent` continues that trace. Host startup and cleanup do not join the story's trace.
|
|
156
178
|
|
|
157
|
-
A kit can collect spans with a native in-memory tracer, or start a fake OTLP receiver and point the application's exporter at it.
|
|
179
|
+
A kit can collect spans with a native in-memory tracer, or start a fake OTLP receiver and point the application's exporter at it. Expose collection, exporter flush and any bounded wait as separate verbs. Assert only after the request span has ended. This small in-memory kit uses Effect's Web Request handler as its host:
|
|
158
180
|
|
|
159
181
|
```ts
|
|
160
182
|
import { Effect, Tracer } from "effect";
|
|
@@ -162,65 +184,69 @@ import { HttpEffect, HttpServerResponse } from "effect/http";
|
|
|
162
184
|
import { expect } from "vitest";
|
|
163
185
|
import { storyKit } from "@shivaedev/test-story/storyKit.ts";
|
|
164
186
|
|
|
165
|
-
const spans: Tracer.NativeSpan[] = [];
|
|
166
|
-
const tracer = Tracer.make({
|
|
167
|
-
span: (options) => {
|
|
168
|
-
const span = new Tracer.NativeSpan(options);
|
|
169
|
-
spans.push(span);
|
|
170
|
-
return span;
|
|
171
|
-
},
|
|
172
|
-
});
|
|
173
|
-
|
|
174
187
|
const app = storyKit({
|
|
175
|
-
around: (effect) => effect.pipe(Effect.withSpan("story")
|
|
188
|
+
around: (effect) => effect.pipe(Effect.withSpan("story")),
|
|
176
189
|
*create() {
|
|
177
|
-
const
|
|
178
|
-
|
|
190
|
+
const spans: Tracer.NativeSpan[] = [];
|
|
191
|
+
const tracer = Tracer.make({
|
|
192
|
+
span: (options) => {
|
|
193
|
+
const span = new Tracer.NativeSpan(options);
|
|
194
|
+
spans.push(span);
|
|
195
|
+
return span;
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
const context = yield* Effect.context<never>().pipe(Effect.withTracer(tracer));
|
|
199
|
+
const handler = HttpEffect.toWebHandlerWith(context)(Effect.succeed(HttpServerResponse.text("ok")));
|
|
200
|
+
return { handler, spans };
|
|
179
201
|
},
|
|
180
202
|
name: "traced application",
|
|
181
203
|
stages: ["ready"],
|
|
182
|
-
verbs: (handler) => ({
|
|
204
|
+
verbs: ({ handler, spans }) => ({
|
|
183
205
|
request: (headers: HeadersInit = {}) => Effect.promise(async () => {
|
|
184
206
|
const response = await handler(new Request("http://app.test/", { headers }));
|
|
185
|
-
|
|
207
|
+
return response.text();
|
|
208
|
+
}),
|
|
209
|
+
requestSpan: () => {
|
|
186
210
|
const span = spans.filter((span) => span.kind === "server").at(-1);
|
|
187
211
|
if (span === undefined) throw new Error("The request exported no server span");
|
|
188
212
|
return span;
|
|
189
|
-
}
|
|
213
|
+
},
|
|
190
214
|
}),
|
|
191
215
|
});
|
|
192
216
|
|
|
193
|
-
app.it("a request is its own root trace", [], function* ({ request }) {
|
|
217
|
+
app.it("a request is its own root trace", [], function* ({ request, requestSpan }) {
|
|
194
218
|
const storySpan = yield* Effect.currentSpan;
|
|
195
|
-
|
|
219
|
+
expect(yield* request()).toBe("ok");
|
|
220
|
+
const span = requestSpan();
|
|
196
221
|
expect(span.parent._tag).toBe("None");
|
|
197
222
|
expect(span.traceId).not.toBe(storySpan.traceId);
|
|
198
223
|
expect(span.status._tag).toBe("Ended");
|
|
199
224
|
});
|
|
200
225
|
|
|
201
|
-
app.it("a request continues its supplied trace", [], function* ({ request }) {
|
|
202
|
-
|
|
226
|
+
app.it("a request continues its supplied trace", [], function* ({ request, requestSpan }) {
|
|
227
|
+
yield* request({ traceparent: "00-1234567890abcdef1234567890abcdef-1234567890abcdef-01" });
|
|
228
|
+
const span = requestSpan();
|
|
203
229
|
expect(span.traceId).toBe("1234567890abcdef1234567890abcdef");
|
|
204
230
|
expect(span.parent).toMatchObject({ value: { spanId: "1234567890abcdef" } });
|
|
205
231
|
});
|
|
206
232
|
```
|
|
207
233
|
|
|
208
|
-
|
|
234
|
+
Detachment only removes the incoming parent: spans that application startup explicitly creates still follow native Effect semantics.
|
|
209
235
|
|
|
210
236
|
#### Reading OTLP exports
|
|
211
237
|
|
|
212
238
|
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:
|
|
213
239
|
|
|
214
240
|
```ts
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
expect(
|
|
220
|
-
|
|
241
|
+
app.it("exports the work span", [], function* ({ runWork, disposeRuntime, spans, requests }) {
|
|
242
|
+
yield* runWork();
|
|
243
|
+
yield* disposeRuntime();
|
|
244
|
+
expect(spans()).toContainEqual(expect.objectContaining({ name: "CountWords" }));
|
|
245
|
+
expect(requests().some(({ body }) => body.includes(Buffer.from("planted-secret")))).toBe(true);
|
|
246
|
+
});
|
|
221
247
|
```
|
|
222
248
|
|
|
223
|
-
`
|
|
249
|
+
Here `app` is the application's kit. Its `create` acquires the receiver and the real exporter pointed at that receiver, `runWork` performs the traced operation, `disposeRuntime` disposes the exporter's runtime and flushes pending spans, and `spans` and `requests` read the receiver's observations. Each action is visible in the spec. The receiver accepts OTLP/HTTP JSON and protobuf. `spans` flattens all exports into decoded trace and span IDs, parent span IDs, names, numeric OTLP kinds and status codes, attributes, events and links. Attributes retain OTLP key/value objects, including lossless decimal strings for integers; each span also has `resourceAttributes`. `requests` retains every request's unmodified `body` Buffer, parsed `headers`, original `rawHeaders` pairs and `path`. Search all body bytes when checking that a secret was not exported. Optional `path`, `status` (a number or attempt callback) and `answer` configure collector responses for failure stories.
|
|
224
250
|
|
|
225
251
|
For timeout stories, return `undefined` from the `status` callback to accept an export without sending response headers. Set `answer: null` to send headers without completing the response body.
|
|
226
252
|
|
|
@@ -258,8 +284,6 @@ const user = yield* Fiber.join(pending);
|
|
|
258
284
|
|
|
259
285
|
A `pages` route accepts a one-based `?page=` query and returns `{ items, next }`, with `next: null` on the last page. No query means page 1; a page past the end returns an empty list; a non-positive or non-integer page gets 400. `api.requests` reads a snapshot of the request log, including rejected and held requests, with `method`, `path` (including query), raw `body` Buffer, `headers`, `params` and `query`.
|
|
260
286
|
|
|
261
|
-
The [fake HTTP stories](https://github.com/ShivaeDev/platform/blob/main/packages/test-story/src/server/outboundApi.spec.ts) use Node fetch to check params, credentials, pages, one-shot failure, hold/release ordering, request logs, narration and listener cleanup.
|
|
262
|
-
|
|
263
287
|
### 2. Once per feature: traits and verbs
|
|
264
288
|
|
|
265
289
|
A trait is a line and a change, tagged with a stage:
|
|
@@ -303,7 +327,7 @@ help: a trait throws when the bakery it asks for cannot exist. Give the test tra
|
|
|
303
327
|
|
|
304
328
|
given the oven is lit
|
|
305
329
|
✗ given the baker has 4 balls of dough
|
|
306
|
-
refused at src/test-support/failingStories.ts:
|
|
330
|
+
refused at src/test-support/failingStories.ts:15:57
|
|
307
331
|
given the bakery has 1 bowls
|
|
308
332
|
|
|
309
333
|
The bakery when the test failed:
|
|
@@ -370,11 +394,11 @@ The package wraps between words or JSON fields toward 120 columns and prints the
|
|
|
370
394
|
|
|
371
395
|
### Library package kits
|
|
372
396
|
|
|
373
|
-
A library package has an engine too: its own services or Layer under test. Keep its kit in `src/test-support/`, acquire the real services in `create` with their dependencies supplied by the kit's `layer`, and use real PostgreSQL wherever the package touches it. Traits seed that engine in domain words;
|
|
397
|
+
A library package has an engine too: its own services or Layer under test. Keep its kit in `src/test-support/`, acquire the real services in `create` or a trait inside the story's per-test Scope, with their dependencies supplied by the kit's `layer`, and use real PostgreSQL wherever the package touches it. Traits seed that engine in domain words; each verb calls one action on the package's public API or reads one observation from that same engine. Verbs never create a fresh service just to run a scenario and return its measurements. The same division applies as for an application: the kit owns setup and wiring, and a spec names traits, shows every action through verbs and states each expectation. Following the [root test guidance](https://github.com/ShivaeDev/platform/blob/main/AGENTS.md), reserve plain Vitest tests for pure functions with no engine behind them.
|
|
374
398
|
|
|
375
399
|
The [`backfill` kit](https://github.com/ShivaeDev/platform/blob/main/packages/effect-test-sql/src/test-support/migrationStory.ts) in `effect-test-sql` is a library example. Its `create` returns the package's `migrationSteps(migrations)` API, its worker Layer provides PostgreSQL, and `around: withIsolatedDatabase` isolates each story. The `oldSchema` and `existingContacts` traits put contacts with email addresses into the old schema; the `backfill` verb calls `steps.migrateToLatest`. Its [story](https://github.com/ShivaeDev/platform/blob/main/packages/effect-test-sql/src/server/migrationStory.spec.ts) proves that those existing addresses survive the migration into a new table. It runs only when `PLATFORM_EFFECT_TEST_SQL_TEST_DATABASE_URL` is configured; a skipped story proves nothing.
|
|
376
400
|
|
|
377
|
-
A fake belongs only at a true external boundary, wired in `create`; keep the package's own engine real. The [resume kit](https://github.com/ShivaeDev/platform/blob/main/packages/effect-contract/src/test-support/resumeStory.ts), for example, creates synthetic browser and native event sources while running the package's real `resumeSignal` API.
|
|
401
|
+
A fake belongs only at a true external boundary, wired in `create`; keep the package's own engine real. The [resume kit](https://github.com/ShivaeDev/platform/blob/main/packages/effect-contract/src/test-support/resumeStory.ts), for example, creates synthetic browser and native event sources while running the package's real `resumeSignal` API.
|
|
378
402
|
|
|
379
403
|
### Whole-stack application kits
|
|
380
404
|
|
|
@@ -382,7 +406,7 @@ Keep one kit in the application’s `test-support/`. `effect-test` owns Vitest r
|
|
|
382
406
|
|
|
383
407
|
Set `runIf: hasPostgres` in the kit definition to gate all its stories once, where `hasPostgres` is a boolean such as `process.env.PLATFORM_EFFECT_SQL_TEST_DATABASE_URL !== undefined`. When false, `it`, `it.fails`, `it.only`, `it.skip`, `it.runIf` and `it.skipIf` all register skipped stories without acquiring the worker Layer or running `around`, creation, traits or bodies. A skipped `it.only` does not focus the file. When true or omitted, each variant keeps its usual Vitest behavior, including explicit skips and `fails` inverting a failure. Per-story conditions can skip more stories; they cannot enable a disabled kit. The engine, traits, verbs and services remain inferred from the definition.
|
|
384
408
|
|
|
385
|
-
The [order feature kit](https://github.com/ShivaeDev/platform/tree/main/packages/effect-react/src/test-support/order-example) runs the
|
|
409
|
+
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.
|
|
386
410
|
|
|
387
411
|
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.
|
|
388
412
|
|
|
@@ -390,7 +414,7 @@ For repository-level tests on a migrated template, `withTestTransaction` from `@
|
|
|
390
414
|
|
|
391
415
|
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.
|
|
392
416
|
|
|
393
|
-
|
|
417
|
+
For cookie sessions, use the DOM host setup below. Native `RpcTest` can exercise handlers in faster local stories, but omits serialization and transport. Failure inspection is synchronous: keep redacted observed values in the engine instead of trying to query PostgreSQL inside `inspect`.
|
|
394
418
|
|
|
395
419
|
### DOM stories with cookie sessions
|
|
396
420
|
|
|
@@ -439,39 +463,47 @@ const cookieApp = storyKit({
|
|
|
439
463
|
|
|
440
464
|
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.
|
|
441
465
|
|
|
442
|
-
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 `
|
|
466
|
+
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.
|
|
443
467
|
|
|
444
|
-
|
|
445
|
-
import { domNavigate } from "@shivaedev/test-story/domNavigate.ts";
|
|
468
|
+
The kit renders a Better Auth client whose `customFetchImpl` is `domFetch(baseUrl)`. The story calls `clickGitHubSignIn`, `settle` and `followRedirect` separately, then reads the heading, cookie and session. The screen driver settles on the screen's request state and reads the heading from the current document after navigation. The provider uses `fakeHttpService`. The authorize, token and user requests at the fake and a later session response provide evidence across the provider and application boundaries. The kit does not copy cookies into request headers.
|
|
446
469
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
signedInPage: () => Effect.promise(async () => ({
|
|
457
|
-
heading: document.querySelector("h1")?.textContent,
|
|
458
|
-
session: await (await engine.fetch("/api/auth/get-session")).json(),
|
|
459
|
-
})),
|
|
460
|
-
}),
|
|
470
|
+
```ts
|
|
471
|
+
socialApp.it("a social sign-in returns with a cookie session", [], function* ({ clickGitHubSignIn, settle, followRedirect, heading, cookie, session }) {
|
|
472
|
+
yield* clickGitHubSignIn();
|
|
473
|
+
yield* settle();
|
|
474
|
+
const response = yield* followRedirect();
|
|
475
|
+
expect(response.status).toBe(200);
|
|
476
|
+
expect(heading()).toBe("Signed in as alice@social.test");
|
|
477
|
+
expect(cookie()).not.toContain("session_token");
|
|
478
|
+
expect(yield* session()).toMatchObject({ user: { email: "alice@social.test" } });
|
|
461
479
|
});
|
|
480
|
+
```
|
|
462
481
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
482
|
+
`domFetch` uses the current window and its cookie jar; it does not isolate multiple users. Run stories using that window serially, and let the application's cleanup end its sessions, restore the page location and close its host. Cookies follow hostnames, not listener ports, so a different loopback port alone does not isolate accounts.
|
|
483
|
+
|
|
484
|
+
### Driving a screen
|
|
485
|
+
|
|
486
|
+
`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.
|
|
487
|
+
|
|
488
|
+
```ts
|
|
489
|
+
import { screenDriver } from "@shivaedev/test-story/screenDriver.ts";
|
|
490
|
+
|
|
491
|
+
verbs: ({ view, requests }, story) => screenDriver({
|
|
492
|
+
root: view.container,
|
|
493
|
+
pending: () => requests.pending,
|
|
494
|
+
tell: story.tell,
|
|
495
|
+
})
|
|
470
496
|
```
|
|
471
497
|
|
|
472
|
-
`
|
|
498
|
+
`click(selector)`, `enter(selector, value)` and `submit(selector = "form")` return Effects. Selectors identify exactly one descendant of the root; missing or ambiguous controls fail. Hidden targets and targets inside hidden ancestors refuse actions and readouts. Text entry focuses an input or textarea and dispatches input and change events. Disabled controls refuse clicks and entry, inert ancestors also prevent form submission, and read-only fields refuse entry. Actions do not wait for settlement, so a story can release a held request before calling `settle()`.
|
|
473
499
|
|
|
474
|
-
`
|
|
500
|
+
`text(selector?)` returns rendered text from the selected element or the whole root. `value(selector)` returns the current input or textarea value. Both narrate what they read through `tell` and leave assertions to the spec. Entry narration omits the supplied value.
|
|
501
|
+
|
|
502
|
+
`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.
|
|
503
|
+
|
|
504
|
+
For React, pass `act: async (action) => { await act(action); }` using React's `act`. The adapter wraps each action and flushes renderer work before checking the pending signal, including while waiting for an asynchronous response. The [React driver stories](https://github.com/ShivaeDev/platform/blob/main/packages/effect-react/src/screenDriver.dom.spec.tsx) cover a native pending atom, a rendered response and a controlled field without React warnings. The driver itself has no renderer dependency.
|
|
505
|
+
|
|
506
|
+
Settlement has a live-time limit of 3 seconds, configurable with the driver's `timeoutMs` or per call with `settle({ timeoutMs })`, and fails with the current screen text and advice when that limit expires. Use the existing DOM environment and scoped `routeOutbound` routing to reach `fakeHttpService`; the driver does not install another transport. The [screen stories](src/server/screenSignals.dom.spec.ts) hold a real HTTP response across repeated renders and sign in by filling and submitting a form against a fake identity provider. These fixtures prove DOM interactions and loopback HTTP, not a browser or a production authentication system.
|
|
475
507
|
|
|
476
508
|
### API by module
|
|
477
509
|
|
|
@@ -481,6 +513,8 @@ Import each module with the prefix `@shivaedev/test-story/`; there is no root en
|
|
|
481
513
|
| --- | --- |
|
|
482
514
|
| `storyKit.ts` | `storyKit`, `Trait`, `RunUntilOptions`, `Story`, `RunHooks`, `StoryKitDefinition`, `Told`, `StoryBody`, `StoryTest`, `StoryIt`, `StoryKit` |
|
|
483
515
|
| `domFetch.ts` | `domFetch(baseUrl)` |
|
|
516
|
+
| `routeOutbound.ts` | `routeOutbound(routes)` |
|
|
517
|
+
| `screenDriver.ts` | `screenDriver(options)`, `ScreenDriverOptions`, `ScreenDriver` |
|
|
484
518
|
| `domNavigate.ts` | `domNavigate(url?)` |
|
|
485
519
|
| `genreTags.ts` | `genreTags(...kitNames)`, `GenreTag` |
|
|
486
520
|
| `domEnvironment.config.ts` | Default Vitest environment preserving Node host globals and happy-dom document transport |
|
|
@@ -496,7 +530,7 @@ pnpm add --save-dev @shivaedev/test-story @effect/vitest@4.0.4 effect@4.0.4 vite
|
|
|
496
530
|
`@effect/vitest`, `effect` and `vitest` are peers, so the application's own versions run the stories.
|
|
497
531
|
|
|
498
532
|
- The package runs inside Vitest. The story reaches the failure by rewriting the first error's message in Vitest's `onTestFailed` hook.
|
|
499
|
-
- Layer acquisition and engine creation failures precede that hook and carry no story appendix. A printed rerun command still needs a Vitest configuration that collects its file
|
|
533
|
+
- Layer acquisition and engine creation failures precede that hook and carry no story appendix. A printed rerun command still needs a Vitest configuration that collects its file.
|
|
500
534
|
- A kit's `it` is created when the kit is defined, so a kit lives in a module that only Vitest test files import.
|
|
501
535
|
- `inspect` runs after the test has failed, so it reads the engine synchronously.
|
|
502
536
|
- The spec line under the `✗` is found by matching the spec's path in the failure's stack, or else in the call site of the last line the story told or the last `runUntil`. A failure raised inside an Effect that the spec does not call directly may have none.
|
|
@@ -7,7 +7,7 @@ const domEnvironment = {
|
|
|
7
7
|
// Vitest loads environments outside the project's source resolution conditions.
|
|
8
8
|
const load = createRequire(import.meta.url);
|
|
9
9
|
const { documentTransport } = load("#internal/documentTransport.ts");
|
|
10
|
-
const { installOutbound } = load("#internal/
|
|
10
|
+
const { installOutbound } = load("#internal/installOutbound.ts");
|
|
11
11
|
const native = Object.fromEntries(["fetch", "AbortController", "AbortSignal", "Headers", "Request", "Response"].map((name) => [name, Reflect.get(global, name)]));
|
|
12
12
|
const restoreOutbound = await installOutbound({}, true);
|
|
13
13
|
const win = new GlobalWindow({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"domEnvironment.config.js","sourceRoot":"","sources":["../../src/server/domEnvironment.config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,EAAoB,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAElE,MAAM,cAAc,GAAgB;IACnC,IAAI,EAAE,gBAAgB;IACtB,KAAK,CAAC,KAAK,CAAC,MAAyB,EAAE,EAAE,QAAQ,GAAG,EAAE,EAAE;QACvD,gFAAgF;QAChF,MAAM,IAAI,GAAG,aAAa,CAAC,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC;QAC5C,MAAM,EAAE,iBAAiB,EAAE,GAAoD,IAAI,CAAC,gCAAgC,CAAC,CAAC;QACtH,MAAM,EAAE,eAAe,EAAE,
|
|
1
|
+
{"version":3,"file":"domEnvironment.config.js","sourceRoot":"","sources":["../../src/server/domEnvironment.config.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"installOutbound.d.ts","sourceRoot":"","sources":["../../../src/server/internal/installOutbound.ts"],"names":[],"mappings":"AAeA,wBAAsB,eAAe,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,EAAE,MAAM,UAAQ,uBAgD7F"}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { isIP } from "node:net";
|
|
2
|
-
import { Effect } from "effect";
|
|
3
2
|
import { unrouted } from "#internal/errors.ts";
|
|
4
3
|
function isDispatcher(value) {
|
|
5
4
|
return typeof value === "object" && value !== null && "dispatch" in value && typeof value.dispatch === "function";
|
|
@@ -52,7 +51,4 @@ export async function installOutbound(routes, strict = false) {
|
|
|
52
51
|
}
|
|
53
52
|
};
|
|
54
53
|
}
|
|
55
|
-
|
|
56
|
-
return Effect.acquireRelease(Effect.promise(() => installOutbound(routes, Reflect.get(globalThis, Symbol.for("test-story.dom")) === true)), (restore) => Effect.sync(restore)).pipe(Effect.asVoid);
|
|
57
|
-
}
|
|
58
|
-
//# sourceMappingURL=routeOutbound.js.map
|
|
54
|
+
//# sourceMappingURL=installOutbound.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"installOutbound.js","sourceRoot":"","sources":["../../../src/server/internal/installOutbound.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare function screenElements(root: () => HTMLElement): {
|
|
2
|
+
element: (selector: string) => HTMLElement;
|
|
3
|
+
enabled: (found: HTMLElement, selector: string) => void;
|
|
4
|
+
enter: (selector: string, value: string) => void;
|
|
5
|
+
form: (selector: string) => HTMLFormElement;
|
|
6
|
+
input: (selector: string) => HTMLInputElement | HTMLTextAreaElement;
|
|
7
|
+
rendered: (found: HTMLElement, selector: string) => HTMLElement;
|
|
8
|
+
};
|
|
9
|
+
//# sourceMappingURL=screenElements.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"screenElements.d.ts","sourceRoot":"","sources":["../../../src/server/internal/screenElements.ts"],"names":[],"mappings":"AAAA,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,WAAW;wBAa1B,MAAM;qBAiBT,WAAW,YAAY,MAAM;sBAQlC,MAAM,SAAS,MAAM;qBAYtB,MAAM;sBA3BC,MAAM;sBAtBN,WAAW,YAAY,MAAM;EA4DtD"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export function screenElements(root) {
|
|
2
|
+
function rendered(found, selector) {
|
|
3
|
+
const visibility = getComputedStyle(found).visibility;
|
|
4
|
+
let hidden = visibility === "hidden" || visibility === "collapse";
|
|
5
|
+
for (let ancestor = found; ancestor; ancestor = ancestor.parentElement) {
|
|
6
|
+
const style = getComputedStyle(ancestor);
|
|
7
|
+
hidden ||= Boolean(ancestor.hidden) || style.display === "none" || style.contentVisibility === "hidden";
|
|
8
|
+
}
|
|
9
|
+
if (hidden) {
|
|
10
|
+
throw new Error(`The screen's ${selector} is not rendered.\nhelp: Reveal the target and its ancestors before acting on or reading it.`);
|
|
11
|
+
}
|
|
12
|
+
return found;
|
|
13
|
+
}
|
|
14
|
+
function element(selector) {
|
|
15
|
+
const matches = root().querySelectorAll(selector);
|
|
16
|
+
const found = matches[0];
|
|
17
|
+
if (matches.length !== 1 || found === undefined) {
|
|
18
|
+
throw new Error(`The screen has ${matches.length} matches for ${JSON.stringify(selector)}.\nhelp: Use a selector for exactly one rendered control or readout.`);
|
|
19
|
+
}
|
|
20
|
+
return rendered(found, selector);
|
|
21
|
+
}
|
|
22
|
+
function input(selector) {
|
|
23
|
+
const found = element(selector);
|
|
24
|
+
if (!(found instanceof HTMLInputElement || found instanceof HTMLTextAreaElement)) {
|
|
25
|
+
throw new Error(`The screen's ${selector} is not a text field.\nhelp: Select a rendered input or textarea to enter or read a value.`);
|
|
26
|
+
}
|
|
27
|
+
return found;
|
|
28
|
+
}
|
|
29
|
+
function enabled(found, selector) {
|
|
30
|
+
if (found.matches(":disabled") || found.closest("[inert]")) {
|
|
31
|
+
throw new Error(`The screen's ${selector} is disabled.\nhelp: Wait for the app to enable the control before acting on it.`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
element,
|
|
36
|
+
enabled,
|
|
37
|
+
enter: (selector, value) => {
|
|
38
|
+
const field = input(selector);
|
|
39
|
+
enabled(field, selector);
|
|
40
|
+
if (field.readOnly) {
|
|
41
|
+
throw new Error(`The screen's ${selector} is read-only.\nhelp: Select an editable input or textarea.`);
|
|
42
|
+
}
|
|
43
|
+
field.focus();
|
|
44
|
+
const prototype = field instanceof HTMLInputElement ? HTMLInputElement.prototype : HTMLTextAreaElement.prototype;
|
|
45
|
+
Object.getOwnPropertyDescriptor(prototype, "value")?.set?.call(field, value);
|
|
46
|
+
field.dispatchEvent(new Event("input", { bubbles: true }));
|
|
47
|
+
field.dispatchEvent(new Event("change", { bubbles: true }));
|
|
48
|
+
},
|
|
49
|
+
form: (selector) => {
|
|
50
|
+
const found = element(selector);
|
|
51
|
+
if (!(found instanceof HTMLFormElement)) {
|
|
52
|
+
throw new Error(`The screen's ${selector} is not a form.\nhelp: Select a rendered form to submit.`);
|
|
53
|
+
}
|
|
54
|
+
enabled(found, selector);
|
|
55
|
+
return found;
|
|
56
|
+
},
|
|
57
|
+
input,
|
|
58
|
+
rendered,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=screenElements.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"screenElements.js","sourceRoot":"","sources":["../../../src/server/internal/screenElements.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Effect, type Scope } from "effect";
|
|
2
|
-
import type { AnyTestLayer } from "@shivaedev/effect-test/
|
|
2
|
+
import type { AnyTestLayer } from "@shivaedev/effect-test/anyTestLayer.ts";
|
|
3
3
|
import type { EffectClock } from "@shivaedev/effect-test/types.ts";
|
|
4
4
|
import { type AnyEffect, type EffectHook, type Hook } from "#internal/hooked.ts";
|
|
5
5
|
import { type Narration } from "#internal/narration.ts";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"seed.d.ts","sourceRoot":"","sources":["../../../src/server/internal/seed.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkB,MAAM,EAAE,KAAK,KAAK,EAAU,MAAM,QAAQ,CAAC;AACpE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"seed.d.ts","sourceRoot":"","sources":["../../../src/server/internal/seed.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkB,MAAM,EAAE,KAAK,KAAK,EAAU,MAAM,QAAQ,CAAC;AACpE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,wCAAwC,CAAC;AAC3E,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iCAAiC,CAAC;AAEnE,OAAO,EAAE,KAAK,SAAS,EAAE,KAAK,UAAU,EAAE,KAAK,IAAI,EAAU,MAAM,qBAAqB,CAAC;AACzF,OAAO,EAAE,KAAK,SAAS,EAAa,MAAM,wBAAwB,CAAC;AACnE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AACjD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAEpD,MAAM,MAAM,UAAU,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC;AAEnG,MAAM,WAAW,UAAU;IAC1B,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,QAAQ,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,SAAS,EAAE,OAAO,CAAC,EAAE,OAAO,CAAC,EAAE,eAAe,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;IAC5I,QAAQ,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CACtC;AAED,MAAM,WAAW,eAAe;IAC/B,QAAQ,CAAC,KAAK,CAAC,EAAE,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,SAAS,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;IACnG,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,KAAK,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC;IAC1G,QAAQ,CAAC,KAAK,CAAC,EAAE,WAAW,CAAC;IAC7B,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC,EAAE,EAAE,SAAS,EAAE,OAAO,CAAC,CAAC;IACpD,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,OAAO,CAAC;IAChD,QAAQ,CAAC,KAAK,CAAC,EAAE,YAAY,CAAC;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACrD,QAAQ,CAAC,GAAG,CAAC,EAAE,aAAa,CAAC,OAAO,CAAC,CAAC;IACtC,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,QAAQ,CAAC,KAAK,EAAE,CAAC,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,UAAU,KAAK,MAAM,CAAC;CAC/D;AAED,eAAO,MAAM,IAAI,6IAuBf,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"start.d.ts","sourceRoot":"","sources":["../../../src/server/internal/start.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkB,MAAM,EAAU,MAAM,QAAQ,CAAC;AACxD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAC;AAK1C,OAAO,EAAE,KAAK,eAAe,EAAmB,KAAK,UAAU,EAAQ,MAAM,mBAAmB,CAAC;AAEjG,eAAO,MAAM,KAAK,iGAIJ,MAAM,KAAK,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,
|
|
1
|
+
{"version":3,"file":"start.d.ts","sourceRoot":"","sources":["../../../src/server/internal/start.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkB,MAAM,EAAU,MAAM,QAAQ,CAAC;AACxD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAC;AAK1C,OAAO,EAAE,KAAK,eAAe,EAAmB,KAAK,UAAU,EAAQ,MAAM,mBAAmB,CAAC;AAEjG,eAAO,MAAM,KAAK,iGAIJ,MAAM,KAAK,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,6CA0B/D,CAAC"}
|
|
@@ -19,7 +19,10 @@ export const start = Effect.fnUntraced(function* (definition, given, context, bo
|
|
|
19
19
|
},
|
|
20
20
|
tell: narration.tell,
|
|
21
21
|
};
|
|
22
|
-
return yield* body(
|
|
22
|
+
return yield* body(Object.defineProperties({}, {
|
|
23
|
+
...Object.getOwnPropertyDescriptors(definition.verbs(engine, story)),
|
|
24
|
+
story: { enumerable: true, value: story },
|
|
25
|
+
}));
|
|
23
26
|
}).pipe(Effect.tapCause((cause) => Effect.sync(() => captureFailure(Cause.prettyErrors(cause)[0]?.stack))));
|
|
24
27
|
});
|
|
25
28
|
//# sourceMappingURL=start.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"start.js","sourceRoot":"","sources":["../../../src/server/internal/start.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAExD,OAAO,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AACjD,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAC7C,OAAO,EAAE,OAAO,EAAE,MAAM,wBAAwB,CAAC;AACjD,OAAO,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AACjD,OAAO,EAA0D,IAAI,EAAE,MAAM,mBAAmB,CAAC;AAEjG,MAAM,CAAC,MAAM,KAAK,GAAG,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,EAC/C,UAA2B,EAC3B,KAA4B,EAC5B,OAAoB,EACpB,IAAgE;IAEhE,MAAM,MAAM,GAAG,KAAK,CAAC,CAAC,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;IAC5G,MAAM,EAAE,SAAS,EAAE,cAAc,EAAE,GAAG,OAAO,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,CAAC,OAAO,EAAE,IAAI,EAAE,UAAU,CAAC,IAAI,EAAE,EAAE,OAAO,CAAC,CAAC;IACvH,OAAO,KAAK,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;QACjC,KAAK,CAAC,CAAC,IAAI,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,KAAK,CAAC,CAAC;QAClD,MAAM,KAAK,GAAe;YACzB,MAAM;YACN,KAAK,EAAE,SAAS,CAAC,KAAK;YACtB,QAAQ,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE;gBAC5B,MAAM,IAAI,GAAG,QAAQ,EAAE,CAAC;gBACxB,SAAS,CAAC,EAAE,GAAG,IAAI,CAAC;gBACpB,OAAO,QAAQ,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,CAAC,IAAI,EAAE,GAAG,EAAE,UAAU,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,SAAS,CAAC,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC;YACvI,CAAC;YACD,IAAI,EAAE,SAAS,CAAC,IAAI;SACpB,CAAC;QACF,OAAO,KAAK,CAAC,CAAC,IAAI,CAAC,EAAE,GAAG,UAAU,CAAC,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,EAAE,KAAK,EAAE,
|
|
1
|
+
{"version":3,"file":"start.js","sourceRoot":"","sources":["../../../src/server/internal/start.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAExD,OAAO,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AACjD,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAC7C,OAAO,EAAE,OAAO,EAAE,MAAM,wBAAwB,CAAC;AACjD,OAAO,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AACjD,OAAO,EAA0D,IAAI,EAAE,MAAM,mBAAmB,CAAC;AAEjG,MAAM,CAAC,MAAM,KAAK,GAAG,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,EAC/C,UAA2B,EAC3B,KAA4B,EAC5B,OAAoB,EACpB,IAAgE;IAEhE,MAAM,MAAM,GAAG,KAAK,CAAC,CAAC,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;IAC5G,MAAM,EAAE,SAAS,EAAE,cAAc,EAAE,GAAG,OAAO,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,CAAC,OAAO,EAAE,IAAI,EAAE,UAAU,CAAC,IAAI,EAAE,EAAE,OAAO,CAAC,CAAC;IACvH,OAAO,KAAK,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;QACjC,KAAK,CAAC,CAAC,IAAI,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,KAAK,CAAC,CAAC;QAClD,MAAM,KAAK,GAAe;YACzB,MAAM;YACN,KAAK,EAAE,SAAS,CAAC,KAAK;YACtB,QAAQ,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE;gBAC5B,MAAM,IAAI,GAAG,QAAQ,EAAE,CAAC;gBACxB,SAAS,CAAC,EAAE,GAAG,IAAI,CAAC;gBACpB,OAAO,QAAQ,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,CAAC,IAAI,EAAE,GAAG,EAAE,UAAU,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,SAAS,CAAC,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC;YACvI,CAAC;YACD,IAAI,EAAE,SAAS,CAAC,IAAI;SACpB,CAAC;QACF,OAAO,KAAK,CAAC,CAAC,IAAI,CACjB,MAAM,CAAC,gBAAgB,CACtB,EAAE,EACF;YACC,GAAG,MAAM,CAAC,yBAAyB,CAAC,UAAU,CAAC,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;YACpE,KAAK,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE;SACzC,CACD,CACD,CAAC;IACH,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,cAAc,CAAC,KAAK,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;AAC7G,CAAC,CAAC,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"storyIt.d.ts","sourceRoot":"","sources":["../../../src/server/internal/storyIt.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAElD,OAAO,KAAK,EAAc,iBAAiB,EAAE,MAAM,iCAAiC,CAAC;AAGrF,OAAO,EAAE,KAAK,SAAS,EAAU,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"storyIt.d.ts","sourceRoot":"","sources":["../../../src/server/internal/storyIt.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAElD,OAAO,KAAK,EAAc,iBAAiB,EAAE,MAAM,iCAAiC,CAAC;AAGrF,OAAO,EAAE,KAAK,SAAS,EAAU,MAAM,qBAAqB,CAAC;AAC7D,OAAO,KAAK,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAIrE,KAAK,SAAS,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,KAAK,SAAS,CAAC,SAAS,EAAE,OAAO,EAAE,KAAK,CAAC,GAAG,SAAS,CAAC;AAE1G,KAAK,SAAS,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,UAAU,EAAE,EAAE,IAAI,CAAC,EAAE,SAAS,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,iBAAiB,KAAK,IAAI,CAAC;AAE9H,MAAM,WAAW,OAAQ,SAAQ,SAAS;IACzC,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,KAAK,EAAE,CAAC,SAAS,EAAE,OAAO,KAAK,SAAS,CAAC;IAClD,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,CAAC,SAAS,EAAE,OAAO,KAAK,SAAS,CAAC;CACnD;AAgCD,wBAAgB,OAAO,CAAC,UAAU,EAAE,eAAe,GAAG,OAAO,CA0B5D"}
|
|
@@ -2,8 +2,8 @@ import { Effect, Layer } from "effect";
|
|
|
2
2
|
import { makeEffectIt } from "@shivaedev/effect-test/vitest.ts";
|
|
3
3
|
import { genreOf } from "#internal/genre.ts";
|
|
4
4
|
import { hooked } from "#internal/hooked.ts";
|
|
5
|
-
import { routeOutbound } from "#internal/routeOutbound.ts";
|
|
6
5
|
import { start } from "#internal/start.ts";
|
|
6
|
+
import { routeOutbound } from "#routeOutbound.ts";
|
|
7
7
|
function withTags(options, tags) {
|
|
8
8
|
if (typeof options === "number") {
|
|
9
9
|
return { tags: [...tags], timeout: options };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"storyIt.js","sourceRoot":"","sources":["../../../src/server/internal/storyIt.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAC;AAEvC,OAAO,EAAE,YAAY,EAAE,MAAM,kCAAkC,CAAC;AAChE,OAAO,EAAc,OAAO,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,EAAkB,MAAM,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"storyIt.js","sourceRoot":"","sources":["../../../src/server/internal/storyIt.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
1
|
import { Effect } from "effect";
|
|
2
|
-
export declare function installOutbound(routes: Readonly<Record<string, string>>, strict?: boolean): Promise<() => void>;
|
|
3
2
|
export declare function routeOutbound(routes: Readonly<Record<string, string>>): Effect.Effect<void, never, import("effect/Scope").Scope>;
|
|
4
3
|
//# sourceMappingURL=routeOutbound.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routeOutbound.d.ts","sourceRoot":"","sources":["../../src/server/routeOutbound.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAGhC,wBAAgB,aAAa,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,4DAKrE"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { installOutbound } from "#internal/installOutbound.ts";
|
|
3
|
+
export function routeOutbound(routes) {
|
|
4
|
+
return Effect.acquireRelease(Effect.promise(() => installOutbound(routes, Reflect.get(globalThis, Symbol.for("test-story.dom")) === true)), (restore) => Effect.sync(restore)).pipe(Effect.asVoid);
|
|
5
|
+
}
|
|
6
|
+
//# sourceMappingURL=routeOutbound.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routeOutbound.js","sourceRoot":"","sources":["../../src/server/routeOutbound.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,OAAO,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC;AAE/D,MAAM,UAAU,aAAa,CAAC,MAAwC;IACrE,OAAO,MAAM,CAAC,cAAc,CAC3B,MAAM,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,eAAe,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC,UAAU,EAAE,MAAM,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,EAC7G,CAAC,OAAO,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CACjC,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;AACvB,CAAC"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
export interface ScreenDriverOptions {
|
|
3
|
+
readonly act?: (action: () => Promise<void>) => PromiseLike<void>;
|
|
4
|
+
readonly pending: () => boolean;
|
|
5
|
+
readonly root: HTMLElement | (() => HTMLElement);
|
|
6
|
+
readonly tell: (line: string) => void;
|
|
7
|
+
readonly timeoutMs?: number;
|
|
8
|
+
}
|
|
9
|
+
export type ScreenDriver = ReturnType<typeof screenDriver>;
|
|
10
|
+
export declare function screenDriver({ root, pending, tell, act, timeoutMs }: ScreenDriverOptions): {
|
|
11
|
+
accessible: (selector: string) => {
|
|
12
|
+
name: string;
|
|
13
|
+
role: string | null;
|
|
14
|
+
};
|
|
15
|
+
click: (selector: string) => Effect.Effect<void, never, never>;
|
|
16
|
+
enter: (selector: string, value: string) => Effect.Effect<void, never, never>;
|
|
17
|
+
settle: (options?: {
|
|
18
|
+
readonly timeoutMs?: number;
|
|
19
|
+
}) => Effect.Effect<void, never, never>;
|
|
20
|
+
submit: (selector?: string) => Effect.Effect<void, never, never>;
|
|
21
|
+
text: (selector?: string) => string;
|
|
22
|
+
value: (selector: string) => string;
|
|
23
|
+
};
|
|
24
|
+
//# sourceMappingURL=screenDriver.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"screenDriver.d.ts","sourceRoot":"","sources":["../../src/server/screenDriver.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAIhC,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;2BAgC9F,MAAM;;;;sBAMX,MAAM;sBAON,MAAM,SAAS,MAAM;uBApCf;QAAE,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE;;sBAuCrC,MAAM;sBAKN,MAAM;EAMzB"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { computeAccessibleName, getRole } from "dom-accessibility-api";
|
|
2
|
+
import { Effect } from "effect";
|
|
3
|
+
import { withLive } from "@shivaedev/effect-test/withLive.ts";
|
|
4
|
+
import { screenElements } from "#internal/screenElements.ts";
|
|
5
|
+
export function screenDriver({ root, pending, tell, act = (action) => action(), timeoutMs = 3000 }) {
|
|
6
|
+
const currentRoot = typeof root === "function" ? root : () => root;
|
|
7
|
+
const elements = screenElements(currentRoot);
|
|
8
|
+
function perform(line, action) {
|
|
9
|
+
return Effect.promise(async () => {
|
|
10
|
+
tell(line);
|
|
11
|
+
await act(async () => action());
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
function settle(options = {}) {
|
|
15
|
+
const limit = options.timeoutMs ?? timeoutMs;
|
|
16
|
+
const wait = Effect.gen(function* () {
|
|
17
|
+
tell("wait for the screen's pending work");
|
|
18
|
+
do {
|
|
19
|
+
yield* Effect.promise(() => act(async () => undefined));
|
|
20
|
+
if (!pending()) {
|
|
21
|
+
tell("the screen has no pending work");
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
yield* Effect.promise(() => act(() => new Promise((resolve) => setTimeout(resolve, 10))));
|
|
25
|
+
} while (true);
|
|
26
|
+
});
|
|
27
|
+
function timedOut() {
|
|
28
|
+
return Effect.die(new Error(`The screen did not settle within ${limit}ms.\nhelp: Release held requests and check the app's pending signal and renderer act adapter.\nScreen: ${currentRoot().textContent}`));
|
|
29
|
+
}
|
|
30
|
+
return withLive(wait.pipe(Effect.timeoutOrElse({ duration: limit, orElse: timedOut })));
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
accessible: (selector) => {
|
|
34
|
+
const element = elements.element(selector);
|
|
35
|
+
const accessible = { name: computeAccessibleName(element), role: getRole(element) };
|
|
36
|
+
tell(`read the accessibility of ${selector}: ${JSON.stringify(accessible)}`);
|
|
37
|
+
return accessible;
|
|
38
|
+
},
|
|
39
|
+
click: (selector) => perform(`click ${selector}`, () => {
|
|
40
|
+
const element = elements.element(selector);
|
|
41
|
+
elements.enabled(element, selector);
|
|
42
|
+
element.focus();
|
|
43
|
+
element.click();
|
|
44
|
+
}),
|
|
45
|
+
enter: (selector, value) => perform(`enter a value in ${selector}`, () => elements.enter(selector, value)),
|
|
46
|
+
settle,
|
|
47
|
+
submit: (selector = "form") => perform(`submit ${selector}`, () => elements.form(selector).requestSubmit()),
|
|
48
|
+
text: (selector) => {
|
|
49
|
+
const text = (selector === undefined ? elements.rendered(currentRoot(), "root") : elements.element(selector)).innerText;
|
|
50
|
+
tell(`read ${selector ?? "the screen"}: ${JSON.stringify(text)}`);
|
|
51
|
+
return text;
|
|
52
|
+
},
|
|
53
|
+
value: (selector) => {
|
|
54
|
+
const value = elements.input(selector).value;
|
|
55
|
+
tell(`read the value in ${selector}: ${JSON.stringify(value)}`);
|
|
56
|
+
return value;
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=screenDriver.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"screenDriver.js","sourceRoot":"","sources":["../../src/server/screenDriver.ts"],"names":[],"mappings":"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"}
|
|
@@ -37,7 +37,7 @@ export interface StoryKitDefinition<TEngine, TStage extends string, TVerbs, TPro
|
|
|
37
37
|
readonly outbound?: Readonly<Record<string, string>>;
|
|
38
38
|
readonly run?: RunHooks<TEngine, TStep, TFailed, TDiagnose>;
|
|
39
39
|
readonly runIf?: boolean;
|
|
40
|
-
readonly stages: readonly
|
|
40
|
+
readonly stages: readonly TStage[];
|
|
41
41
|
readonly verbs: (engine: TEngine, story: Story<TEngine, TStep | TFailed | TDiagnose>) => TVerbs;
|
|
42
42
|
}
|
|
43
43
|
export type Told<TEngine, TVerbs, TRun extends AnyEffect> = TVerbs & {
|
|
@@ -60,8 +60,8 @@ export interface StoryKit<TEngine, TStage extends string, TVerbs, TRun extends A
|
|
|
60
60
|
};
|
|
61
61
|
readonly traits: <const TGiven extends readonly Trait<TEngine, TStage, unknown>[]>(...given: TGiven) => Trait<TEngine, TStage, TraitServices<TGiven[number]>>;
|
|
62
62
|
}
|
|
63
|
-
export declare function storyKit<TCreate extends () => unknown, const TStage extends string, TVerbs extends object, TProvided = never, TAfter extends AfterResults<TStage> = Record<never, never>, TStep extends AnyEffect = never, TFailed extends AnyEffect = never, TDiagnose extends AnyEffect = never>(definition: InferredDefinition<TCreate, TStage, TVerbs, TProvided, TAfter, TStep, TFailed, TDiagnose>): StoryKit<HookValue<ReturnType<TCreate>>, TStage, TVerbs, TStep | TFailed | TDiagnose, TProvided>;
|
|
64
|
-
export declare function storyKit<TEngine, const TStage extends string, TVerbs extends object, TProvided = never, TCreate extends AnyEffect = never, TAfter extends {
|
|
63
|
+
export declare function storyKit<TCreate extends () => unknown, const TStage extends string = never, TVerbs extends object = object, TProvided = never, TAfter extends AfterResults<TStage> = Record<never, never>, TStep extends AnyEffect = never, TFailed extends AnyEffect = never, TDiagnose extends AnyEffect = never>(definition: InferredDefinition<TCreate, TStage, TVerbs, TProvided, TAfter, TStep, TFailed, TDiagnose>): StoryKit<HookValue<ReturnType<TCreate>>, TStage, TVerbs, TStep | TFailed | TDiagnose, TProvided>;
|
|
64
|
+
export declare function storyKit<TEngine, const TStage extends string = never, TVerbs extends object = object, TProvided = never, TCreate extends AnyEffect = never, TAfter extends {
|
|
65
65
|
readonly [TKey in TStage]?: AnyEffect;
|
|
66
66
|
} = Record<never, never>, TStep extends AnyEffect = never, TFailed extends AnyEffect = never, TDiagnose extends AnyEffect = never>(definition: LegacyDefinition<TEngine, TStage, TVerbs, TProvided, TCreate, TAfter, TStep, TFailed, TDiagnose>): StoryKit<TEngine, TStage, TVerbs, TStep | TFailed | TDiagnose, TProvided>;
|
|
67
67
|
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"storyKit.d.ts","sourceRoot":"","sources":["../../src/server/storyKit.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"storyKit.d.ts","sourceRoot":"","sources":["../../src/server/storyKit.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.8.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,9 +24,8 @@
|
|
|
24
24
|
},
|
|
25
25
|
"exports": {
|
|
26
26
|
"./*.ts": {
|
|
27
|
-
"types": "./dist/server/*.d.ts",
|
|
28
27
|
"source": "./src/server/*.ts",
|
|
29
|
-
"
|
|
28
|
+
"types": "./dist/server/*.d.ts",
|
|
30
29
|
"default": "./dist/server/*.js"
|
|
31
30
|
},
|
|
32
31
|
"./internal/*": null,
|
|
@@ -38,21 +37,20 @@
|
|
|
38
37
|
"!src/**/*.test.ts",
|
|
39
38
|
"!src/**/*.spec.ts",
|
|
40
39
|
"!src/**/test-support",
|
|
41
|
-
"docs",
|
|
42
40
|
"CHANGELOG.md",
|
|
43
41
|
"README.md"
|
|
44
42
|
],
|
|
45
43
|
"dependencies": {
|
|
46
|
-
"@shivaedev/effect-test": "0.
|
|
44
|
+
"@shivaedev/effect-test": "0.6.0",
|
|
45
|
+
"dom-accessibility-api": "0.7.1"
|
|
47
46
|
},
|
|
48
47
|
"devDependencies": {
|
|
49
48
|
"@effect/vitest": "4.0.4",
|
|
50
|
-
"@shivaedev/quality": "0.
|
|
49
|
+
"@shivaedev/quality": "0.26.0",
|
|
51
50
|
"@types/node": "24.19.2",
|
|
52
|
-
"@typescript/native": "npm:typescript@7.0.2",
|
|
53
51
|
"effect": "4.0.4",
|
|
54
52
|
"happy-dom": "20.14.6",
|
|
55
|
-
"typescript": "
|
|
53
|
+
"typescript": "7.0.2",
|
|
56
54
|
"vitest": "5.0.3"
|
|
57
55
|
},
|
|
58
56
|
"peerDependencies": {
|
|
@@ -8,7 +8,7 @@ const domEnvironment: Environment = {
|
|
|
8
8
|
// Vitest loads environments outside the project's source resolution conditions.
|
|
9
9
|
const load = createRequire(import.meta.url);
|
|
10
10
|
const { documentTransport }: typeof import("#internal/documentTransport.ts") = load("#internal/documentTransport.ts");
|
|
11
|
-
const { installOutbound }: typeof import("#internal/
|
|
11
|
+
const { installOutbound }: typeof import("#internal/installOutbound.ts") = load("#internal/installOutbound.ts");
|
|
12
12
|
const native = Object.fromEntries(
|
|
13
13
|
["fetch", "AbortController", "AbortSignal", "Headers", "Request", "Response"].map((name) => [name, Reflect.get(global, name)]),
|
|
14
14
|
);
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { isIP } from "node:net";
|
|
2
|
-
import { Effect } from "effect";
|
|
3
2
|
import { unrouted } from "#internal/errors.ts";
|
|
4
3
|
|
|
5
4
|
interface DispatchOptions {
|
|
@@ -63,10 +62,3 @@ export async function installOutbound(routes: Readonly<Record<string, string>>,
|
|
|
63
62
|
}
|
|
64
63
|
};
|
|
65
64
|
}
|
|
66
|
-
|
|
67
|
-
export function routeOutbound(routes: Readonly<Record<string, string>>) {
|
|
68
|
-
return Effect.acquireRelease(
|
|
69
|
-
Effect.promise(() => installOutbound(routes, Reflect.get(globalThis, Symbol.for("test-story.dom")) === true)),
|
|
70
|
-
(restore) => Effect.sync(restore),
|
|
71
|
-
).pipe(Effect.asVoid);
|
|
72
|
-
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export function screenElements(root: () => HTMLElement) {
|
|
2
|
+
function rendered(found: HTMLElement, selector: string) {
|
|
3
|
+
const visibility = getComputedStyle(found).visibility;
|
|
4
|
+
let hidden = visibility === "hidden" || visibility === "collapse";
|
|
5
|
+
for (let ancestor: HTMLElement | null = found; ancestor; ancestor = ancestor.parentElement) {
|
|
6
|
+
const style = getComputedStyle(ancestor);
|
|
7
|
+
hidden ||= Boolean(ancestor.hidden) || style.display === "none" || style.contentVisibility === "hidden";
|
|
8
|
+
}
|
|
9
|
+
if (hidden) {
|
|
10
|
+
throw new Error(`The screen's ${selector} is not rendered.\nhelp: Reveal the target and its ancestors before acting on or reading it.`);
|
|
11
|
+
}
|
|
12
|
+
return found;
|
|
13
|
+
}
|
|
14
|
+
function element(selector: string) {
|
|
15
|
+
const matches = root().querySelectorAll<HTMLElement>(selector);
|
|
16
|
+
const found = matches[0];
|
|
17
|
+
if (matches.length !== 1 || found === undefined) {
|
|
18
|
+
throw new Error(
|
|
19
|
+
`The screen has ${matches.length} matches for ${JSON.stringify(selector)}.\nhelp: Use a selector for exactly one rendered control or readout.`,
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
return rendered(found, selector);
|
|
23
|
+
}
|
|
24
|
+
function input(selector: string) {
|
|
25
|
+
const found = element(selector);
|
|
26
|
+
if (!(found instanceof HTMLInputElement || found instanceof HTMLTextAreaElement)) {
|
|
27
|
+
throw new Error(`The screen's ${selector} is not a text field.\nhelp: Select a rendered input or textarea to enter or read a value.`);
|
|
28
|
+
}
|
|
29
|
+
return found;
|
|
30
|
+
}
|
|
31
|
+
function enabled(found: HTMLElement, selector: string) {
|
|
32
|
+
if (found.matches(":disabled") || found.closest("[inert]")) {
|
|
33
|
+
throw new Error(`The screen's ${selector} is disabled.\nhelp: Wait for the app to enable the control before acting on it.`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
element,
|
|
38
|
+
enabled,
|
|
39
|
+
enter: (selector: string, value: string) => {
|
|
40
|
+
const field = input(selector);
|
|
41
|
+
enabled(field, selector);
|
|
42
|
+
if (field.readOnly) {
|
|
43
|
+
throw new Error(`The screen's ${selector} is read-only.\nhelp: Select an editable input or textarea.`);
|
|
44
|
+
}
|
|
45
|
+
field.focus();
|
|
46
|
+
const prototype = field instanceof HTMLInputElement ? HTMLInputElement.prototype : HTMLTextAreaElement.prototype;
|
|
47
|
+
Object.getOwnPropertyDescriptor(prototype, "value")?.set?.call(field, value);
|
|
48
|
+
field.dispatchEvent(new Event("input", { bubbles: true }));
|
|
49
|
+
field.dispatchEvent(new Event("change", { bubbles: true }));
|
|
50
|
+
},
|
|
51
|
+
form: (selector: string) => {
|
|
52
|
+
const found = element(selector);
|
|
53
|
+
if (!(found instanceof HTMLFormElement)) {
|
|
54
|
+
throw new Error(`The screen's ${selector} is not a form.\nhelp: Select a rendered form to submit.`);
|
|
55
|
+
}
|
|
56
|
+
enabled(found, selector);
|
|
57
|
+
return found;
|
|
58
|
+
},
|
|
59
|
+
input,
|
|
60
|
+
rendered,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Cause, Context, Effect, type Scope, Tracer } from "effect";
|
|
2
|
-
import type { AnyTestLayer } from "@shivaedev/effect-test/
|
|
2
|
+
import type { AnyTestLayer } from "@shivaedev/effect-test/anyTestLayer.ts";
|
|
3
3
|
import type { EffectClock } from "@shivaedev/effect-test/types.ts";
|
|
4
4
|
import { refusal } from "#internal/errors.ts";
|
|
5
5
|
import { type AnyEffect, type EffectHook, type Hook, hooked } from "#internal/hooked.ts";
|
|
@@ -26,6 +26,14 @@ export const start = Effect.fnUntraced(function* (
|
|
|
26
26
|
},
|
|
27
27
|
tell: narration.tell,
|
|
28
28
|
};
|
|
29
|
-
return yield* body(
|
|
29
|
+
return yield* body(
|
|
30
|
+
Object.defineProperties(
|
|
31
|
+
{},
|
|
32
|
+
{
|
|
33
|
+
...Object.getOwnPropertyDescriptors(definition.verbs(engine, story)),
|
|
34
|
+
story: { enumerable: true, value: story },
|
|
35
|
+
},
|
|
36
|
+
),
|
|
37
|
+
);
|
|
30
38
|
}).pipe(Effect.tapCause((cause) => Effect.sync(() => captureFailure(Cause.prettyErrors(cause)[0]?.stack))));
|
|
31
39
|
});
|
|
@@ -4,9 +4,9 @@ import type { EffectTest, EffectTestOptions } from "@shivaedev/effect-test/types
|
|
|
4
4
|
import { makeEffectIt } from "@shivaedev/effect-test/vitest.ts";
|
|
5
5
|
import { type Genre, genreOf } from "#internal/genre.ts";
|
|
6
6
|
import { type AnyEffect, hooked } from "#internal/hooked.ts";
|
|
7
|
-
import { routeOutbound } from "#internal/routeOutbound.ts";
|
|
8
7
|
import type { LooseDefinition, LooseTrait } from "#internal/seed.ts";
|
|
9
8
|
import { start } from "#internal/start.ts";
|
|
9
|
+
import { routeOutbound } from "#routeOutbound.ts";
|
|
10
10
|
|
|
11
11
|
type LooseBody = (told: object, context: TestContext) => Generator<AnyEffect, unknown, never> | undefined;
|
|
12
12
|
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { installOutbound } from "#internal/installOutbound.ts";
|
|
3
|
+
|
|
4
|
+
export function routeOutbound(routes: Readonly<Record<string, string>>) {
|
|
5
|
+
return Effect.acquireRelease(
|
|
6
|
+
Effect.promise(() => installOutbound(routes, Reflect.get(globalThis, Symbol.for("test-story.dom")) === true)),
|
|
7
|
+
(restore) => Effect.sync(restore),
|
|
8
|
+
).pipe(Effect.asVoid);
|
|
9
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { computeAccessibleName, getRole } from "dom-accessibility-api";
|
|
2
|
+
import { Effect } from "effect";
|
|
3
|
+
import { withLive } from "@shivaedev/effect-test/withLive.ts";
|
|
4
|
+
import { screenElements } from "#internal/screenElements.ts";
|
|
5
|
+
|
|
6
|
+
export interface ScreenDriverOptions {
|
|
7
|
+
readonly act?: (action: () => Promise<void>) => PromiseLike<void>;
|
|
8
|
+
readonly pending: () => boolean;
|
|
9
|
+
readonly root: HTMLElement | (() => HTMLElement);
|
|
10
|
+
readonly tell: (line: string) => void;
|
|
11
|
+
readonly timeoutMs?: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type ScreenDriver = ReturnType<typeof screenDriver>;
|
|
15
|
+
|
|
16
|
+
export function screenDriver({ root, pending, tell, act = (action) => action(), timeoutMs = 3000 }: ScreenDriverOptions) {
|
|
17
|
+
const currentRoot = typeof root === "function" ? root : () => root;
|
|
18
|
+
const elements = screenElements(currentRoot);
|
|
19
|
+
function perform(line: string, action: () => void) {
|
|
20
|
+
return Effect.promise(async () => {
|
|
21
|
+
tell(line);
|
|
22
|
+
await act(async () => action());
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
function settle(options: { readonly timeoutMs?: number } = {}) {
|
|
26
|
+
const limit = options.timeoutMs ?? timeoutMs;
|
|
27
|
+
const wait = Effect.gen(function* () {
|
|
28
|
+
tell("wait for the screen's pending work");
|
|
29
|
+
do {
|
|
30
|
+
yield* Effect.promise(() => act(async () => undefined));
|
|
31
|
+
if (!pending()) {
|
|
32
|
+
tell("the screen has no pending work");
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
yield* Effect.promise(() => act(() => new Promise<void>((resolve) => setTimeout(resolve, 10))));
|
|
36
|
+
} while (true);
|
|
37
|
+
});
|
|
38
|
+
function timedOut() {
|
|
39
|
+
return Effect.die(
|
|
40
|
+
new Error(
|
|
41
|
+
`The screen did not settle within ${limit}ms.\nhelp: Release held requests and check the app's pending signal and renderer act adapter.\nScreen: ${currentRoot().textContent}`,
|
|
42
|
+
),
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
return withLive(wait.pipe(Effect.timeoutOrElse({ duration: limit, orElse: timedOut })));
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
accessible: (selector: string) => {
|
|
49
|
+
const element = elements.element(selector);
|
|
50
|
+
const accessible = { name: computeAccessibleName(element), role: getRole(element) };
|
|
51
|
+
tell(`read the accessibility of ${selector}: ${JSON.stringify(accessible)}`);
|
|
52
|
+
return accessible;
|
|
53
|
+
},
|
|
54
|
+
click: (selector: string) =>
|
|
55
|
+
perform(`click ${selector}`, () => {
|
|
56
|
+
const element = elements.element(selector);
|
|
57
|
+
elements.enabled(element, selector);
|
|
58
|
+
element.focus();
|
|
59
|
+
element.click();
|
|
60
|
+
}),
|
|
61
|
+
enter: (selector: string, value: string) => perform(`enter a value in ${selector}`, () => elements.enter(selector, value)),
|
|
62
|
+
settle,
|
|
63
|
+
submit: (selector = "form") => perform(`submit ${selector}`, () => elements.form(selector).requestSubmit()),
|
|
64
|
+
text: (selector?: string) => {
|
|
65
|
+
const text = (selector === undefined ? elements.rendered(currentRoot(), "root") : elements.element(selector)).innerText;
|
|
66
|
+
tell(`read ${selector ?? "the screen"}: ${JSON.stringify(text)}`);
|
|
67
|
+
return text;
|
|
68
|
+
},
|
|
69
|
+
value: (selector: string) => {
|
|
70
|
+
const value = elements.input(selector).value;
|
|
71
|
+
tell(`read the value in ${selector}: ${JSON.stringify(value)}`);
|
|
72
|
+
return value;
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
package/src/server/storyKit.ts
CHANGED
|
@@ -55,7 +55,7 @@ export interface StoryKitDefinition<
|
|
|
55
55
|
readonly outbound?: Readonly<Record<string, string>>;
|
|
56
56
|
readonly run?: RunHooks<TEngine, TStep, TFailed, TDiagnose>;
|
|
57
57
|
readonly runIf?: boolean;
|
|
58
|
-
readonly stages: readonly
|
|
58
|
+
readonly stages: readonly TStage[];
|
|
59
59
|
readonly verbs: (engine: TEngine, story: Story<TEngine, TStep | TFailed | TDiagnose>) => TVerbs;
|
|
60
60
|
}
|
|
61
61
|
|
|
@@ -101,8 +101,8 @@ export interface StoryKit<TEngine, TStage extends string, TVerbs, TRun extends A
|
|
|
101
101
|
|
|
102
102
|
export function storyKit<
|
|
103
103
|
TCreate extends () => unknown,
|
|
104
|
-
const TStage extends string,
|
|
105
|
-
TVerbs extends object,
|
|
104
|
+
const TStage extends string = never,
|
|
105
|
+
TVerbs extends object = object,
|
|
106
106
|
TProvided = never,
|
|
107
107
|
TAfter extends AfterResults<TStage> = Record<never, never>,
|
|
108
108
|
TStep extends AnyEffect = never,
|
|
@@ -114,8 +114,8 @@ export function storyKit<
|
|
|
114
114
|
// Keep explicit engine and effect type arguments accepted by existing kits.
|
|
115
115
|
export function storyKit<
|
|
116
116
|
TEngine,
|
|
117
|
-
const TStage extends string,
|
|
118
|
-
TVerbs extends object,
|
|
117
|
+
const TStage extends string = never,
|
|
118
|
+
TVerbs extends object = object,
|
|
119
119
|
TProvided = never,
|
|
120
120
|
TCreate extends AnyEffect = never,
|
|
121
121
|
TAfter extends { readonly [TKey in TStage]?: AnyEffect } = Record<never, never>,
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"routeOutbound.d.ts","sourceRoot":"","sources":["../../../src/server/internal/routeOutbound.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAehC,wBAAsB,eAAe,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,EAAE,MAAM,UAAQ,uBAgD7F;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,4DAKrE"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"routeOutbound.js","sourceRoot":"","sources":["../../../src/server/internal/routeOutbound.ts"],"names":[],"mappings":"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"}
|
package/docs/north-star.md
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
# North star
|
|
2
|
-
|
|
3
|
-
## The problem
|
|
4
|
-
|
|
5
|
-
Agents now write most of the code, tests included, and a test file an agent writes tends to be hundreds of lines long. Each test rebuilds its setup by hand, a little differently from the last, and where rebuilding a complex setup is too much work, it mocks the part that was hard to build. Nobody reads these files. A human could ask an agent what a suite actually tests, but nobody asks consistently enough for that to be a safety net, so we lose sight of what we test exactly when more code is written than ever.
|
|
6
|
-
|
|
7
|
-
## The ideal
|
|
8
|
-
|
|
9
|
-
A test reads like a short English story: what exists, what someone does, and what is true afterwards. Its setup is a list of traits in the domain's words, such as `ovenIsLit()` and `hasDough(3)`, its actions go through the application's real verbs, and the real engine runs between them. Everything that is not specific to this one test lives elsewhere, written once:
|
|
10
|
-
|
|
11
|
-
- once per engine, a story kit that knows how to create the engine, seed it stage by stage, step it and show its state;
|
|
12
|
-
- once per feature, the traits and verbs that name that feature in domain words;
|
|
13
|
-
- in each test, only the story.
|
|
14
|
-
|
|
15
|
-
This changes the cost and benefit of a good test for both readers. For an agent, a story test is the cheaper one to write and to read on every later visit, because it is a handful of lines that reuse what already exists. Because every story runs the whole engine, each test also checks, almost by accident, the parts its author never thought about and would have mocked. For a human, a spec that reads as English leaves no excuse not to read it. With so little noise around the signal, an agent that writes a sloppy or pointless test produces something a reviewer sees at a glance.
|
|
16
|
-
|
|
17
|
-
## What good looks like
|
|
18
|
-
|
|
19
|
-
- A spec is a few lines of domain words. If a spec needs a helper, a loop or hand-built state, a trait or verb is missing from the kit.
|
|
20
|
-
- An impossible setup refuses loudly instead of seeding a state the application could never reach.
|
|
21
|
-
- A failure is a console version of stopping in a debugger: the whole story, where it stopped and the spec line there, and the engine's state at that moment, written so an agent with no knowledge of this package can fix the test, and complete enough that a CI log alone is enough to reproduce it.
|
|
22
|
-
- The kit author writes hooks and the package infers every type from them. There is one kit, built on Effect: a hook is a plain function, or a generator function when it needs a service or fails with a typed error, so no definition writes `Effect.gen` and a plain engine needs no Effect at all.
|
|
23
|
-
- A spec writes only its story. The kit hands it an `it` that already knows the engine's services, so a spec never builds a test harness, and names every test after its genre, so a report shows which engine it ran and a run can pick one engine's stories.
|
|
24
|
-
|
|
25
|
-
## Trade-offs
|
|
26
|
-
|
|
27
|
-
- **Real engine over speed.** A story is slower than a unit test with a mock. We accept that, and keep engines fast enough to step in a test instead of faking them.
|
|
28
|
-
- **Hooks over freedom.** The kit fixes the shape of an engine: create, stages, after-stage hooks, a step, a state to inspect, verbs. An engine that does not fit should change its kit's hooks, not bypass the kit.
|
|
29
|
-
- **Cheap locations over complete ones.** The failure shows one spec line, where the story stopped, found by matching the spec's path in the failure's stack or in the call site of the story's last line. A failure raised deep inside an Effect verb may have none; finding one would take machinery we do not want.
|
|
30
|
-
|
|
31
|
-
## What it will not do
|
|
32
|
-
|
|
33
|
-
- Run without Vitest, or print a failure anywhere but the failing test's own error.
|
|
34
|
-
- Own any application's traits, verbs or steps; they belong to the application's `test-support/`.
|
|
35
|
-
- Mock the application's own code, or help a test reach into an engine's internals. A fake belongs only at a true external boundary, and the kit's `create` is where it is wired in.
|
package/docs/roadmap.md
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
# Roadmap
|
|
2
|
-
|
|
3
|
-
This file owns the story kit's implementation, next work and open questions. The [framework roadmap](https://github.com/ShivaeDev/platform/blob/main/docs/framework/roadmap.md) owns composed application features and host-level validation. Those roadmaps link to each other rather than repeating a completion checklist.
|
|
4
|
-
|
|
5
|
-
## Built
|
|
6
|
-
|
|
7
|
-
- [x] `storyKit(definition)`: one Effect kit per engine, with every type and the services its layer provides inferred from its hooks. Plain engines plug in with plain functions.
|
|
8
|
-
- [x] Traits seeded stage by stage, with a hook after each stage, and refusal of an impossible setup.
|
|
9
|
-
- [x] The kit's own `it`, built on `@shivaedev/effect-test`, which hands the body the verbs and the story and names every test after the kit's genre. Vitest tags per genre are opt-in through `genreTags(...kitNames)` in the Vitest config.
|
|
10
|
-
- [x] `story.runUntil(condition)`: steps the engine until the condition holds, stops on what `run.failed` reports, and stops after a step budget with a diagnosis, keeping every hook's typed failure in its error channel.
|
|
11
|
-
- [x] A failed test prints a short guide, its story, where it stopped with the spec line there, and the engine's state, inline or in a file under `node_modules/.cache/test-story/`, broken at 120 columns.
|
|
12
|
-
- [x] `domEnvironment` keeps Node fetch classes for an in-process host and happy-dom fetch for the client; `domFetch(baseUrl)` connects the window to that host, with a starter story proving Better Auth sign-up, sign-in, session reads and sign-out on PostgreSQL through cookies.
|
|
13
|
-
- [x] `domNavigate` loads a routed document into happy-dom, updates its location and returns a response whose body remains readable. [Document transport stories](../src/server/documentTransport.dom.spec.ts) assert the rendered heading, location and response body.
|
|
14
|
-
- [x] `outbound` routes exact hostnames to fake HTTP servers while preserving request paths, queries, methods, headers and bodies, and restores Node routing after successful and failed stories. [Outbound stories](../src/server/outbound.spec.ts) assert routing and restoration; [document transport stories](../src/server/documentTransport.dom.spec.ts) cover redirects, cookies and XMLHttpRequest; [DOM refusal tests](../src/server/domOutbound.spec.ts) assert that unrouted external fetches, document requests and redirects fail the story even when application code catches the error.
|
|
15
|
-
- [x] In-process host creation separates request traces from the story span: requests without trace headers get distinct root traces, supplied trace headers continue their trace, request work stays under the request span, and traits and steps stay under the story span. [Host tracing stories](../src/server/hostTracing.spec.ts) assert span parents, trace IDs and completed request spans.
|
|
16
|
-
- [x] Creation, traits and stage `after` hooks can return Effects directly, with services from the kit's Layer and resources released in the story's Scope, covered by [Effect hook stories](../src/server/effectHooks.spec.ts).
|
|
17
|
-
- [x] Kit hooks and steps support a live clock and a per-story TestClock override. [Live-time stories](../src/server/liveTime.spec.ts) cover polling real workers with `withLive` without advancing virtual token expiry.
|
|
18
|
-
|
|
19
|
-
## Next
|
|
20
|
-
|
|
21
|
-
- [ ] Move the Work Fleet engine's hand-written story DSL onto the kit and let what it needs decide the next hook.
|
|
22
|
-
|
|
23
|
-
## Open questions
|
|
24
|
-
|
|
25
|
-
- A long story prints in full. Should a story of thousands of lines print its traits and its last lines, and write the rest to a file the way a large engine state is written?
|
|
26
|
-
- `inspect` runs synchronously when the test has failed. An engine whose state lives behind an Effect service cannot be read then. Is that worth an Effect inspect, captured when the failure happens?
|