react-shared-states 1.0.7 → 1.0.11

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.
@@ -0,0 +1,526 @@
1
+ import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'
2
+ import React, {useEffect} from 'react'
3
+ import {act, cleanup, fireEvent, render, screen} from '@testing-library/react'
4
+ import {
5
+ createSharedFunction,
6
+ createSharedState,
7
+ createSharedSubscription,
8
+ sharedFunctionsApi,
9
+ sharedStatesApi,
10
+ SharedStatesProvider,
11
+ sharedSubscriptionsApi,
12
+ useSharedFunction,
13
+ useSharedState,
14
+ useSharedSubscription
15
+ } from "../src";
16
+ import type {Subscriber, SubscriberEvents} from "../src/hooks/use-shared-subscription";
17
+
18
+ // Mocking random to have predictable keys for created states/functions/subscriptions
19
+ vi.mock('../src/lib/utils', async (importActual) => {
20
+ const actual = await importActual<typeof import('../src/lib/utils')>();
21
+ let count = 0;
22
+ // noinspection JSUnusedGlobalSymbols
23
+ return {
24
+ ...actual,
25
+ random: () => `test-key-${count++}`,
26
+ };
27
+ });
28
+
29
+ beforeEach(() => {
30
+ cleanup();
31
+ // Reset the mocked random key counter
32
+ vi.clearAllMocks();
33
+ });
34
+ afterEach(() => {
35
+ vi.useRealTimers();
36
+ })
37
+
38
+ describe('useSharedState', () => {
39
+ it('should share state between two components', () => {
40
+ const TestComponent1 = () => {
41
+ const [count] = useSharedState('count', 0);
42
+ return <span data-testid="value1">{count}</span>;
43
+ };
44
+
45
+ const TestComponent2 = () => {
46
+ const [count, setCount] = useSharedState('count', 0);
47
+ return (
48
+ <div>
49
+ <span data-testid="value2">{count}</span>
50
+ <button onClick={() => setCount(c => c + 1)}>inc</button>
51
+ </div>
52
+ );
53
+ };
54
+
55
+ render(
56
+ <>
57
+ <TestComponent1/>
58
+ <TestComponent2/>
59
+ </>
60
+ );
61
+
62
+ expect(screen.getByTestId('value1').textContent).toBe('0');
63
+ expect(screen.getByTestId('value2').textContent).toBe('0');
64
+
65
+ act(() => {
66
+ fireEvent.click(screen.getByText('inc'));
67
+ });
68
+
69
+ expect(screen.getByTestId('value1').textContent).toBe('1');
70
+ expect(screen.getByTestId('value2').textContent).toBe('1');
71
+ });
72
+
73
+ it('should isolate state with SharedStatesProvider', () => {
74
+ const TestComponent = () => {
75
+ const [count, setCount] = useSharedState('count', 0);
76
+ return (
77
+ <div>
78
+ <span>{count}</span>
79
+ <button onClick={() => setCount(c => c + 1)}>inc</button>
80
+ </div>
81
+ );
82
+ };
83
+
84
+ render(
85
+ <div>
86
+ <div data-testid="scope1">
87
+ <SharedStatesProvider scopeName="scope1">
88
+ <TestComponent/>
89
+ </SharedStatesProvider>
90
+ </div>
91
+ <div data-testid="scope2">
92
+ <SharedStatesProvider scopeName="scope2">
93
+ <TestComponent/>
94
+ </SharedStatesProvider>
95
+ </div>
96
+ </div>
97
+ );
98
+
99
+ const scope1Button = screen.getAllByText('inc')[0];
100
+ const scope2Button = screen.getAllByText('inc')[1];
101
+
102
+ act(() => {
103
+ fireEvent.click(scope1Button);
104
+ });
105
+
106
+ expect(screen.getByTestId('scope1').textContent).toContain('1');
107
+ expect(screen.getByTestId('scope2').textContent).toContain('0');
108
+
109
+ act(() => {
110
+ fireEvent.click(scope2Button);
111
+ fireEvent.click(scope2Button);
112
+ });
113
+
114
+ expect(screen.getByTestId('scope1').textContent).toContain('1');
115
+ expect(screen.getByTestId('scope2').textContent).toContain('2');
116
+ });
117
+
118
+ it('should work with createSharedState', () => {
119
+ const sharedCounter = createSharedState(10);
120
+
121
+ const TestComponent1 = () => {
122
+ const [count] = useSharedState(sharedCounter);
123
+ return <span data-testid="value1">{count}</span>;
124
+ };
125
+
126
+ const TestComponent2 = () => {
127
+ const [count, setCount] = useSharedState(sharedCounter);
128
+ return <button onClick={() => setCount(count + 5)}>inc</button>;
129
+ };
130
+
131
+ render(
132
+ <>
133
+ <TestComponent1/>
134
+ <TestComponent2/>
135
+ </>
136
+ );
137
+
138
+ expect(screen.getByTestId('value1').textContent).toBe('10');
139
+
140
+ act(() => {
141
+ fireEvent.click(screen.getByText('inc'));
142
+ });
143
+
144
+ expect(screen.getByTestId('value1').textContent).toBe('15');
145
+ });
146
+
147
+ it('should allow direct api manipulation with createSharedState objects', () => {
148
+ const sharedCounter = createSharedState(100);
149
+
150
+ // Get initial value
151
+ expect(sharedStatesApi.get(sharedCounter)).toBe(100);
152
+
153
+ // Set a new value
154
+ act(() => {
155
+ sharedStatesApi.set(sharedCounter, 200);
156
+ });
157
+
158
+ // Get updated value
159
+ expect(sharedStatesApi.get(sharedCounter)).toBe(200);
160
+
161
+ // Clear the value
162
+ act(() => {
163
+ sharedStatesApi.clear(sharedCounter);
164
+ });
165
+
166
+ // Get value after clear (should be initial value because createSharedState re-initializes it)
167
+ expect(sharedStatesApi.get(sharedCounter)).toBe(100);
168
+ });
169
+ });
170
+
171
+ describe('useSharedFunction', () => {
172
+ const mockApiCall = vi.fn((...args: any[]) => new Promise(resolve => setTimeout(() => resolve(`result: ${args.join(',')}`), 100)));
173
+
174
+ beforeEach(() => {
175
+ mockApiCall.mockClear();
176
+ vi.useFakeTimers();
177
+ });
178
+
179
+ const TestComponent = ({fnKey, sharedFn}: { fnKey: string, sharedFn?: any }) => {
180
+ const {state, trigger, forceTrigger, clear} = sharedFn ? useSharedFunction(sharedFn) : useSharedFunction(fnKey, mockApiCall);
181
+ return (
182
+ <div>
183
+ {state.isLoading && <span>Loading...</span>}
184
+ {state.error as any && <span>{String(state.error)}</span>}
185
+ {state.results && <span data-testid="result">{String(state.results)}</span>}
186
+ <button onClick={() => trigger('arg1')}>trigger</button>
187
+ <button onClick={() => forceTrigger('arg2')}>force</button>
188
+ <button onClick={() => clear()}>clear</button>
189
+ </div>
190
+ );
191
+ };
192
+
193
+ it('should handle async function lifecycle', async () => {
194
+ render(<TestComponent fnKey="test-fn"/>);
195
+
196
+ // Initial state
197
+ expect(screen.queryByText('Loading...')).toBeNull();
198
+ expect(screen.queryByTestId('result')).toBeNull();
199
+
200
+ // Trigger
201
+ act(() => {
202
+ fireEvent.click(screen.getByText('trigger'));
203
+ });
204
+ expect(screen.getByText('Loading...')).toBeDefined();
205
+
206
+ // Resolve
207
+ await act(async () => {
208
+ await vi.advanceTimersByTimeAsync(100);
209
+ });
210
+ expect(screen.queryByText('Loading...')).toBeNull();
211
+ expect(screen.getByTestId('result').textContent).toBe('result: arg1');
212
+ expect(mockApiCall).toHaveBeenCalledTimes(1);
213
+ expect(mockApiCall).toHaveBeenCalledWith('arg1');
214
+ });
215
+
216
+ it('should not trigger if already running or has data', async () => {
217
+ render(<TestComponent fnKey="test-fn"/>);
218
+ act(() => {
219
+ fireEvent.click(screen.getByText('trigger'));
220
+ });
221
+ await act(async () => {
222
+ await vi.advanceTimersByTimeAsync(100);
223
+ });
224
+ expect(mockApiCall).toHaveBeenCalledTimes(1);
225
+
226
+ // Trigger again, should not call mockApiCall
227
+ act(() => {
228
+ fireEvent.click(screen.getByText('trigger'));
229
+ });
230
+ expect(mockApiCall).toHaveBeenCalledTimes(1);
231
+ });
232
+
233
+ it('should force trigger', async () => {
234
+ render(<TestComponent fnKey="test-fn"/>);
235
+ act(() => {
236
+ fireEvent.click(screen.getByText('trigger'));
237
+ });
238
+ await act(async () => {
239
+ await vi.advanceTimersByTimeAsync(100);
240
+ });
241
+ expect(mockApiCall).toHaveBeenCalledTimes(1);
242
+
243
+ // Force trigger
244
+ act(() => {
245
+ fireEvent.click(screen.getByText('force'));
246
+ });
247
+ expect(screen.getByText('Loading...')).toBeDefined();
248
+ await act(async () => {
249
+ await vi.advanceTimersByTimeAsync(100);
250
+ });
251
+ expect(mockApiCall).toHaveBeenCalledTimes(2);
252
+ expect(mockApiCall).toHaveBeenCalledWith('arg2');
253
+ expect(screen.getByTestId('result').textContent).toBe('result: arg2');
254
+ });
255
+
256
+ it('should clear state', async () => {
257
+ render(<TestComponent fnKey="test-fn"/>);
258
+ act(() => {
259
+ fireEvent.click(screen.getByText('trigger'));
260
+ });
261
+ await act(async () => {
262
+ await vi.advanceTimersByTimeAsync(100);
263
+ });
264
+ expect(screen.getByTestId('result')).toBeDefined();
265
+
266
+ act(() => {
267
+ fireEvent.click(screen.getByText('clear'));
268
+ });
269
+ expect(screen.queryByTestId('result')).toBeNull();
270
+ });
271
+
272
+ it('should work with createSharedFunction', async () => {
273
+ const sharedFunction = createSharedFunction(mockApiCall);
274
+ render(<TestComponent fnKey="unused" sharedFn={sharedFunction}/>);
275
+
276
+ act(() => {
277
+ fireEvent.click(screen.getByText('trigger'));
278
+ });
279
+ await act(async () => {
280
+ await vi.advanceTimersByTimeAsync(100);
281
+ });
282
+ expect(mockApiCall).toHaveBeenCalledTimes(1);
283
+ expect(screen.getByTestId('result').textContent).toBe('result: arg1');
284
+ });
285
+
286
+ it('should allow direct api manipulation with createSharedFunction objects', () => {
287
+ const sharedFunction = createSharedFunction(async (arg: string) => `result: ${arg}`);
288
+
289
+ // Get initial state
290
+ const initialState = sharedFunctionsApi.get(sharedFunction);
291
+ expect(initialState.results).toBeUndefined();
292
+ expect(initialState.isLoading).toBe(false);
293
+ expect(initialState.error).toBeUndefined();
294
+
295
+ // Set a new state
296
+ act(() => {
297
+ sharedFunctionsApi.set(sharedFunction, {
298
+ fnState: {
299
+ results: 'test data',
300
+ isLoading: true,
301
+ error: 'test error',
302
+ }
303
+ });
304
+ });
305
+
306
+ // Get updated state
307
+ const updatedState = sharedFunctionsApi.get(sharedFunction);
308
+ expect(updatedState.results).toBe('test data');
309
+ expect(updatedState.isLoading).toBe(true);
310
+ expect(updatedState.error).toBe('test error');
311
+
312
+ // Clear the value
313
+ act(() => {
314
+ sharedFunctionsApi.clear(sharedFunction);
315
+ });
316
+
317
+ // Get value after clear (should be initial value)
318
+ const clearedState = sharedFunctionsApi.get(sharedFunction);
319
+ expect(clearedState.results).toBeUndefined();
320
+ expect(clearedState.isLoading).toBe(false);
321
+ expect(clearedState.error).toBeUndefined();
322
+ });
323
+ });
324
+
325
+ describe('useSharedSubscription', () => {
326
+ it('should handle subscription lifecycle', () => {
327
+ const mockSubscriber = vi.fn<Subscriber<string>>((set) => {
328
+ set('initial data');
329
+ return () => {
330
+ };
331
+ });
332
+
333
+ const TestComponent = () => {
334
+ const {state: {data}, trigger} = useSharedSubscription('test-sub', mockSubscriber);
335
+
336
+ useEffect(() => {
337
+ trigger();
338
+ }, []);
339
+
340
+ return <span data-testid="data">{data}</span>;
341
+ };
342
+
343
+ render(<TestComponent/>);
344
+
345
+ expect(mockSubscriber).toHaveBeenCalledTimes(1);
346
+ expect(screen.getByTestId('data').textContent).toBe('initial data');
347
+ });
348
+
349
+ it('should allow direct api manipulation with createSharedSubscription objects', () => {
350
+ const mockSubscriber = vi.fn();
351
+ const sharedSubscription = createSharedSubscription(mockSubscriber);
352
+
353
+ // Get initial state
354
+ const initialState = sharedSubscriptionsApi.get(sharedSubscription);
355
+ expect(initialState.data).toBeUndefined();
356
+ expect(initialState.isLoading).toBe(false);
357
+ expect(initialState.error).toBeUndefined();
358
+
359
+ // Set a new state
360
+ act(() => {
361
+ sharedSubscriptionsApi.set(sharedSubscription, {
362
+ fnState: {
363
+ data: 'test data',
364
+ isLoading: true,
365
+ error: 'test error',
366
+ }
367
+ });
368
+ });
369
+
370
+ // Get updated state
371
+ const updatedState = sharedSubscriptionsApi.get(sharedSubscription);
372
+ expect(updatedState.data).toBe('test data');
373
+ expect(updatedState.isLoading).toBe(true);
374
+ expect(updatedState.error).toBe('test error');
375
+
376
+ // Clear the value
377
+ act(() => {
378
+ sharedSubscriptionsApi.clear(sharedSubscription);
379
+ });
380
+
381
+ // Get value after clear (should be initial value)
382
+ const clearedState = sharedSubscriptionsApi.get(sharedSubscription);
383
+ expect(clearedState.data).toBeUndefined();
384
+ expect(clearedState.isLoading).toBe(false);
385
+ expect(clearedState.error).toBeUndefined();
386
+ });
387
+ });
388
+
389
+ describe('useSharedSubscription', () => {
390
+ let mockSubscriber: (set: SubscriberEvents.Set<any>, onError: SubscriberEvents.OnError, onCompletion: SubscriberEvents.OnCompletion) => () => void;
391
+ const mockUnsubscribe = vi.fn();
392
+
393
+ beforeEach(() => {
394
+ mockUnsubscribe.mockClear();
395
+ mockSubscriber = vi.fn((set, _onError, onCompletion) => {
396
+ // Simulate async subscription
397
+ const timeout = setTimeout(() => {
398
+ set('initial data');
399
+ onCompletion();
400
+ }, 100);
401
+ return () => {
402
+ clearTimeout(timeout);
403
+ mockUnsubscribe();
404
+ };
405
+ });
406
+ vi.useFakeTimers();
407
+ });
408
+
409
+ const TestComponent = ({subKey, sharedSub}: { subKey: string, sharedSub?: any }) => {
410
+ const {state, trigger, unsubscribe} = sharedSub ? useSharedSubscription(sharedSub) : useSharedSubscription(subKey, mockSubscriber);
411
+ return (
412
+ <div>
413
+ {state.isLoading && <span>Loading...</span>}
414
+ {state.error && <span>{String(state.error)}</span>}
415
+ {state.data && <span data-testid="data">{String(state.data)}</span>}
416
+ <span>Subscribed: {String(state.subscribed)}</span>
417
+ <button onClick={() => trigger()}>subscribe</button>
418
+ <button onClick={() => unsubscribe()}>unsubscribe</button>
419
+ </div>
420
+ );
421
+ };
422
+
423
+ it('should handle subscription lifecycle', async () => {
424
+ render(<TestComponent subKey="test-sub"/>);
425
+
426
+ // Initial state
427
+ expect(screen.getByText('Subscribed: false')).toBeDefined();
428
+
429
+ // Trigger subscription
430
+ act(() => {
431
+ fireEvent.click(screen.getByText('subscribe'));
432
+ });
433
+ expect(screen.getByText('Loading...')).toBeDefined();
434
+
435
+ // Subscription completes
436
+ await act(async () => {
437
+ await vi.advanceTimersByTimeAsync(100);
438
+ });
439
+ expect(screen.queryByText('Loading...')).toBeNull();
440
+ expect(screen.getByTestId('data').textContent).toBe('initial data');
441
+ expect(screen.getByText('Subscribed: true')).toBeDefined();
442
+ expect(mockSubscriber).toHaveBeenCalledTimes(1);
443
+ });
444
+
445
+ it('should unsubscribe', async () => {
446
+ render(<TestComponent subKey="test-sub"/>);
447
+ act(() => {
448
+ fireEvent.click(screen.getByText('subscribe'));
449
+ });
450
+ await act(async () => {
451
+ await vi.advanceTimersByTimeAsync(100);
452
+ });
453
+
454
+ act(() => {
455
+ fireEvent.click(screen.getByText('unsubscribe'));
456
+ });
457
+ expect(mockUnsubscribe).toHaveBeenCalledTimes(1);
458
+ expect(screen.getByText('Subscribed: false')).toBeDefined();
459
+ });
460
+
461
+ it('should automatically unsubscribe on unmount', async () => {
462
+ const {unmount} = render(<TestComponent subKey="test-sub"/>);
463
+ act(() => {
464
+ fireEvent.click(screen.getByText('subscribe'));
465
+ });
466
+ await act(async () => {
467
+ await vi.advanceTimersByTimeAsync(100);
468
+ });
469
+
470
+ unmount();
471
+ expect(mockUnsubscribe).toHaveBeenCalledTimes(1);
472
+ });
473
+
474
+ it('should work with createSharedSubscription', async () => {
475
+ const sharedSubscription = createSharedSubscription(mockSubscriber);
476
+ render(<TestComponent subKey="unused" sharedSub={sharedSubscription}/>);
477
+
478
+ act(() => {
479
+ fireEvent.click(screen.getByText('subscribe'));
480
+ });
481
+ await act(async () => {
482
+ await vi.advanceTimersByTimeAsync(100);
483
+ });
484
+ expect(mockSubscriber).toHaveBeenCalledTimes(1);
485
+ expect(screen.getByTestId('data').textContent).toBe('initial data');
486
+ });
487
+
488
+ it('should allow direct api manipulation with createSharedSubscription objects', () => {
489
+ const mockSubscriber = vi.fn();
490
+ const sharedSubscription = createSharedSubscription(mockSubscriber);
491
+
492
+ // Get initial state
493
+ const initialState = sharedSubscriptionsApi.get(sharedSubscription);
494
+ expect(initialState.data).toBeUndefined();
495
+ expect(initialState.isLoading).toBe(false);
496
+ expect(initialState.error).toBeUndefined();
497
+
498
+ // Set a new state
499
+ act(() => {
500
+ sharedSubscriptionsApi.set(sharedSubscription, {
501
+ fnState: {
502
+ data: 'test data',
503
+ isLoading: true,
504
+ error: 'test error',
505
+ }
506
+ });
507
+ });
508
+
509
+ // Get updated state
510
+ const updatedState = sharedSubscriptionsApi.get(sharedSubscription);
511
+ expect(updatedState.data).toBe('test data');
512
+ expect(updatedState.isLoading).toBe(true);
513
+ expect(updatedState.error).toBe('test error');
514
+
515
+ // Clear the value
516
+ act(() => {
517
+ sharedSubscriptionsApi.clear(sharedSubscription);
518
+ });
519
+
520
+ // Get value after clear (should be initial value)
521
+ const clearedState = sharedSubscriptionsApi.get(sharedSubscription);
522
+ expect(clearedState.data).toBeUndefined();
523
+ expect(clearedState.isLoading).toBe(false);
524
+ expect(clearedState.error).toBeUndefined();
525
+ });
526
+ });
@@ -0,0 +1,8 @@
1
+ import { defineConfig, mergeConfig } from 'vitest/config'
2
+ import viteConfig from './vite.config'
3
+
4
+ export default mergeConfig(viteConfig, defineConfig({
5
+ test: {
6
+ environment: "jsdom",
7
+ },
8
+ }))
@@ -1,61 +0,0 @@
1
- import { AFunction, DataMapValue, Prefix } from './types';
2
- type SharedDataType<T> = DataMapValue & T;
3
- export declare abstract class SharedData<T> {
4
- data: Map<string, SharedDataType<T>>;
5
- defaultValue(): T;
6
- addListener(key: string, prefix: Prefix, listener: AFunction): void;
7
- removeListener(key: string, prefix: Prefix, listener: AFunction): void;
8
- callListeners(key: string, prefix: Prefix): void;
9
- init(key: string, prefix: Prefix, data: T): void;
10
- clearAll(withoutListeners?: boolean): void;
11
- clear(key: string, prefix: Prefix, withoutListeners?: boolean): void;
12
- get(key: string, prefix: Prefix): SharedDataType<T> | undefined;
13
- setValue(key: string, prefix: Prefix, data: T): void;
14
- has(key: string, prefix: Prefix): string | undefined;
15
- static prefix(key: string, prefix: Prefix): string;
16
- static extractPrefix(mapKey: string): string[];
17
- useEffect(key: string, prefix: Prefix, unsub?: (() => void) | null): void;
18
- }
19
- export declare class SharedApi<T> {
20
- private sharedData;
21
- constructor(sharedData: SharedData<T>);
22
- /**
23
- * get a value from the shared data
24
- * @param key
25
- * @param scopeName
26
- */
27
- get<S extends string = string>(key: S, scopeName: Prefix): T;
28
- /**
29
- * set a value in the shared data
30
- * @param key
31
- * @param value
32
- * @param scopeName
33
- */
34
- set<S extends string = string>(key: S, value: T, scopeName: Prefix): void;
35
- /**
36
- * clear all values from the shared data
37
- */
38
- clearAll(): void;
39
- /**
40
- * clear all values from the shared data in a scope
41
- * @param scopeName
42
- */
43
- clearScope(scopeName?: Prefix): void;
44
- /**
45
- * clear a value from the shared data
46
- * @param key
47
- * @param scopeName
48
- */
49
- clear(key: string, scopeName: Prefix): void;
50
- /**
51
- * check if a value exists in the shared data
52
- * @param key
53
- * @param scopeName
54
- */
55
- has(key: string, scopeName?: Prefix): boolean;
56
- /**
57
- * get all values from the shared data
58
- */
59
- getAll(): Record<string, Record<string, any>>;
60
- }
61
- export {};