@shivaedev/test-story 0.9.0 → 0.10.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,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.10.0 - 2026-10-18
4
+
5
+ ### Added
6
+
7
+ - 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.
8
+ - 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.
9
+
3
10
  ## 0.9.0 - 2026-10-18
4
11
 
5
12
  ### Added
package/README.md CHANGED
@@ -477,6 +477,23 @@ const cookieApp = storyKit({
477
477
 
478
478
  `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
479
 
480
+ #### Seeding a signed-in precondition
481
+
482
+ `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.
483
+
484
+ ```ts
485
+ import { signedInUser } from "@shivaedev/platform/server/better-auth/testing.ts";
486
+ import { seedCookies } from "@shivaedev/test-story/seedCookies.ts";
487
+
488
+ const { cookies } = yield* Effect.promise(() => signedInUser(testAuth, {
489
+ email: "alice@example.test",
490
+ name: "Alice",
491
+ }));
492
+ yield* seedCookies(cookies);
493
+ ```
494
+
495
+ `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`.
496
+
480
497
  #### Social sign-in through an outbound provider
481
498
 
482
499
  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 +548,7 @@ Import each module with the prefix `@shivaedev/test-story/`; there is no root en
531
548
  | --- | --- |
532
549
  | `storyKit.ts` | `storyKit`, `Trait`, `RunUntilOptions`, `Story`, `RunHooks`, `StoryKitDefinition`, `Told`, `StoryBody`, `StoryTest`, `StoryIt`, `StoryKit` |
533
550
  | `domFetch.ts` | `domFetch(baseUrl)` |
551
+ | `seedCookies.ts` | `seedCookies(cookies)`, `DomCookie` |
534
552
  | `routeOutbound.ts` | `routeOutbound(routes)` |
535
553
  | `screenDriver.ts` | `screenDriver(options)`, `ScreenDriverOptions`, `ScreenDriver` |
536
554
  | `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.10.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": {
@@ -46,7 +46,7 @@
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
+ });