@shivaedev/test-story 0.9.0 → 0.11.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 CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.11.0 - 2026-10-18
4
+
5
+ ### Added
6
+
7
+ - Pass a UTC `DateTime` to the existing `clock` option to pin a story or Effect fixture before its per-test setup runs. The exported `EffectClock` type now accepts `DateTime.Utc` alongside `"test"` and `"live"`. Hosts, workers and screens acquired with the story's Effect Context share native `Clock` / `TestClock`, including explicit `TestClock.adjust` advances; application code using `Clock` or `DateTime.now` needs no changes.
8
+
9
+ ## 0.10.0 - 2026-10-18
10
+
11
+ ### Added
12
+
13
+ - Platform exports `authTestUtils(options?)` and `signedInUser(auth, fields)` from `server/better-auth/testing.ts` for typed Better Auth test utilities and signed-in story preconditions with user fields and roles, without OAuth or password hashing.
14
+ - Test-story exports `seedCookies(cookies)` and `DomCookie` from `seedCookies.ts` to seed happy-dom's cookie container, including HttpOnly session cookies accepted by a real HTTP host.
15
+
3
16
  ## 0.9.0 - 2026-10-18
4
17
 
5
18
  ### Added
package/README.md CHANGED
@@ -131,7 +131,7 @@ This is the bakery kit this package tests itself with. `emptyBakery`, `stillWait
131
131
  | `name` | What the engine is called in failures, "the bakery", and the kit's genre, "Bakery Story". |
132
132
  | `outbound` | Optional. A map of exact hostnames to fake server origins for Node fetch and the DOM environment's document requests. |
133
133
  | `around(effect)` | Optional. Wraps creation, staged seeding and the body; worker Layer acquisition remains outside it. It may require the worker services and per-test Scope. |
134
- | `clock` | Optional. `"test"` by default, or `"live"` for HTTP and DOM stories. A story’s `options.clock` takes precedence. |
134
+ | `clock` | Optional. `"test"` by default, `"live"` for real time, or a UTC `DateTime` to start TestClock at that instant. A story’s `options.clock` takes precedence. |
135
135
  | `create()` | A fresh engine. Every story starts from a new one. |
136
136
  | `layer` | Optional. The worker Effect layer that provides the services hooks, traits and test bodies use. Per-test Scope is already available to creation, staged and run hooks, traits and bodies. A hook that needs a service the layer does not provide does not compile, and neither does a test whose traits or body need one. |
137
137
  | `stages` | The order traits are applied in. A trait names its stage, and one that names a stage the kit does not declare does not compile. |
@@ -379,6 +379,29 @@ bakery.it("tells each step into the story", [ovenIsLit()], function* ({ baker, o
379
379
 
380
380
  A spec starts a story, acts through verbs and checks what a user would see. When it needs a setup or an action the kit lacks, add a trait or verb to the kit instead of building it in the spec.
381
381
 
382
+ ### Pinning time
383
+
384
+ Pass a UTC `DateTime` as the story's `clock` option to pin time before its wrapper, creation, traits and hooks run. Code that uses `Clock` or `DateTime.now` reads that instant. Advance it explicitly with native `TestClock.adjust`:
385
+
386
+ ```ts
387
+ import { DateTime } from "effect";
388
+ import * as TestClock from "effect/testing/TestClock";
389
+
390
+ app.it("shows the worker's date", [], function* ({ click, settle, text }) {
391
+ yield* click("button");
392
+ yield* settle();
393
+ expect(text("#worker")).toBe("Worker: since Oct 17");
394
+ yield* TestClock.adjust("1 day");
395
+ yield* click("button");
396
+ yield* settle();
397
+ expect(text("#worker")).toBe("Worker: since Oct 18");
398
+ }, { clock: DateTime.makeUnsafe("2026-10-17T12:34:56.000Z") });
399
+ ```
400
+
401
+ Acquire hosts, workers and screens in per-story Scope so they inherit the story's Clock. An independent runtime or native HTTP handler must capture the Effect Context at acquisition, as with `HttpEffect.toWebHandlerWith(context)` or `Effect.runPromiseWith(context)`. Worker-scoped kit `layer` acquisition stays outside the per-story clock; shared services acquired there do not acquire a new clock for each story. Direct JavaScript wall-time reads are not replaced. Formatting stays with the application, including its locale and time zone.
402
+
403
+ The [clock application stories](src/server/fixedClock.dom.spec.ts) run loopback HTTP, a queued worker that writes a timestamp to a file, and a DOM screen with its own Effect runner. They check exact timestamps and rendered dates, advancing time, and a live story without the option. A live-clock kit still runs on real time when a story omits the override.
404
+
382
405
  ### Running the engine
383
406
 
384
407
  Hooks and steps that wait for real workers can use `withLive(Effect.sleep(...))` or `withLive(eventually(...))` from `@shivaedev/effect-test/withLive.ts` and `@shivaedev/effect-test/eventually.ts`. Both work under the kit's live clock and a story's TestClock override. The wrapped work uses live time without advancing the story's virtual time; outside it, the selected clock still applies. Unwrapped `eventually` uses the active clock and advances TestClock between attempts.
@@ -477,6 +500,23 @@ const cookieApp = storyKit({
477
500
 
478
501
  `startHost`, `postgres` and `authVerbs` are application plumbing. Actions in `authVerbs` perform requests, and readouts return response data for assertions in the spec. A sign-in story drives its real sign-in flow; feature stories start from a faked signed-in precondition as described in the shared skill.
479
502
 
503
+ #### Seeding a signed-in precondition
504
+
505
+ `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.
506
+
507
+ ```ts
508
+ import { signedInUser } from "@shivaedev/platform/server/better-auth/testing.ts";
509
+ import { seedCookies } from "@shivaedev/test-story/seedCookies.ts";
510
+
511
+ const { cookies } = yield* Effect.promise(() => signedInUser(testAuth, {
512
+ email: "alice@example.test",
513
+ name: "Alice",
514
+ }));
515
+ yield* seedCookies(cookies);
516
+ ```
517
+
518
+ `testAuth` is a test-only auth instance configured with Platform's `authTestUtils()` and the real host's database, secret and cookie policy. The seeded session travels on the document's fetch and stays hidden from `document.cookie` when HttpOnly. Use `http://localhost:<port>` for local DOM hosts with secure cookies: happy-dom matches secure cookies on localhost, but not on `http://127.0.0.1`. The listener can still bind to `127.0.0.1`.
519
+
480
520
  #### Social sign-in through an outbound provider
