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.
- package/.editorconfig +3 -0
- package/README.md +519 -70
- package/dist/SharedValuesManager.d.ts +73 -0
- package/dist/context/SharedStatesContext.d.ts +5 -4
- package/dist/hooks/index.d.ts +6 -3
- package/dist/hooks/use-shared-function.d.ts +44 -17
- package/dist/hooks/use-shared-state.d.ts +24 -4
- package/dist/hooks/use-shared-subscription.d.ts +51 -22
- package/dist/lib/utils.d.ts +2 -2
- package/dist/main.esm.js +411 -355
- package/dist/main.min.js +5 -5
- package/dist/types.d.ts +6 -2
- package/package.json +6 -2
- package/tests/index.test.tsx +526 -0
- package/vitest.config.ts +8 -0
- package/dist/SharedData.d.ts +0 -61
|
@@ -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
|
+
});
|
package/vitest.config.ts
ADDED
package/dist/SharedData.d.ts
DELETED
|
@@ -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 {};
|