@brendanbank/atrium-test-utils 0.15.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/LICENCE.md ADDED
@@ -0,0 +1,25 @@
1
+ # BSD 2-Clause License
2
+
3
+ Copyright (c) 2026, Brendan Bank
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice,
9
+ this list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
16
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
18
+ ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
19
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
20
+ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
21
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
22
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
23
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
24
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
25
+ POSSIBILITY OF SUCH DAMAGE.
package/README.md ADDED
@@ -0,0 +1,153 @@
1
+ # `@brendanbank/atrium-test-utils`
2
+
3
+ Vitest helpers for unit-testing [atrium](https://github.com/brendanbank/atrium)
4
+ host bundles. Replaces hand-rolled mocks of `window.__ATRIUM_REGISTRY__`
5
+ + `window.React` + `useMe()` so a host can render permission-gated
6
+ components and fire synthetic SSE events without spinning up the SPA.
7
+
8
+ ## Why
9
+
10
+ Before this package, casa-style host tests had to wire mocks like:
11
+
12
+ ```ts
13
+ beforeEach(() => {
14
+ (window as any).__ATRIUM_REGISTRY__ = {
15
+ registerHomeWidget: vi.fn(),
16
+ /* …seven more vi.fn()s… */
17
+ subscribeEvent: vi.fn(),
18
+ };
19
+ (window as any).React = require('react');
20
+ });
21
+ ```
22
+
23
+ …and stub `useMe`'s underlying fetch separately. The seam is the same
24
+ in every host, so most teams skipped the unit tests and relied on
25
+ Playwright. This package gives them back.
26
+
27
+ ## Installation
28
+
29
+ ```
30
+ pnpm add -D @brendanbank/atrium-test-utils
31
+ ```
32
+
33
+ The package is published on **GitHub Packages**, same scope as the
34
+ other atrium SDK packages — see the host-bundle-utils
35
+ [`README`](../host-bundle-utils/README.md) for the `.npmrc` setup.
36
+
37
+ `vitest`, `@testing-library/react`, `react`, `react-dom`, and
38
+ `@tanstack/react-query` are peer deps — install them once in your
39
+ host's `frontend/package.json`; this package will not bundle a
40
+ second copy.
41
+
42
+ ## Usage
43
+
44
+ ```tsx
45
+ import {
46
+ fireAtriumEvent,
47
+ mockAtriumRegistry,
48
+ renderWithAtrium,
49
+ type MockAtriumHandles,
50
+ type UserContext,
51
+ } from '@brendanbank/atrium-test-utils';
52
+
53
+ const ALICE: UserContext = {
54
+ id: 1, email: 'alice@example.com', full_name: 'Alice',
55
+ is_active: true,
56
+ roles: ['owner'],
57
+ permissions: ['commission.view.all'],
58
+ impersonating_from: null,
59
+ };
60
+
61
+ let handles: MockAtriumHandles;
62
+
63
+ beforeEach(() => {
64
+ handles = mockAtriumRegistry({ me: ALICE });
65
+ });
66
+
67
+ afterEach(() => {
68
+ handles.cleanup();
69
+ });
70
+
71
+ test('owners see the per-agent picker, not the self view', () => {
72
+ const { getByLabelText } = renderWithAtrium(<CommissionsPage />);
73
+ expect(getByLabelText('Agent')).toBeInTheDocument();
74
+ });
75
+
76
+ test('the host bundle registers a home widget on import', async () => {
77
+ await import('../src/main');
78
+ expect(handles.homeWidgets).toHaveLength(1);
79
+ expect(handles.homeWidgets[0].key).toBe('commission-card');
80
+ });
81
+
82
+ test('refetches bookings on booking.created', async () => {
83
+ renderWithAtrium(<BookingsPage />);
84
+ fireAtriumEvent('booking.created', { booking_id: 42 });
85
+ // …assert refetch fired
86
+ });
87
+ ```
88
+
89
+ ## API
90
+
91
+ ### `mockAtriumRegistry(options?)`
92
+
93
+ Installs a recording fake on `window.__ATRIUM_REGISTRY__`, exposes
94
+ React via `window.React`, and stubs `window.__atrium_i18n__` with
95
+ the bundled `common.*` keys. Returns handles:
96
+
97
+ | Field | Type | Description |
98
+ | ---------------------- | ----------------------------- | ----------------------------------------------------- |
99
+ | `registry` | `AtriumRegistry` | The fake (also at `window.__ATRIUM_REGISTRY__`). |
100
+ | `homeWidgets` | `HomeWidget[]` | Recorded `registerHomeWidget` calls in order. |
101
+ | `routes` | `RouteEntry[]` | Recorded `registerRoute` calls. |
102
+ | `navItems` | `NavItem[]` | Recorded `registerNavItem` calls. |
103
+ | `adminTabs` | `AdminTab[]` | Recorded `registerAdminTab` calls. |
104
+ | `profileItems` | `ProfileItem[]` | Recorded `registerProfileItem` calls. |
105
+ | `notificationKinds` | `NotificationKindRenderer[]` | Recorded `registerNotificationKind` calls. |
106
+ | `localeOverlays` | `LocaleOverlay[]` | Recorded `registerLocale` calls. |
107
+ | `reset()` | `() => void` | Drop registrations + subscribers, keep window mocks. |
108
+ | `cleanup()` | `() => void` | Restore pre-mock window state. Call in `afterEach`. |
109
+
110
+ Options:
111
+
112
+ | Option | Description |
113
+ | ------------------- | ---------------------------------------------------------------------------- |
114
+ | `me` | What `useMe()` resolves to. Pass `null` for signed-out. Default `null`. |
115
+ | `i18n.resources` | Locale → flat dot-path → string. Default: bundled English `common.*`. |
116
+ | `i18n.language` | Active locale. Default `'en'`. |
117
+
118
+ ### `renderWithAtrium(ui, options?)`
119
+
120
+ `@testing-library/react`'s `render` pre-wired with a fresh
121
+ `QueryClient` and `<AtriumProvider>` pointed at the configured `me`.
122
+ Returns the standard `RenderResult` so existing assertions
123
+ (`getByRole`, `rerender`, `unmount`) work unchanged.
124
+
125
+ Options extend the standard `RenderOptions` with:
126
+
127
+ | Option | Description |
128
+ | ------------- | ------------------------------------------------------------------------ |
129
+ | `me` | Override `me` for this render. Falls back to the registry default. |
130
+ | `queryClient` | Share a client across renders so cache assertions survive remounts. |
131
+ | `wrap` | Additional wrapper component (e.g. `MantineProvider`, `RouterProvider`). |
132
+
133
+ ### `fireAtriumEvent(kind, payload)`
134
+
135
+ Dispatches a synthetic SSE-style event to every handler the host
136
+ registered via the fake registry's `subscribeEvent`. No-op when
137
+ called outside a `mockAtriumRegistry` scope, so a forgotten setup
138
+ shows as an empty assertion rather than a crash.
139
+
140
+ ## Versioning
141
+
142
+ Tracks `@brendanbank/atrium-host-bundle-utils`'s public API. When a
143
+ new registry slot lands in host-bundle-utils, the matching mock
144
+ shows up here in the next minor.
145
+
146
+ ## See also
147
+
148
+ - [`@brendanbank/atrium-host-bundle-utils`](../host-bundle-utils/) — the
149
+ runtime helpers + hooks this package mocks.
150
+ - [`@brendanbank/atrium-host-types`](../host-types/) — typed registry
151
+ shapes.
152
+ - [`docs/published-images.md`](../../docs/published-images.md) — the
153
+ full host-extension contract.
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Vitest helpers for unit-testing atrium host bundles.
3
+ *
4
+ * Atrium loads a host bundle by setting ``window.__ATRIUM_REGISTRY__``
5
+ * and ``window.React`` and then ``await import()``-ing the bundle.
6
+ * Hosts that test components touching that surface have to fake the
7
+ * registry by hand — most casa tests today reach for Playwright
8
+ * instead, which is much slower and more brittle.
9
+ *
10
+ * This package installs the fakes once per test:
11
+ *
12
+ * - ``mockAtriumRegistry({ me })`` — installs a recording fake on
13
+ * ``window.__ATRIUM_REGISTRY__``, mirrors atrium's
14
+ * ``subscribeEvent`` semantics with an in-memory bus, exposes
15
+ * React via ``window.React`` (re-using the host's copy — atrium
16
+ * uses a single shared React in production), and stubs
17
+ * ``window.__atrium_i18n__`` so ``__atrium_t__`` calls resolve
18
+ * against an English fallback bundle. Returns handles so the test
19
+ * can assert against what the import-time side-effects registered.
20
+ * - ``renderWithAtrium(ui)`` — wraps ``@testing-library/react``'s
21
+ * ``render`` with a fresh ``QueryClient`` + ``<AtriumProvider>``
22
+ * pre-wired to return the configured ``me``. Returns the standard
23
+ * ``RenderResult`` so existing assertions work unchanged.
24
+ * - ``fireAtriumEvent(kind, payload)`` — dispatches a synthetic
25
+ * event the same way atrium's SSE stream would. Subscribers
26
+ * registered via the fake registry's ``subscribeEvent`` see it.
27
+ *
28
+ * Peer deps only — vitest, @testing-library/react, react,
29
+ * @tanstack/react-query are the host's, not bundled. The package
30
+ * provides the seam, not the framework.
31
+ */
32
+ import { type ComponentType, type ReactElement, type ReactNode } from 'react';
33
+ import { QueryClient } from '@tanstack/react-query';
34
+ import { type RenderOptions, type RenderResult } from '@testing-library/react';
35
+ import { type UserContext } from '@brendanbank/atrium-host-bundle-utils/react';
36
+ import type { AdminTab, AtriumEvent, AtriumEventHandler, AtriumRegistry, HomeWidget, LocaleOverlay, NavItem, NotificationKindRenderer, ProfileItem, RouteEntry } from '@brendanbank/atrium-host-types';
37
+ export type { AdminTab, AtriumEvent, AtriumEventHandler, AtriumRegistry, HomeWidget, LocaleOverlay, NavItem, NotificationKindRenderer, ProfileItem, RouteEntry, UserContext, };
38
+ /** Dispatch an atrium SSE-style event to every handler the host
39
+ * subscribed via the fake registry. No-op when called outside a
40
+ * ``mockAtriumRegistry`` scope so a test that forgets to call
41
+ * ``mockAtriumRegistry`` first surfaces as an empty assertion rather
42
+ * than a TypeError. */
43
+ export declare function fireAtriumEvent(kind: string, payload: Record<string, unknown>): void;
44
+ /** Recorded registrations + reset/cleanup hooks returned by
45
+ * ``mockAtriumRegistry``. Tests assert against the array fields and
46
+ * call ``cleanup()`` from ``afterEach`` to drop the
47
+ * window globals back to their pre-mock state. */
48
+ export interface MockAtriumHandles {
49
+ /** The fake registry installed on ``window.__ATRIUM_REGISTRY__``.
50
+ * Test bodies that want to call register methods directly (instead
51
+ * of through a host bundle's import-time side-effects) reach for
52
+ * this. */
53
+ registry: AtriumRegistry;
54
+ homeWidgets: HomeWidget[];
55
+ routes: RouteEntry[];
56
+ navItems: NavItem[];
57
+ adminTabs: AdminTab[];
58
+ profileItems: ProfileItem[];
59
+ notificationKinds: NotificationKindRenderer[];
60
+ localeOverlays: LocaleOverlay[];
61
+ /** Drop every recorded registration + every event subscriber.
62
+ * Useful between tests inside the same ``describe`` that want to
63
+ * share the rest of the mock context. */
64
+ reset(): void;
65
+ /** Restore the pre-mock window state. Call from ``afterEach`` so
66
+ * the next test's ``mockAtriumRegistry`` starts from a clean
67
+ * baseline. */
68
+ cleanup(): void;
69
+ }
70
+ export interface MockAtriumOptions {
71
+ /** What ``useMe()`` resolves to. Pass ``null`` to simulate a
72
+ * signed-out user (atrium responded 401/403). Defaults to
73
+ * ``null``. */
74
+ me?: UserContext | null;
75
+ /** Locale resources the fake ``__atrium_t__`` resolves against.
76
+ * Keys are flat dot-paths (``'common.save'``); the outer key is
77
+ * the locale code. Defaults to a minimal English bundle covering
78
+ * the keys atrium ships in ``common.*``. Pass an empty object to
79
+ * exercise the missing-key fallback. */
80
+ i18n?: {
81
+ resources?: Record<string, Record<string, string>>;
82
+ language?: string;
83
+ };
84
+ }
85
+ /** Install a recording fake registry on ``window`` for the duration of
86
+ * a test. The handles returned record every register* call so tests
87
+ * can assert that a host bundle wired up the slots it claims to.
88
+ *
89
+ * ```ts
90
+ * let handles: MockAtriumHandles;
91
+ * beforeEach(() => {
92
+ * handles = mockAtriumRegistry({
93
+ * me: {
94
+ * id: 1, email: 'alice@example.com', full_name: 'Alice',
95
+ * is_active: true, roles: ['admin'],
96
+ * permissions: ['hello.toggle'], impersonating_from: null,
97
+ * },
98
+ * });
99
+ * });
100
+ * afterEach(() => handles.cleanup());
101
+ *
102
+ * test('host bundle registers a home widget', async () => {
103
+ * await import('../src/main');
104
+ * expect(handles.homeWidgets).toHaveLength(1);
105
+ * expect(handles.homeWidgets[0].key).toBe('hello-world');
106
+ * });
107
+ * ```
108
+ *
109
+ * Side effects:
110
+ * - ``window.__ATRIUM_REGISTRY__`` is replaced with the fake.
111
+ * - ``window.React`` is set to the package's React copy so
112
+ * ``makeWrapperElement(...)`` calls resolve. Tests that need a
113
+ * specific React identity can override after the call.
114
+ * - ``window.__atrium_i18n__`` is set to a fake i18n with the
115
+ * bundled English ``common.*`` strings (override via the
116
+ * ``i18n`` option). */
117
+ export declare function mockAtriumRegistry(options?: MockAtriumOptions): MockAtriumHandles;
118
+ export interface RenderWithAtriumOptions extends RenderOptions {
119
+ /** Override ``me`` for this single render. Useful for
120
+ * parameterised tests that vary the signed-in user without
121
+ * re-installing the registry. Falls back to the value from
122
+ * ``mockAtriumRegistry({ me })``. */
123
+ me?: UserContext | null;
124
+ /** Provide your own QueryClient — useful if the test asserts
125
+ * against cached data or invalidations. Defaults to a fresh client
126
+ * with retries disabled so a 401 surfaces immediately. */
127
+ queryClient?: QueryClient;
128
+ /** Wrap children in additional providers (MantineProvider,
129
+ * RouterProvider, etc.). Default: identity. */
130
+ wrap?: ComponentType<{
131
+ children: ReactNode;
132
+ }>;
133
+ }
134
+ /** ``@testing-library/react``'s ``render`` pre-wired with a fresh
135
+ * QueryClient and ``<AtriumProvider>`` pointed at the configured
136
+ * ``me``. Returns the standard ``RenderResult`` so existing
137
+ * assertions (``getByRole``, ``rerender``, ``unmount``) work
138
+ * unchanged.
139
+ *
140
+ * ```tsx
141
+ * test('owners see the per-agent picker', () => {
142
+ * const { getByLabelText } = renderWithAtrium(<CommissionsPage />);
143
+ * expect(getByLabelText('Agent')).toBeInTheDocument();
144
+ * });
145
+ * ```
146
+ *
147
+ * Pass ``me`` to override the value from ``mockAtriumRegistry`` for
148
+ * a single render — handy in parameterised tests. Pass
149
+ * ``queryClient`` to share a client across renders so cache
150
+ * assertions survive the second mount. */
151
+ export declare function renderWithAtrium(ui: ReactElement, options?: RenderWithAtriumOptions): RenderResult;
152
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,OAAO,EAEL,KAAK,aAAa,EAClB,KAAK,YAAY,EACjB,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AAEf,OAAO,EACL,WAAW,EAEZ,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAU,KAAK,aAAa,EAAE,KAAK,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAEvF,OAAO,EAEL,KAAK,WAAW,EACjB,MAAM,6CAA6C,CAAC;AACrD,OAAO,KAAK,EACV,QAAQ,EACR,WAAW,EACX,kBAAkB,EAClB,cAAc,EACd,UAAU,EACV,aAAa,EACb,OAAO,EACP,wBAAwB,EACxB,WAAW,EACX,UAAU,EACX,MAAM,gCAAgC,CAAC;AAExC,YAAY,EACV,QAAQ,EACR,WAAW,EACX,kBAAkB,EAClB,cAAc,EACd,UAAU,EACV,aAAa,EACb,OAAO,EACP,wBAAwB,EACxB,WAAW,EACX,UAAU,EACV,WAAW,GACZ,CAAC;AAsDF;;;;wBAIwB;AACxB,wBAAgB,eAAe,CAC7B,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC/B,IAAI,CAWN;AAMD;;;mDAGmD;AACnD,MAAM,WAAW,iBAAiB;IAChC;;;gBAGY;IACZ,QAAQ,EAAE,cAAc,CAAC;IACzB,WAAW,EAAE,UAAU,EAAE,CAAC;IAC1B,MAAM,EAAE,UAAU,EAAE,CAAC;IACrB,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,SAAS,EAAE,QAAQ,EAAE,CAAC;IACtB,YAAY,EAAE,WAAW,EAAE,CAAC;IAC5B,iBAAiB,EAAE,wBAAwB,EAAE,CAAC;IAC9C,cAAc,EAAE,aAAa,EAAE,CAAC;IAChC;;8CAE0C;IAC1C,KAAK,IAAI,IAAI,CAAC;IACd;;oBAEgB;IAChB,OAAO,IAAI,IAAI,CAAC;CACjB;AAED,MAAM,WAAW,iBAAiB;IAChC;;oBAEgB;IAChB,EAAE,CAAC,EAAE,WAAW,GAAG,IAAI,CAAC;IACxB;;;;6CAIyC;IACzC,IAAI,CAAC,EAAE;QACL,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;QACnD,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,CAAC;CACH;AA8DD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;2BA+B2B;AAC3B,wBAAgB,kBAAkB,CAChC,OAAO,GAAE,iBAAsB,GAC9B,iBAAiB,CAqEnB;AAQD,MAAM,WAAW,uBAAwB,SAAQ,aAAa;IAC5D;;;0CAGsC;IACtC,EAAE,CAAC,EAAE,WAAW,GAAG,IAAI,CAAC;IACxB;;+DAE2D;IAC3D,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B;oDACgD;IAChD,IAAI,CAAC,EAAE,aAAa,CAAC;QAAE,QAAQ,EAAE,SAAS,CAAA;KAAE,CAAC,CAAC;CAC/C;AAQD;;;;;;;;;;;;;;;;2CAgB2C;AAC3C,wBAAgB,gBAAgB,CAC9B,EAAE,EAAE,YAAY,EAChB,OAAO,GAAE,uBAA4B,GACpC,YAAY,CAgBd"}
package/dist/index.js ADDED
@@ -0,0 +1,265 @@
1
+ // Copyright (c) 2026 Brendan Bank
2
+ // SPDX-License-Identifier: BSD-2-Clause
3
+ /**
4
+ * Vitest helpers for unit-testing atrium host bundles.
5
+ *
6
+ * Atrium loads a host bundle by setting ``window.__ATRIUM_REGISTRY__``
7
+ * and ``window.React`` and then ``await import()``-ing the bundle.
8
+ * Hosts that test components touching that surface have to fake the
9
+ * registry by hand — most casa tests today reach for Playwright
10
+ * instead, which is much slower and more brittle.
11
+ *
12
+ * This package installs the fakes once per test:
13
+ *
14
+ * - ``mockAtriumRegistry({ me })`` — installs a recording fake on
15
+ * ``window.__ATRIUM_REGISTRY__``, mirrors atrium's
16
+ * ``subscribeEvent`` semantics with an in-memory bus, exposes
17
+ * React via ``window.React`` (re-using the host's copy — atrium
18
+ * uses a single shared React in production), and stubs
19
+ * ``window.__atrium_i18n__`` so ``__atrium_t__`` calls resolve
20
+ * against an English fallback bundle. Returns handles so the test
21
+ * can assert against what the import-time side-effects registered.
22
+ * - ``renderWithAtrium(ui)`` — wraps ``@testing-library/react``'s
23
+ * ``render`` with a fresh ``QueryClient`` + ``<AtriumProvider>``
24
+ * pre-wired to return the configured ``me``. Returns the standard
25
+ * ``RenderResult`` so existing assertions work unchanged.
26
+ * - ``fireAtriumEvent(kind, payload)`` — dispatches a synthetic
27
+ * event the same way atrium's SSE stream would. Subscribers
28
+ * registered via the fake registry's ``subscribeEvent`` see it.
29
+ *
30
+ * Peer deps only — vitest, @testing-library/react, react,
31
+ * @tanstack/react-query are the host's, not bundled. The package
32
+ * provides the seam, not the framework.
33
+ */
34
+ import { createElement, } from 'react';
35
+ import * as ReactNS from 'react';
36
+ import { QueryClient, QueryClientProvider, } from '@tanstack/react-query';
37
+ import { render } from '@testing-library/react';
38
+ import { AtriumProvider, } from '@brendanbank/atrium-host-bundle-utils/react';
39
+ function makeFakeI18n(resources, language) {
40
+ return {
41
+ language,
42
+ t(key, options) {
43
+ const langs = [language, 'en'];
44
+ for (const l of langs) {
45
+ const hit = resources[l]?.[key];
46
+ if (typeof hit === 'string') {
47
+ return interpolate(hit, options);
48
+ }
49
+ }
50
+ const def = options?.defaultValue;
51
+ return typeof def === 'string' ? def : key;
52
+ },
53
+ };
54
+ }
55
+ function interpolate(s, vars) {
56
+ if (!vars)
57
+ return s;
58
+ return s.replace(/\{\{(\w+)\}\}/g, (match, key) => {
59
+ if (key === 'defaultValue')
60
+ return match;
61
+ const v = vars[key];
62
+ return v == null ? match : String(v);
63
+ });
64
+ }
65
+ let activeSubscribers = null;
66
+ /** Dispatch an atrium SSE-style event to every handler the host
67
+ * subscribed via the fake registry. No-op when called outside a
68
+ * ``mockAtriumRegistry`` scope so a test that forgets to call
69
+ * ``mockAtriumRegistry`` first surfaces as an empty assertion rather
70
+ * than a TypeError. */
71
+ export function fireAtriumEvent(kind, payload) {
72
+ const subs = activeSubscribers;
73
+ if (!subs)
74
+ return;
75
+ const handlers = subs.get(kind);
76
+ if (!handlers || handlers.size === 0)
77
+ return;
78
+ const event = { kind, payload };
79
+ // Snapshot so a handler that unsubscribes mid-fire doesn't skip a
80
+ // sibling handler that's later in iteration order.
81
+ for (const handler of [...handlers]) {
82
+ handler(event);
83
+ }
84
+ }
85
+ const DEFAULT_I18N_EN = {
86
+ 'common.save': 'Save',
87
+ 'common.cancel': 'Cancel',
88
+ 'common.delete': 'Delete',
89
+ 'common.edit': 'Edit',
90
+ 'common.new': 'New',
91
+ 'common.close': 'Close',
92
+ 'common.confirm': 'Confirm',
93
+ 'common.back': 'Back',
94
+ 'common.loading': 'Loading…',
95
+ 'common.empty': 'Nothing here yet.',
96
+ 'common.error': 'Something went wrong.',
97
+ 'common.search': 'Search',
98
+ 'common.required': 'Required',
99
+ 'common.language': 'Language',
100
+ };
101
+ function buildFakeRegistry(handles, subs) {
102
+ return {
103
+ registerHomeWidget(widget) {
104
+ handles.homeWidgets.push(widget);
105
+ },
106
+ registerRoute(route) {
107
+ handles.routes.push(route);
108
+ },
109
+ registerNavItem(item) {
110
+ handles.navItems.push(item);
111
+ },
112
+ registerAdminTab(tab) {
113
+ handles.adminTabs.push(tab);
114
+ },
115
+ registerProfileItem(item) {
116
+ handles.profileItems.push(item);
117
+ },
118
+ registerNotificationKind(renderer) {
119
+ handles.notificationKinds.push(renderer);
120
+ },
121
+ registerLocale(overlay) {
122
+ handles.localeOverlays.push(overlay);
123
+ },
124
+ subscribeEvent(kind, handler) {
125
+ let set = subs.get(kind);
126
+ if (!set) {
127
+ set = new Set();
128
+ subs.set(kind, set);
129
+ }
130
+ set.add(handler);
131
+ return () => {
132
+ set?.delete(handler);
133
+ };
134
+ },
135
+ };
136
+ }
137
+ /** Install a recording fake registry on ``window`` for the duration of
138
+ * a test. The handles returned record every register* call so tests
139
+ * can assert that a host bundle wired up the slots it claims to.
140
+ *
141
+ * ```ts
142
+ * let handles: MockAtriumHandles;
143
+ * beforeEach(() => {
144
+ * handles = mockAtriumRegistry({
145
+ * me: {
146
+ * id: 1, email: 'alice@example.com', full_name: 'Alice',
147
+ * is_active: true, roles: ['admin'],
148
+ * permissions: ['hello.toggle'], impersonating_from: null,
149
+ * },
150
+ * });
151
+ * });
152
+ * afterEach(() => handles.cleanup());
153
+ *
154
+ * test('host bundle registers a home widget', async () => {
155
+ * await import('../src/main');
156
+ * expect(handles.homeWidgets).toHaveLength(1);
157
+ * expect(handles.homeWidgets[0].key).toBe('hello-world');
158
+ * });
159
+ * ```
160
+ *
161
+ * Side effects:
162
+ * - ``window.__ATRIUM_REGISTRY__`` is replaced with the fake.
163
+ * - ``window.React`` is set to the package's React copy so
164
+ * ``makeWrapperElement(...)`` calls resolve. Tests that need a
165
+ * specific React identity can override after the call.
166
+ * - ``window.__atrium_i18n__`` is set to a fake i18n with the
167
+ * bundled English ``common.*`` strings (override via the
168
+ * ``i18n`` option). */
169
+ export function mockAtriumRegistry(options = {}) {
170
+ if (typeof window === 'undefined') {
171
+ throw new Error('[atrium-test-utils] mockAtriumRegistry requires a jsdom-style ' +
172
+ 'window — set vitest.environment to "jsdom" in vitest.config.ts');
173
+ }
174
+ const w = window;
175
+ const saved = {
176
+ registry: w.__ATRIUM_REGISTRY__,
177
+ React: w.React,
178
+ i18n: w.__atrium_i18n__,
179
+ };
180
+ const subs = new Map();
181
+ const handles = {
182
+ registry: undefined,
183
+ homeWidgets: [],
184
+ routes: [],
185
+ navItems: [],
186
+ adminTabs: [],
187
+ profileItems: [],
188
+ notificationKinds: [],
189
+ localeOverlays: [],
190
+ reset() {
191
+ handles.homeWidgets.length = 0;
192
+ handles.routes.length = 0;
193
+ handles.navItems.length = 0;
194
+ handles.adminTabs.length = 0;
195
+ handles.profileItems.length = 0;
196
+ handles.notificationKinds.length = 0;
197
+ handles.localeOverlays.length = 0;
198
+ subs.clear();
199
+ },
200
+ cleanup() {
201
+ w.__ATRIUM_REGISTRY__ = saved.registry;
202
+ w.React = saved.React;
203
+ w.__atrium_i18n__ = saved.i18n;
204
+ if (activeSubscribers === subs) {
205
+ activeSubscribers = null;
206
+ }
207
+ subs.clear();
208
+ },
209
+ };
210
+ handles.registry = buildFakeRegistry(handles, subs);
211
+ w.__ATRIUM_REGISTRY__ = handles.registry;
212
+ w.React = ReactNS;
213
+ const i18nResources = options.i18n?.resources ?? { en: DEFAULT_I18N_EN };
214
+ const language = options.i18n?.language ?? 'en';
215
+ w.__atrium_i18n__ = makeFakeI18n(i18nResources, language);
216
+ // Stash the configured ``me`` on a module-level slot so
217
+ // ``renderWithAtrium`` can pass it through to AtriumProvider's
218
+ // ``fetchUserContext`` override without each call site re-passing
219
+ // it. Cleared by ``cleanup()`` via the activeSubscribers reset.
220
+ configuredMe = options.me ?? null;
221
+ activeSubscribers = subs;
222
+ return handles;
223
+ }
224
+ // ---------------------------------------------------------------------------
225
+ // renderWithAtrium
226
+ // ---------------------------------------------------------------------------
227
+ let configuredMe = null;
228
+ function makeTestQueryClient() {
229
+ return new QueryClient({
230
+ defaultOptions: { queries: { retry: false } },
231
+ });
232
+ }
233
+ /** ``@testing-library/react``'s ``render`` pre-wired with a fresh
234
+ * QueryClient and ``<AtriumProvider>`` pointed at the configured
235
+ * ``me``. Returns the standard ``RenderResult`` so existing
236
+ * assertions (``getByRole``, ``rerender``, ``unmount``) work
237
+ * unchanged.
238
+ *
239
+ * ```tsx
240
+ * test('owners see the per-agent picker', () => {
241
+ * const { getByLabelText } = renderWithAtrium(<CommissionsPage />);
242
+ * expect(getByLabelText('Agent')).toBeInTheDocument();
243
+ * });
244
+ * ```
245
+ *
246
+ * Pass ``me`` to override the value from ``mockAtriumRegistry`` for
247
+ * a single render — handy in parameterised tests. Pass
248
+ * ``queryClient`` to share a client across renders so cache
249
+ * assertions survive the second mount. */
250
+ export function renderWithAtrium(ui, options = {}) {
251
+ const { me, queryClient, wrap, ...rest } = options;
252
+ const client = queryClient ?? makeTestQueryClient();
253
+ const meValue = me === undefined ? configuredMe : me;
254
+ const Wrap = wrap;
255
+ function Wrapper({ children }) {
256
+ const inner = createElement(AtriumProvider, {
257
+ fetchUserContext: async () => meValue,
258
+ children,
259
+ });
260
+ const wrapped = Wrap ? createElement(Wrap, { children: inner }) : inner;
261
+ return createElement(QueryClientProvider, { client, children: wrapped });
262
+ }
263
+ return render(ui, { wrapper: Wrapper, ...rest });
264
+ }
265
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@brendanbank/atrium-test-utils",
3
+ "version": "0.15.0",
4
+ "description": "Vitest helpers for unit-testing atrium host bundles. Installs a fake __ATRIUM_REGISTRY__ + window.React, wires a working QueryClient, and exposes a synthetic event bus that mirrors atrium's subscribeEvent semantics — so a host can render permission-gated components and fire SSE events from a test without standing up the SPA.",
5
+ "license": "BSD-2-Clause",
6
+ "type": "module",
7
+ "files": [
8
+ "dist",
9
+ "README.md"
10
+ ],
11
+ "main": "./dist/index.js",
12
+ "types": "./dist/index.d.ts",
13
+ "exports": {
14
+ ".": {
15
+ "types": "./dist/index.d.ts",
16
+ "default": "./dist/index.js"
17
+ }
18
+ },
19
+ "dependencies": {
20
+ "@brendanbank/atrium-host-bundle-utils": "0.15.0",
21
+ "@brendanbank/atrium-host-types": "0.15.0"
22
+ },
23
+ "peerDependencies": {
24
+ "@tanstack/react-query": "^5.0.0",
25
+ "@testing-library/react": "^16.0.0",
26
+ "react": "^18.0.0 || ^19.0.0",
27
+ "react-dom": "^18.0.0 || ^19.0.0",
28
+ "vitest": "^1.0.0 || ^2.0.0 || ^3.0.0 || ^4.0.0"
29
+ },
30
+ "devDependencies": {
31
+ "@tanstack/react-query": "^5.100.5",
32
+ "@testing-library/jest-dom": "^6.9.1",
33
+ "@testing-library/react": "^16.3.2",
34
+ "@types/node": "^25.6.0",
35
+ "@types/react": "^19.2.14",
36
+ "@types/react-dom": "^19.2.3",
37
+ "jsdom": "^29.0.2",
38
+ "react": "^19.2.5",
39
+ "react-dom": "^19.2.5",
40
+ "typescript": "^6.0.3",
41
+ "vitest": "^4.1.5"
42
+ },
43
+ "publishConfig": {
44
+ "access": "public"
45
+ },
46
+ "repository": {
47
+ "type": "git",
48
+ "url": "https://github.com/brendanbank/atrium.git",
49
+ "directory": "packages/test-utils"
50
+ },
51
+ "scripts": {
52
+ "build": "tsc -p tsconfig.json",
53
+ "typecheck": "tsc -p tsconfig.json --noEmit",
54
+ "test": "vitest run",
55
+ "test:watch": "vitest",
56
+ "clean": "rm -rf dist"
57
+ }
58
+ }