481
521
 
482
522
  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.
@@ -531,6 +571,7 @@ Import each module with the prefix `@shivaedev/test-story/`; there is no root en
531
571
  | --- | --- |
532
572
  | `storyKit.ts` | `storyKit`, `Trait`, `RunUntilOptions`, `Story`, `RunHooks`, `StoryKitDefinition`, `Told`, `StoryBody`, `StoryTest`, `StoryIt`, `StoryKit` |
533
573
  | `domFetch.ts` | `domFetch(baseUrl)` |
574
+ | `seedCookies.ts` | `seedCookies(cookies)`, `DomCookie` |
534
575
  | `routeOutbound.ts` | `routeOutbound(routes)` |
535
576
  | `screenDriver.ts` | `screenDriver(options)`, `ScreenDriverOptions`, `ScreenDriver` |
536
577
  | `domNavigate.ts` | `domNavigate(url?)` |
@@ -0,0 +1,13 @@
1
+ import { Effect } from "effect";
2
+ export interface DomCookie {
3
+ readonly domain?: string;
4
+ readonly expires?: number;
5
+ readonly httpOnly?: boolean;
6
+ readonly name: string;
7
+ readonly path?: string;
8
+ readonly sameSite?: "Lax" | "Strict" | "None";
9
+ readonly secure?: boolean;
10
+ readonly value: string;
11
+ }
12
+ export declare const seedCookies: (cookies: readonly DomCookie[]) => Effect.Effect<undefined, never, never>;
13
+ //# sourceMappingURL=seedCookies.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"seedCookies.d.ts","sourceRoot":"","sources":["../../src/server/seedCookies.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAIhC,MAAM,WAAW,SAAS;IACzB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,QAAQ,CAAC,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;IAC9C,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACvB;AAID,eAAO,MAAM,WAAW,2EAqBtB,CAAC"}
@@ -0,0 +1,25 @@
1
+ import { Effect } from "effect";
2
+ import { BrowserWindow, CookieSameSiteEnum } from "happy-dom";
3
+ import WindowBrowserContext from "happy-dom/lib/window/WindowBrowserContext.js";
4
+ const sameSites = { Lax: CookieSameSiteEnum.lax, None: CookieSameSiteEnum.none, Strict: CookieSameSiteEnum.strict };
5
+ export const seedCookies = Effect.fnUntraced(function* (cookies) {
6
+ if (!(window instanceof BrowserWindow)) {
7
+ return yield* Effect.die("Cookie preconditions need a happy-dom window.\nhelp: Use test-story's DOM environment before seeding cookies.");
8
+ }
9
+ const context = new WindowBrowserContext(window).getBrowserContext();
10
+ if (context === null) {
11
+ return yield* Effect.die("Cannot seed cookies after the document has closed.");
12
+ }
13
+ context.cookieContainer.addCookies(cookies.map((cookie) => ({
14
+ key: cookie.name,
15
+ "originURL": new URL(window.location.href),
16
+ value: cookie.value,
17
+ ...(cookie.domain === undefined ? {} : { domain: cookie.domain }),
18
+ ...(cookie.path === undefined ? {} : { path: cookie.path }),
19
+ ...(cookie.httpOnly === undefined ? {} : { httpOnly: cookie.httpOnly }),
20
+ ...(cookie.secure === undefined ? {} : { secure: cookie.secure }),
21
+ ...(cookie.sameSite === undefined ? {} : { sameSite: sameSites[cookie.sameSite] }),
22
+ ...(cookie.expires === undefined ? {} : { expires: new Date(cookie.expires * 1000) }),
23
+ })));
24
+ });
25
+ //# sourceMappingURL=seedCookies.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"seedCookies.js","sourceRoot":"","sources":["../../src/server/seedCookies.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,OAAO,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAC9D,OAAO,oBAAoB,MAAM,8CAA8C,CAAC;AAahF,MAAM,SAAS,GAAG,EAAE,GAAG,EAAE,kBAAkB,CAAC,GAAG,EAAE,IAAI,EAAE,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,kBAAkB,CAAC,MAAM,EAAE,CAAC;AAEpH,MAAM,CAAC,MAAM,WAAW,GAAG,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,OAA6B;IACpF,IAAI,CAAC,CAAC,MAAM,YAAY,aAAa,CAAC,EAAE,CAAC;QACxC,OAAO,KAAK,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,+GAA+G,CAAC,CAAC;IAC3I,CAAC;IACD,MAAM,OAAO,GAAG,IAAI,oBAAoB,CAAC,MAAM,CAAC,CAAC,iBAAiB,EAAE,CAAC;IACrE,IAAI,OAAO,KAAK,IAAI,EAAE,CAAC;QACtB,OAAO,KAAK,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,oDAAoD,CAAC,CAAC;IAChF,CAAC;IACD,OAAO,CAAC,eAAe,CAAC,UAAU,CACjC,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;QACxB,GAAG,EAAE,MAAM,CAAC,IAAI;QAChB,WAAW,EAAE,IAAI,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC;QAC1C,KAAK,EAAE,MAAM,CAAC,KAAK;QACnB,GAAG,CAAC,MAAM,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC;QACjE,GAAG,CAAC,MAAM,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,CAAC;QAC3D,GAAG,CAAC,MAAM,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,CAAC;QACvE,GAAG,CAAC,MAAM,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC;QACjE,GAAG,CAAC,MAAM,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC;QAClF,GAAG,CAAC,MAAM,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,IAAI,CAAC,MAAM,CAAC,OAAO,GAAG,IAAI,CAAC,EAAE,CAAC;KACrF,CAAC,CAAC,CACH,CAAC;AACH,CAAC,CAAC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shivaedev/test-story",
3
- "version": "0.9.0",
3
+ "version": "0.11.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": {
@@ -41,12 +41,12 @@
41
41
  "README.md"
42
42
  ],
