@everystate/react 1.0.0 → 1.0.1

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/README.md CHANGED
@@ -2,7 +2,8 @@
2
2
 
3
3
  **React adapter for EveryState with hooks**
4
4
 
5
- Use EveryState in React with `usePath`, `useIntent`, and `useAsync` hooks.
5
+ Use EveryState in React with `usePath`, `useIntent`, `useWildcard`, and `useAsync` hooks.
6
+ Built on React 18's `useSyncExternalStore` for concurrent-mode safety.
6
7
 
7
8
  ## Installation
8
9
 
@@ -13,20 +14,19 @@ npm install @everystate/react @everystate/core react
13
14
  ## Quick Start
14
15
 
15
16
  ```jsx
16
- import { createEventState } from '@everystate/core';
17
- import { EventStateProvider, usePath } from '@everystate/react';
17
+ import { createEveryState } from '@everystate/core';
18
+ import { EventStateProvider, usePath, useIntent } from '@everystate/react';
18
19
 
19
- const store = createEventState({ count: 0 });
20
+ const store = createEveryState({ count: 0 });
20
21
 
21
22
  function Counter() {
22
23
  const count = usePath('count');
24
+ const setCount = useIntent('count');
23
25
 
24
26
  return (
25
27
  <div>
26
28
  <p>Count: {count}</p>
27
- <button onClick={() => store.set('count', count + 1)}>
28
- Increment
29
- </button>
29
+ <button onClick={() => setCount(count + 1)}>Increment</button>
30
30
  </div>
31
31
  );
32
32
  }
@@ -42,9 +42,11 @@ function App() {
42
42
 
43
43
  ## Hooks
44
44
 
45
- - **`usePath(path)`** Subscribe to a specific path
46
- - **`useIntent(intentName, handler)`** Handle user intents
47
- - **`useAsync(path, fetcher)`** Async data loading
45
+ - **`usePath(path)`** - Subscribe to a dot-path. Re-renders only when that path changes.
46
+ - **`useIntent(path)`** - Returns a stable setter function for a path. Memoized to prevent unnecessary re-renders.
47
+ - **`useWildcard(path)`** - Subscribe to a wildcard path (e.g. `'user.*'`). Returns the parent object.
48
+ - **`useAsync(path)`** - Returns `{ data, status, error, execute, cancel }` for async operations.
49
+ - **`useStore()`** - Returns the raw store from context.
48
50
 
49
51
  ## License
50
52
 
@@ -0,0 +1,127 @@
1
+ import { createContext, useContext, useMemo, useSyncExternalStore } from 'react';
2
+
3
+ // ---- Context ----
4
+ const EventStateContext = createContext(null);
5
+
6
+ /**
7
+ * Provider: makes a store available to all child components via hooks.
8
+ * The store is created *outside* React. The provider is pure dependency injection.
9
+ *
10
+ * @param {{ store: object, children: React.ReactNode }} props
11
+ */
12
+ export function EventStateProvider({ store, children }) {
13
+ return (
14
+ <EventStateContext.Provider value={store}>
15
+ {children}
16
+ </EventStateContext.Provider>
17
+ );
18
+ }
19
+
20
+ /**
21
+ * useStore: returns the EventState store from context.
22
+ * Throws if called outside an EventStateProvider.
23
+ *
24
+ * @returns {object} The EventState store
25
+ */
26
+ export function useStore() {
27
+ const store = useContext(EventStateContext);
28
+ if (!store) {
29
+ throw new Error(
30
+ 'useStore: no store found. Wrap your component tree in <EventStateProvider store={store}>.'
31
+ );
32
+ }
33
+ return store;
34
+ }
35
+
36
+ /**
37
+ * usePath: subscribe to a dot-path in the store.
38
+ * Re-renders the component only when the value at that path changes.
39
+ * Uses React 18's useSyncExternalStore for concurrent-mode safety.
40
+ *
41
+ * @param {string} path: dot-separated state path (e.g. 'state.tasks')
42
+ * @returns {any} The current value at the path
43
+ */
44
+ export function usePath(path) {
45
+ const store = useStore();
46
+
47
+ const subscribe = useMemo(
48
+ () => (onStoreChange) => store.subscribe(path, () => onStoreChange()),
49
+ [store, path]
50
+ );
51
+
52
+ const getSnapshot = useMemo(
53
+ () => () => store.get(path),
54
+ [store, path]
55
+ );
56
+
57
+ return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
58
+ }
59
+
60
+ /**
61
+ * useIntent: returns a stable function that publishes a value to a path.
62
+ * Memoized so it won't cause unnecessary re-renders when passed as a prop.
63
+ *
64
+ * @param {string} path: dot-separated intent path (e.g. 'intent.addTask')
65
+ * @returns {(value: any) => any} A setter function
66
+ */
67
+ export function useIntent(path) {
68
+ const store = useStore();
69
+ return useMemo(
70
+ () => (value) => store.set(path, value),
71
+ [store, path]
72
+ );
73
+ }
74
+
75
+ /**
76
+ * useWildcard: subscribe to a wildcard path (e.g. 'state.*').
77
+ * Re-renders whenever any child of that path changes.
78
+ * The returned value is the parent object at the path prefix.
79
+ *
80
+ * @param {string} wildcardPath: e.g. 'state.tasks.*' or 'state.*'
81
+ * @returns {any} The current value at the parent path
82
+ */
83
+ export function useWildcard(wildcardPath) {
84
+ const store = useStore();
85
+ const parentPath = wildcardPath.endsWith('.*')
86
+ ? wildcardPath.slice(0, -2)
87
+ : wildcardPath;
88
+
89
+ const subscribe = useMemo(
90
+ () => (onStoreChange) => store.subscribe(wildcardPath, () => onStoreChange()),
91
+ [store, wildcardPath]
92
+ );
93
+
94
+ const getSnapshot = useMemo(
95
+ () => () => store.get(parentPath),
96
+ [store, parentPath]
97
+ );
98
+
99
+ return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
100
+ }
101
+
102
+ /**
103
+ * useAsync: trigger an async operation and subscribe to its status.
104
+ * Returns { data, status, error, execute, cancel }.
105
+ *
106
+ * @param {string} path: base path for the async operation
107
+ * @returns {{ data: any, status: string, error: any, execute: Function, cancel: Function }}
108
+ */
109
+ export function useAsync(path) {
110
+ const store = useStore();
111
+
112
+ const data = usePath(`${path}.data`);
113
+ const status = usePath(`${path}.status`);
114
+ const error = usePath(`${path}.error`);
115
+
116
+ const execute = useMemo(
117
+ () => (fetcher) => store.setAsync(path, fetcher),
118
+ [store, path]
119
+ );
120
+
121
+ const cancel = useMemo(
122
+ () => () => store.cancel(path),
123
+ [store, path]
124
+ );
125
+
126
+ return { data, status, error, execute, cancel };
127
+ }
package/index.d.ts ADDED
@@ -0,0 +1,66 @@
1
+ /**
2
+ * @everystate/react
3
+ *
4
+ * React adapter for EveryState with hooks.
5
+ * Built on React 18's useSyncExternalStore for concurrent-mode safety.
6
+ */
7
+
8
+ import type { EveryStateStore } from '@everystate/core';
9
+ import type { ReactNode } from 'react';
10
+
11
+ /**
12
+ * Provider: makes a store available to all child components via hooks.
13
+ * The store is created *outside* React. The provider is pure dependency injection.
14
+ */
15
+ export function EventStateProvider(props: {
16
+ store: EveryStateStore;
17
+ children: ReactNode;
18
+ }): JSX.Element;
19
+
20
+ /**
21
+ * Returns the EveryState store from context.
22
+ * Throws if called outside an EventStateProvider.
23
+ */
24
+ export function useStore(): EveryStateStore;
25
+
26
+ /**
27
+ * Subscribe to a dot-path in the store.
28
+ * Re-renders the component only when the value at that path changes.
29
+ * Uses React 18's useSyncExternalStore for concurrent-mode safety.
30
+ *
31
+ * @param path - Dot-separated state path (e.g. 'user.name')
32
+ * @returns The current value at the path
33
+ */
34
+ export function usePath(path: string): any;
35
+
36
+ /**
37
+ * Returns a stable function that publishes a value to a path.
38
+ * Memoized so it won't cause unnecessary re-renders when passed as a prop.
39
+ *
40
+ * @param path - Dot-separated intent path (e.g. 'intent.addTask')
41
+ * @returns A setter function
42
+ */
43
+ export function useIntent(path: string): (value: any) => any;
44
+
45
+ /**
46
+ * Subscribe to a wildcard path (e.g. 'state.*').
47
+ * Re-renders whenever any child of that path changes.
48
+ * The returned value is the parent object at the path prefix.
49
+ *
50
+ * @param wildcardPath - e.g. 'state.tasks.*' or 'state.*'
51
+ * @returns The current value at the parent path
52
+ */
53
+ export function useWildcard(wildcardPath: string): any;
54
+
55
+ /**
56
+ * Trigger an async operation and subscribe to its status.
57
+ *
58
+ * @param path - Base path for the async operation
59
+ */
60
+ export function useAsync(path: string): {
61
+ data: any;
62
+ status: string | undefined;
63
+ error: any;
64
+ execute: (fetcher: (signal: AbortSignal) => Promise<any>) => Promise<any>;
65
+ cancel: () => void;
66
+ };
package/index.js CHANGED
@@ -1,8 +1,15 @@
1
1
  /**
2
2
  * @everystate/react
3
3
  *
4
- * EveryState wrapper for @uistate/react
5
- * Re-exports all functionality from the underlying @uistate/react package
4
+ * React adapter for EveryState with hooks:
5
+ * EventStateProvider, useStore, usePath, useIntent, useWildcard, useAsync
6
6
  */
7
7
 
8
- export * from '@uistate/react';
8
+ export {
9
+ EventStateProvider,
10
+ useStore,
11
+ usePath,
12
+ useIntent,
13
+ useWildcard,
14
+ useAsync,
15
+ } from './eventStateReact.js';
package/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "@everystate/react",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "EveryState React: React adapter with usePath, useIntent, useAsync hooks and EventStateProvider",
5
5
  "type": "module",
6
6
  "main": "index.js",
7
+ "types": "index.d.ts",
7
8
  "keywords": [
8
9
  "everystate",
9
10
  "react",
@@ -24,7 +25,7 @@
24
25
  "type": "git",
25
26
  "url": "https://github.com/ImsirovicAjdin/everystate-react"
26
27
  },
27
- "dependencies": {
28
- "@uistate/react": "^1.0.1"
28
+ "peerDependencies": {
29
+ "react": ">=18.0.0"
29
30
  }
30
31
  }
package/self-test.js ADDED
@@ -0,0 +1,166 @@
1
+ /**
2
+ * @everystate/react: zero-dependency self-test
3
+ *
4
+ * Since the React adapter uses JSX and React hooks (which require a React
5
+ * runtime and JSX transform), this self-test verifies:
6
+ * 1. The module exports exist and are functions
7
+ * 2. The store-side patterns that the hooks consume work correctly
8
+ *
9
+ * The hooks themselves (usePath, useIntent, useWildcard, useAsync) are
10
+ * thin wrappers around store.subscribe + useSyncExternalStore.
11
+ * Testing the store patterns proves the hooks will work.
12
+ */
13
+
14
+ import { createEveryState } from '@everystate/core';
15
+
16
+ let passed = 0;
17
+ let failed = 0;
18
+
19
+ function assert(label, condition) {
20
+ if (condition) {
21
+ console.log(` ✓ ${label}`);
22
+ passed++;
23
+ } else {
24
+ console.error(` ✗ ${label}`);
25
+ failed++;
26
+ }
27
+ }
28
+
29
+ function section(title) {
30
+ console.log(`\n${title}`);
31
+ }
32
+
33
+ // -- 1. usePath pattern: subscribe + get -----------------------------
34
+
35
+ section('1. usePath pattern: subscribe + get');
36
+
37
+ const s1 = createEveryState({ user: { name: 'Alice' } });
38
+ let s1snap = s1.get('user.name');
39
+ const unsub1 = s1.subscribe('user.name', () => { s1snap = s1.get('user.name'); });
40
+ assert('initial snapshot', s1snap === 'Alice');
41
+
42
+ s1.set('user.name', 'Bob');
43
+ assert('snapshot updates on set', s1snap === 'Bob');
44
+
45
+ unsub1();
46
+ s1.set('user.name', 'Charlie');
47
+ assert('unsubscribe stops updates', s1snap === 'Bob');
48
+ s1.destroy();
49
+
50
+ // -- 2. useIntent pattern: stable setter -----------------------------
51
+
52
+ section('2. useIntent pattern: stable setter');
53
+
54
+ const s2 = createEveryState({ intent: { addTask: null } });
55
+ const setter = (value) => s2.set('intent.addTask', value);
56
+ setter({ text: 'Buy milk' });
57
+ assert('setter writes to path', s2.get('intent.addTask').text === 'Buy milk');
58
+
59
+ setter(null);
60
+ assert('setter clears value', s2.get('intent.addTask') === null);
61
+ s2.destroy();
62
+
63
+ // -- 3. useWildcard pattern: subscribe wildcard + get parent ---------
64
+
65
+ section('3. useWildcard pattern: wildcard subscribe');
66
+
67
+ const s3 = createEveryState({ state: { tasks: { t1: 'A', t2: 'B' } } });
68
+ let wildcardFires = 0;
69
+ const unsub3 = s3.subscribe('state.tasks.*', () => {
70
+ wildcardFires++;
71
+ });
72
+
73
+ s3.set('state.tasks.t1', 'A updated');
74
+ assert('wildcard fires on child change', wildcardFires === 1);
75
+
76
+ s3.set('state.tasks.t3', 'C');
77
+ assert('wildcard fires on new child', wildcardFires === 2);
78
+
79
+ const parent = s3.get('state.tasks');
80
+ assert('get parent returns object', typeof parent === 'object');
81
+ assert('parent has t1', parent.t1 === 'A updated');
82
+ assert('parent has t3', parent.t3 === 'C');
83
+
84
+ unsub3();
85
+ s3.destroy();
86
+
87
+ // -- 4. useAsync pattern: setAsync lifecycle -------------------------
88
+
89
+ section('4. useAsync pattern: setAsync lifecycle');
90
+
91
+ const s4 = createEveryState({});
92
+ const promise = s4.setAsync('users', async () => [{ id: 1, name: 'Alice' }]);
93
+
94
+ // During loading
95
+ assert('loading: status = loading', s4.get('users.status') === 'loading');
96
+ assert('loading: error = null', s4.get('users.error') === null);
97
+
98
+ await promise;
99
+
100
+ // After success
101
+ assert('success: status = success', s4.get('users.status') === 'success');
102
+ assert('success: data is array', Array.isArray(s4.get('users.data')));
103
+ assert('success: data[0].name = Alice', s4.get('users.data')[0].name === 'Alice');
104
+ s4.destroy();
105
+
106
+ // -- 5. useAsync error pattern ---------------------------------------
107
+
108
+ section('5. useAsync error pattern');
109
+
110
+ const s5 = createEveryState({});
111
+ try {
112
+ await s5.setAsync('data', async () => { throw new Error('Network error'); });
113
+ } catch {}
114
+ assert('error: status = error', s5.get('data.status') === 'error');
115
+ assert('error: error message exists', typeof s5.get('data.error') === 'string' || s5.get('data.error') instanceof Error);
116
+ s5.destroy();
117
+
118
+ // -- 6. Provider pattern: store as context ---------------------------
119
+
120
+ section('6. Provider pattern: store as external dependency');
121
+
122
+ const s6 = createEveryState({ count: 0 });
123
+ // The provider just passes the store via React context.
124
+ // We verify the store is usable as an external store.
125
+ const subscribe = (onStoreChange) => s6.subscribe('count', () => onStoreChange());
126
+ const getSnapshot = () => s6.get('count');
127
+
128
+ let latestSnapshot = getSnapshot();
129
+ const unsub6 = subscribe(() => { latestSnapshot = getSnapshot(); });
130
+
131
+ s6.set('count', 10);
132
+ assert('external store subscribe works', latestSnapshot === 10);
133
+
134
+ s6.set('count', 20);
135
+ assert('external store re-fires', latestSnapshot === 20);
136
+
137
+ unsub6();
138
+ s6.destroy();
139
+
140
+ // -- 7. Batch pattern (React 18 automatic batching) ------------------
141
+
142
+ section('7. batch pattern (React 18 compat)');
143
+
144
+ const s7 = createEveryState({ a: 0, b: 0 });
145
+ let renderCount = 0;
146
+ s7.subscribe('a', () => { renderCount++; });
147
+ s7.subscribe('b', () => { renderCount++; });
148
+
149
+ s7.batch(() => {
150
+ s7.set('a', 1);
151
+ s7.set('b', 2);
152
+ });
153
+ assert('batch: 2 subscribers fire once each', renderCount === 2);
154
+ assert('batch: a = 1', s7.get('a') === 1);
155
+ assert('batch: b = 2', s7.get('b') === 2);
156
+ s7.destroy();
157
+
158
+ // -- Summary ---------------------------------------------------------
159
+
160
+ console.log(`\n@everystate/react v1.0.0 self-test`);
161
+ if (failed > 0) {
162
+ console.error(`✗ ${failed} assertion(s) failed, ${passed} passed`);
163
+ process.exit(1);
164
+ } else {
165
+ console.log(`✓ ${passed} assertions passed`);
166
+ }
@@ -0,0 +1,179 @@
1
+ /**
2
+ * @everystate/react: integration tests via @everystate/test
3
+ *
4
+ * Tests the store-side patterns that React hooks consume.
5
+ * Since EveryState is the IR, testing the IR proves the hooks work.
6
+ * The hooks are thin wrappers: usePath = subscribe + get,
7
+ * useIntent = set, useWildcard = subscribe wildcard + get parent.
8
+ *
9
+ * JSX/React-specific behavior (re-renders, concurrent mode) requires
10
+ * a React test environment and is outside the scope of these tests.
11
+ */
12
+
13
+ import { createEventTest, runTests } from '@everystate/test';
14
+ import { createEveryState } from '@everystate/core';
15
+
16
+ const results = runTests({
17
+
18
+ // -- usePath patterns ----------------------------------------------
19
+
20
+ 'usePath: subscribe to exact path': () => {
21
+ const t = createEventTest({ user: { name: 'Alice', age: 30 } });
22
+ t.trigger('user.name', 'Bob');
23
+ t.assertPath('user.name', 'Bob');
24
+ t.assertType('user.name', 'string');
25
+ t.assertEventFired('user.name', 1);
26
+ },
27
+
28
+ 'usePath: nested path subscription': () => {
29
+ const t = createEventTest({ app: { settings: { theme: 'dark' } } });
30
+ t.trigger('app.settings.theme', 'light');
31
+ t.assertPath('app.settings.theme', 'light');
32
+ t.assertEventFired('app.settings.theme', 1);
33
+ },
34
+
35
+ 'usePath: unsubscribe stops notifications': () => {
36
+ const store = createEventState({ count: 0 });
37
+ let fires = 0;
38
+ const unsub = store.subscribe('count', () => { fires++; });
39
+ store.set('count', 1);
40
+ unsub();
41
+ store.set('count', 2);
42
+ if (fires !== 1) throw new Error(`Expected 1 fire after unsub, got ${fires}`);
43
+ store.destroy();
44
+ },
45
+
46
+ // -- useIntent patterns --------------------------------------------
47
+
48
+ 'useIntent: set value at path': () => {
49
+ const t = createEventTest({ intent: { addTask: null } });
50
+ t.trigger('intent.addTask', { text: 'Buy milk', done: false });
51
+ t.assertPath('intent.addTask', { text: 'Buy milk', done: false });
52
+ t.assertType('intent.addTask', 'object');
53
+ },
54
+
55
+ 'useIntent: reset intent after processing': () => {
56
+ const t = createEventTest({ intent: { submit: null } });
57
+ t.trigger('intent.submit', { form: 'login' });
58
+ t.trigger('intent.submit', null);
59
+ t.assertPath('intent.submit', null);
60
+ },
61
+
62
+ // -- useWildcard patterns ------------------------------------------
63
+
64
+ 'useWildcard: fires on any child change': () => {
65
+ const store = createEventState({ state: { tasks: {} } });
66
+ let fires = 0;
67
+ store.subscribe('state.tasks.*', () => { fires++; });
68
+ store.set('state.tasks.t1', { text: 'A', done: false });
69
+ store.set('state.tasks.t2', { text: 'B', done: false });
70
+ if (fires !== 2) throw new Error(`Expected 2 fires, got ${fires}`);
71
+ store.destroy();
72
+ },
73
+
74
+ 'useWildcard: get parent returns full object': () => {
75
+ const t = createEventTest({ state: { tasks: { t1: 'A', t2: 'B' } } });
76
+ t.trigger('state.tasks.t3', 'C');
77
+ t.assertPath('state.tasks', { t1: 'A', t2: 'B', t3: 'C' });
78
+ },
79
+
80
+ // -- useAsync patterns ---------------------------------------------
81
+
82
+ 'useAsync: loading → success lifecycle': async () => {
83
+ const store = createEventState({});
84
+ await store.setAsync('users', async () => [{ id: 1, name: 'Alice' }]);
85
+ if (store.get('users.status') !== 'success') throw new Error('Expected success');
86
+ if (!Array.isArray(store.get('users.data'))) throw new Error('Expected array');
87
+ store.destroy();
88
+ },
89
+
90
+ 'useAsync: loading → error lifecycle': async () => {
91
+ const store = createEventState({});
92
+ try {
93
+ await store.setAsync('data', async () => { throw new Error('fail'); });
94
+ } catch {}
95
+ if (store.get('data.status') !== 'error') throw new Error('Expected error');
96
+ store.destroy();
97
+ },
98
+
99
+ 'useAsync: status/data/error paths are typed': () => {
100
+ const t = createEventTest({});
101
+ t.store.setMany({
102
+ 'users.status': 'success',
103
+ 'users.data': [{ id: 1, name: 'Alice' }],
104
+ 'users.error': null,
105
+ });
106
+ t.assertType('users.status', 'string');
107
+ t.assertArrayOf('users.data', { id: 'number', name: 'string' });
108
+ },
109
+
110
+ // -- Provider pattern ----------------------------------------------
111
+
112
+ 'provider: store as external store (useSyncExternalStore compat)': () => {
113
+ const store = createEventState({ count: 0 });
114
+ // Simulate useSyncExternalStore contract
115
+ let snapshot = store.get('count');
116
+ const subscribe = (cb) => store.subscribe('count', () => {
117
+ snapshot = store.get('count');
118
+ cb();
119
+ });
120
+ let renderCount = 0;
121
+ const unsub = subscribe(() => { renderCount++; });
122
+
123
+ store.set('count', 1);
124
+ if (snapshot !== 1) throw new Error('Snapshot should be 1');
125
+ if (renderCount !== 1) throw new Error('Should have rendered once');
126
+
127
+ store.set('count', 2);
128
+ if (snapshot !== 2) throw new Error('Snapshot should be 2');
129
+ if (renderCount !== 2) throw new Error('Should have rendered twice');
130
+
131
+ unsub();
132
+ store.destroy();
133
+ },
134
+
135
+ // -- batch (React 18 automatic batching compat) --------------------
136
+
137
+ 'batch: atomic updates (React 18 compat)': () => {
138
+ const t = createEventTest({ form: { name: '', email: '' } });
139
+ t.store.batch(() => {
140
+ t.trigger('form.name', 'Alice');
141
+ t.trigger('form.email', 'alice@example.com');
142
+ });
143
+ t.assertPath('form.name', 'Alice');
144
+ t.assertPath('form.email', 'alice@example.com');
145
+ // Each path fires once after batch
146
+ t.assertEventFired('form.name', 1);
147
+ t.assertEventFired('form.email', 1);
148
+ },
149
+
150
+ 'batch: setMany for atomic route updates': () => {
151
+ const t = createEventTest({});
152
+ t.store.setMany({
153
+ 'ui.route.view': 'dashboard',
154
+ 'ui.route.path': '/dashboard',
155
+ 'ui.route.params': {},
156
+ });
157
+ t.assertPath('ui.route.view', 'dashboard');
158
+ t.assertPath('ui.route.path', '/dashboard');
159
+ t.assertType('ui.route.view', 'string');
160
+ },
161
+
162
+ // -- type generation from React patterns ---------------------------
163
+
164
+ 'types: React app state shape': () => {
165
+ const t = createEventTest({
166
+ user: { name: 'Alice', email: 'alice@example.com', role: 'admin' },
167
+ tasks: [{ id: 1, text: 'Buy milk', done: false }],
168
+ ui: { theme: 'dark', sidebarOpen: true },
169
+ });
170
+ t.assertShape('user', { name: 'string', email: 'string', role: 'string' });
171
+ t.assertArrayOf('tasks', { id: 'number', text: 'string', done: 'boolean' });
172
+ t.assertShape('ui', { theme: 'string', sidebarOpen: 'boolean' });
173
+
174
+ const types = t.getTypeAssertions();
175
+ if (types.length !== 3) throw new Error(`Expected 3 type assertions, got ${types.length}`);
176
+ },
177
+ });
178
+
179
+ if (results.failed > 0) process.exit(1);