43
43
  "dependencies": {
44
- "@shivaedev/effect-test": "0.6.0",
44
+ "@shivaedev/effect-test": "0.7.0",
45
45
  "dom-accessibility-api": "0.7.1"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@effect/vitest": "4.0.4",
49
- "@shivaedev/quality": "0.27.0",
49
+ "@shivaedev/quality": "0.27.1",
50
50
  "@types/node": "24.19.2",
51
51
  "effect": "4.0.4",
52
52
  "happy-dom": "20.14.6",
@@ -0,0 +1,39 @@
1
+ import { Effect } from "effect";
2
+ import { BrowserWindow, CookieSameSiteEnum } from "happy-dom";
3
+ import WindowBrowserContext from "happy-dom/lib/window/WindowBrowserContext.js";
4
+
5
+ export interface DomCookie {
6
+ readonly domain?: string;
7
+ readonly expires?: number;
8
+ readonly httpOnly?: boolean;
9
+ readonly name: string;
10
+ readonly path?: string;
11
+ readonly sameSite?: "Lax" | "Strict" | "None";
12
+ readonly secure?: boolean;
13
+ readonly value: string;
14
+ }
15
+
16
+ const sameSites = { Lax: CookieSameSiteEnum.lax, None: CookieSameSiteEnum.none, Strict: CookieSameSiteEnum.strict };
17
+
18
+ export const seedCookies = Effect.fnUntraced(function* (cookies: readonly DomCookie[]) {
19
+ if (!(window instanceof BrowserWindow)) {
20
+ return yield* Effect.die("Cookie preconditions need a happy-dom window.\nhelp: Use test-story's DOM environment before seeding cookies.");
21
+ }
22
+ const context = new WindowBrowserContext(window).getBrowserContext();
23
+ if (context === null) {
24
+ return yield* Effect.die("Cannot seed cookies after the document has closed.");
25
+ }
26
+ context.cookieContainer.addCookies(
27
+ cookies.map((cookie) => ({
28
+ key: cookie.name,
29
+ "originURL": new URL(window.location.href),
30
+ value: cookie.value,
31
+ ...(cookie.domain === undefined ? {} : { domain: cookie.domain }),
32
+ ...(cookie.path === undefined ? {} : { path: cookie.path }),
33
+ ...(cookie.httpOnly === undefined ? {} : { httpOnly: cookie.httpOnly }),
34
+ ...(cookie.secure === undefined ? {} : { secure: cookie.secure }),
35
+ ...(cookie.sameSite === undefined ? {} : { sameSite: sameSites[cookie.sameSite] }),
36
+ ...(cookie.expires === undefined ? {} : { expires: new Date(cookie.expires * 1000) }),
37
+ })),
38
+ );
39
+ });