react-broadcast-sync 1.6.0 → 1.8.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/README.md +59 -11
- package/dist/index.cjs.js +1 -1
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.esm.js +1 -1
- package/dist/index.esm.js.map +1 -1
- package/dist/types/types.d.ts +17 -0
- package/package.json +6 -1
- package/.eslintignore +0 -5
- package/.github/workflows/ci.yml +0 -61
- package/.github/workflows/release.yml +0 -39
- package/.husky/pre-commit +0 -5
- package/.nvmrc +0 -1
- package/.prettierignore +0 -4
- package/.prettierrc +0 -9
- package/.releaserc.json +0 -22
- package/CHANGELOG.md +0 -97
- package/assets/react-broadcast-sync-logo.png +0 -0
- package/commitlint.config.js +0 -3
- package/demo/react-broadcast-sync-demo/README.md +0 -130
- package/demo/react-broadcast-sync-demo/index.html +0 -14
- package/demo/react-broadcast-sync-demo/package-lock.json +0 -6379
- package/demo/react-broadcast-sync-demo/package.json +0 -42
- package/demo/react-broadcast-sync-demo/public/assets/react-broadcast-sync-demo-video.gif +0 -0
- package/demo/react-broadcast-sync-demo/public/assets/react-broadcast-sync-logo.png +0 -0
- package/demo/react-broadcast-sync-demo/src/App.tsx +0 -277
- package/demo/react-broadcast-sync-demo/src/components/Counter.tsx +0 -63
- package/demo/react-broadcast-sync-demo/src/components/TextSync.tsx +0 -61
- package/demo/react-broadcast-sync-demo/src/components/TodoList.tsx +0 -186
- package/demo/react-broadcast-sync-demo/src/hooks/useCounterBroadcast.ts +0 -96
- package/demo/react-broadcast-sync-demo/src/hooks/useTextBroadcast.ts +0 -37
- package/demo/react-broadcast-sync-demo/src/hooks/useTodoBroadcast.ts +0 -249
- package/demo/react-broadcast-sync-demo/src/main.tsx +0 -11
- package/demo/react-broadcast-sync-demo/src/theme.ts +0 -62
- package/demo/react-broadcast-sync-demo/tsconfig.json +0 -26
- package/demo/react-broadcast-sync-demo/tsconfig.node.json +0 -10
- package/demo/react-broadcast-sync-demo/vite.config.ts +0 -12
- package/eslint.config.js +0 -38
- package/jest.config.cjs +0 -9
- package/jest.setup.ts +0 -1
- package/rollup.config.mjs +0 -98
- package/src/BroadcastProvider.tsx +0 -28
- package/src/hooks/useBroadcastChannel.ts +0 -513
- package/src/index.ts +0 -9
- package/src/tests/BroadcastProvider.test.tsx +0 -157
- package/src/tests/debug.test.ts +0 -273
- package/src/tests/messageUtils.test.ts +0 -252
- package/src/tests/useBroadcastChannel.same-tab.test.tsx +0 -992
- package/src/tests/useBroadcastChannel.test.tsx +0 -1243
- package/src/types/types.ts +0 -124
- package/src/utils/debug.ts +0 -80
- package/src/utils/messageUtils.ts +0 -122
- package/tsconfig.json +0 -36
|
@@ -1,992 +0,0 @@
|
|
|
1
|
-
import { renderHook, act } from '@testing-library/react';
|
|
2
|
-
import { useBroadcastChannel } from '../hooks/useBroadcastChannel';
|
|
3
|
-
|
|
4
|
-
let mockChannels: any[] = [];
|
|
5
|
-
|
|
6
|
-
class MockBroadcastChannel {
|
|
7
|
-
name: string;
|
|
8
|
-
onmessage: ((event: MessageEvent) => void) | null = null;
|
|
9
|
-
postMessage = jest.fn((data: any) => {
|
|
10
|
-
// Simulate broadcasting to all channels with the same name
|
|
11
|
-
const targetChannels = mockChannels.filter(
|
|
12
|
-
channel => channel.name === this.name && channel !== this
|
|
13
|
-
);
|
|
14
|
-
|
|
15
|
-
targetChannels.forEach(channel => {
|
|
16
|
-
if (channel.onmessage) {
|
|
17
|
-
// Use setTimeout to simulate async behavior
|
|
18
|
-
setTimeout(() => {
|
|
19
|
-
// Support both single and batched (array) messages
|
|
20
|
-
if (Array.isArray(data)) {
|
|
21
|
-
data.forEach(msg => channel.onmessage!({ data: msg } as MessageEvent));
|
|
22
|
-
} else {
|
|
23
|
-
channel.onmessage!({ data } as MessageEvent);
|
|
24
|
-
}
|
|
25
|
-
}, 0);
|
|
26
|
-
}
|
|
27
|
-
});
|
|
28
|
-
});
|
|
29
|
-
close = jest.fn();
|
|
30
|
-
addEventListener = jest.fn((event: string, callback: (event: MessageEvent) => void) => {
|
|
31
|
-
if (event === 'message') {
|
|
32
|
-
this.onmessage = callback;
|
|
33
|
-
}
|
|
34
|
-
});
|
|
35
|
-
removeEventListener = jest.fn();
|
|
36
|
-
|
|
37
|
-
constructor(name: string) {
|
|
38
|
-
this.name = name;
|
|
39
|
-
mockChannels.push(this);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
simulateMessage(data: any) {
|
|
43
|
-
if (this.onmessage) {
|
|
44
|
-
setTimeout(() => {
|
|
45
|
-
// Support both single and batched (array) messages
|
|
46
|
-
if (Array.isArray(data)) {
|
|
47
|
-
data.forEach(msg => this.onmessage!({ data: msg } as MessageEvent));
|
|
48
|
-
} else {
|
|
49
|
-
this.onmessage!({ data } as MessageEvent);
|
|
50
|
-
}
|
|
51
|
-
}, 0);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const waitForAsync = async () => {
|
|
57
|
-
// Wait for all async operations to complete
|
|
58
|
-
await act(async () => {
|
|
59
|
-
await new Promise(resolve => setTimeout(resolve, 10));
|
|
60
|
-
});
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
beforeAll(() => {
|
|
64
|
-
global.BroadcastChannel = MockBroadcastChannel as any;
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
beforeEach(() => {
|
|
68
|
-
mockChannels = [];
|
|
69
|
-
jest.clearAllMocks();
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
afterEach(() => {
|
|
73
|
-
jest.clearAllMocks();
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
afterAll(() => {
|
|
77
|
-
delete (global as any).BroadcastChannel;
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
describe('useBroadcastChannel - Same Tab Multi-Hook Behavior', () => {
|
|
81
|
-
const CHANNEL_NAME = 'test-channel';
|
|
82
|
-
|
|
83
|
-
// Common options to disable cleanup intervals in tests
|
|
84
|
-
const testOptions = {
|
|
85
|
-
cleaningInterval: 0, // Disable auto cleanup to avoid act warnings
|
|
86
|
-
deduplicationTTL: 60000, // Long TTL for tests
|
|
87
|
-
batchingDelayMs: 0, // Disable batching for immediate delivery in tests
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
describe('1. Two Hooks with Different Source Names', () => {
|
|
91
|
-
it('should allow hooks with different sources to receive messages from each other', async () => {
|
|
92
|
-
const { result: hook1 } = renderHook(() =>
|
|
93
|
-
useBroadcastChannel(CHANNEL_NAME, {
|
|
94
|
-
...testOptions,
|
|
95
|
-
sourceName: 'source-1',
|
|
96
|
-
})
|
|
97
|
-
);
|
|
98
|
-
|
|
99
|
-
const { result: hook2 } = renderHook(() =>
|
|
100
|
-
useBroadcastChannel(CHANNEL_NAME, {
|
|
101
|
-
...testOptions,
|
|
102
|
-
sourceName: 'source-2',
|
|
103
|
-
})
|
|
104
|
-
);
|
|
105
|
-
|
|
106
|
-
await waitForAsync();
|
|
107
|
-
|
|
108
|
-
// Hook 1 sends a message
|
|
109
|
-
act(() => {
|
|
110
|
-
hook1.current.postMessage('greeting', { from: 'hook1' });
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
await waitForAsync();
|
|
114
|
-
|
|
115
|
-
// Hook 2 should receive the message
|
|
116
|
-
expect(hook2.current.messages).toHaveLength(1);
|
|
117
|
-
expect(hook2.current.messages[0].message).toEqual({ from: 'hook1' });
|
|
118
|
-
expect(hook2.current.messages[0].source).toBe('source-1');
|
|
119
|
-
|
|
120
|
-
// Hook 2 sends a message
|
|
121
|
-
act(() => {
|
|
122
|
-
hook2.current.postMessage('response', { from: 'hook2' });
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
await waitForAsync();
|
|
126
|
-
|
|
127
|
-
// Hook 1 should receive the message
|
|
128
|
-
expect(hook1.current.messages).toHaveLength(1);
|
|
129
|
-
expect(hook1.current.messages[0].message).toEqual({ from: 'hook2' });
|
|
130
|
-
expect(hook1.current.messages[0].source).toBe('source-2');
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
it('should not deduplicate messages from different sources', async () => {
|
|
134
|
-
const { result: hook1 } = renderHook(() =>
|
|
135
|
-
useBroadcastChannel(CHANNEL_NAME, {
|
|
136
|
-
...testOptions,
|
|
137
|
-
sourceName: 'source-1',
|
|
138
|
-
})
|
|
139
|
-
);
|
|
140
|
-
|
|
141
|
-
const { result: hook2 } = renderHook(() =>
|
|
142
|
-
useBroadcastChannel(CHANNEL_NAME, {
|
|
143
|
-
...testOptions,
|
|
144
|
-
sourceName: 'source-2',
|
|
145
|
-
})
|
|
146
|
-
);
|
|
147
|
-
|
|
148
|
-
await waitForAsync();
|
|
149
|
-
|
|
150
|
-
// Both hooks send identical message content
|
|
151
|
-
act(() => {
|
|
152
|
-
hook1.current.postMessage('test', { data: 'same' });
|
|
153
|
-
hook2.current.postMessage('test', { data: 'same' });
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
await waitForAsync();
|
|
157
|
-
|
|
158
|
-
// Both hooks should receive messages from the other source
|
|
159
|
-
expect(hook1.current.messages).toHaveLength(1);
|
|
160
|
-
expect(hook2.current.messages).toHaveLength(1);
|
|
161
|
-
expect(hook1.current.messages[0].source).toBe('source-2');
|
|
162
|
-
expect(hook2.current.messages[0].source).toBe('source-1');
|
|
163
|
-
});
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
describe('2. Two Hooks with Same Source Name', () => {
|
|
167
|
-
it('should filter self-originated messages correctly with same source', async () => {
|
|
168
|
-
const { result: hook1 } = renderHook(() =>
|
|
169
|
-
useBroadcastChannel(CHANNEL_NAME, {
|
|
170
|
-
...testOptions,
|
|
171
|
-
sourceName: 'same-source',
|
|
172
|
-
})
|
|
173
|
-
);
|
|
174
|
-
|
|
175
|
-
const { result: hook2 } = renderHook(() =>
|
|
176
|
-
useBroadcastChannel(CHANNEL_NAME, {
|
|
177
|
-
...testOptions,
|
|
178
|
-
sourceName: 'same-source',
|
|
179
|
-
})
|
|
180
|
-
);
|
|
181
|
-
|
|
182
|
-
await waitForAsync();
|
|
183
|
-
|
|
184
|
-
// Hook 1 sends a message
|
|
185
|
-
act(() => {
|
|
186
|
-
hook1.current.postMessage('test', { from: 'hook1' });
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
await waitForAsync();
|
|
190
|
-
|
|
191
|
-
// Neither hook should receive the message (filtered as self-originated)
|
|
192
|
-
expect(hook1.current.messages).toHaveLength(0);
|
|
193
|
-
expect(hook2.current.messages).toHaveLength(0);
|
|
194
|
-
|
|
195
|
-
// Hook 1 should have the message in sentMessages
|
|
196
|
-
expect(hook1.current.sentMessages).toHaveLength(1);
|
|
197
|
-
expect(hook2.current.sentMessages).toHaveLength(0);
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
it('should handle cleanup properly on unmount with same source', async () => {
|
|
201
|
-
const { result: hook1, unmount: unmount1 } = renderHook(() =>
|
|
202
|
-
useBroadcastChannel(CHANNEL_NAME, {
|
|
203
|
-
...testOptions,
|
|
204
|
-
sourceName: 'same-source',
|
|
205
|
-
})
|
|
206
|
-
);
|
|
207
|
-
|
|
208
|
-
const { result: hook2 } = renderHook(() =>
|
|
209
|
-
useBroadcastChannel(CHANNEL_NAME, {
|
|
210
|
-
...testOptions,
|
|
211
|
-
sourceName: 'same-source',
|
|
212
|
-
})
|
|
213
|
-
);
|
|
214
|
-
|
|
215
|
-
await waitForAsync();
|
|
216
|
-
|
|
217
|
-
// Verify both hooks are working (channel name includes namespace suffix)
|
|
218
|
-
expect(hook1.current.channelName).toBe(`${CHANNEL_NAME}`);
|
|
219
|
-
expect(hook2.current.channelName).toBe(`${CHANNEL_NAME}`);
|
|
220
|
-
|
|
221
|
-
// Unmount hook1
|
|
222
|
-
unmount1();
|
|
223
|
-
|
|
224
|
-
// Hook2 should still work normally
|
|
225
|
-
act(() => {
|
|
226
|
-
hook2.current.postMessage('after-unmount', { test: true });
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
expect(hook2.current.sentMessages).toHaveLength(1);
|
|
230
|
-
expect(hook2.current.error).toBeNull();
|
|
231
|
-
});
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
describe('3. Deduplication in Same Tab', () => {
|
|
235
|
-
it('should deduplicate identical messages from same source', async () => {
|
|
236
|
-
const { result: hook1 } = renderHook(() =>
|
|
237
|
-
useBroadcastChannel(CHANNEL_NAME, {
|
|
238
|
-
...testOptions,
|
|
239
|
-
sourceName: 'test-source',
|
|
240
|
-
})
|
|
241
|
-
);
|
|
242
|
-
|
|
243
|
-
const { result: hook2 } = renderHook(() =>
|
|
244
|
-
useBroadcastChannel(CHANNEL_NAME, {
|
|
245
|
-
...testOptions,
|
|
246
|
-
sourceName: 'test-source',
|
|
247
|
-
})
|
|
248
|
-
);
|
|
249
|
-
|
|
250
|
-
await waitForAsync();
|
|
251
|
-
|
|
252
|
-
// Simulate external message with same ID arriving multiple times
|
|
253
|
-
const duplicateMessage = {
|
|
254
|
-
id: 'duplicate-id',
|
|
255
|
-
type: 'test',
|
|
256
|
-
message: { data: 'test' },
|
|
257
|
-
source: 'external-source',
|
|
258
|
-
timestamp: Date.now(),
|
|
259
|
-
};
|
|
260
|
-
|
|
261
|
-
act(() => {
|
|
262
|
-
// Simulate the same message to both channels
|
|
263
|
-
mockChannels.forEach(channel => {
|
|
264
|
-
channel.simulateMessage(duplicateMessage);
|
|
265
|
-
channel.simulateMessage(duplicateMessage); // Send duplicate
|
|
266
|
-
});
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
await waitForAsync();
|
|
270
|
-
|
|
271
|
-
// Should only receive the message once due to deduplication
|
|
272
|
-
expect(hook1.current.messages).toHaveLength(1);
|
|
273
|
-
expect(hook2.current.messages).toHaveLength(1);
|
|
274
|
-
});
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
describe('4. Cleanup on Unmount', () => {
|
|
278
|
-
it('should not interfere with other hooks when one unmounts', async () => {
|
|
279
|
-
const { result: hook1, unmount: unmount1 } = renderHook(() =>
|
|
280
|
-
useBroadcastChannel(CHANNEL_NAME, {
|
|
281
|
-
...testOptions,
|
|
282
|
-
sourceName: 'source-1',
|
|
283
|
-
})
|
|
284
|
-
);
|
|
285
|
-
|
|
286
|
-
const { result: hook2 } = renderHook(() =>
|
|
287
|
-
useBroadcastChannel(CHANNEL_NAME, {
|
|
288
|
-
...testOptions,
|
|
289
|
-
sourceName: 'source-2',
|
|
290
|
-
})
|
|
291
|
-
);
|
|
292
|
-
|
|
293
|
-
await waitForAsync();
|
|
294
|
-
|
|
295
|
-
// Verify both hooks work initially
|
|
296
|
-
act(() => {
|
|
297
|
-
hook1.current.postMessage('test', { from: 'hook1' });
|
|
298
|
-
});
|
|
299
|
-
|
|
300
|
-
await waitForAsync();
|
|
301
|
-
expect(hook2.current.messages).toHaveLength(1);
|
|
302
|
-
|
|
303
|
-
// Unmount hook1
|
|
304
|
-
unmount1();
|
|
305
|
-
|
|
306
|
-
// Hook2 should continue working normally
|
|
307
|
-
act(() => {
|
|
308
|
-
hook2.current.postMessage('after-unmount', { from: 'hook2' });
|
|
309
|
-
});
|
|
310
|
-
|
|
311
|
-
expect(hook2.current.sentMessages).toHaveLength(1);
|
|
312
|
-
});
|
|
313
|
-
});
|
|
314
|
-
|
|
315
|
-
describe('5. Different keepLatestMessage Values', () => {
|
|
316
|
-
it('should handle different keepLatestMessage settings correctly', async () => {
|
|
317
|
-
const { result: hookWithHistory } = renderHook(() =>
|
|
318
|
-
useBroadcastChannel(CHANNEL_NAME, {
|
|
319
|
-
...testOptions,
|
|
320
|
-
sourceName: 'source-history',
|
|
321
|
-
keepLatestMessage: false,
|
|
322
|
-
})
|
|
323
|
-
);
|
|
324
|
-
|
|
325
|
-
const { result: hookLatestOnly } = renderHook(() =>
|
|
326
|
-
useBroadcastChannel(CHANNEL_NAME, {
|
|
327
|
-
...testOptions,
|
|
328
|
-
sourceName: 'source-latest',
|
|
329
|
-
keepLatestMessage: true,
|
|
330
|
-
})
|
|
331
|
-
);
|
|
332
|
-
|
|
333
|
-
await waitForAsync();
|
|
334
|
-
|
|
335
|
-
// Send multiple messages from external source
|
|
336
|
-
const externalMessages = [
|
|
337
|
-
{
|
|
338
|
-
id: 'msg-1',
|
|
339
|
-
type: 'test',
|
|
340
|
-
message: { count: 1 },
|
|
341
|
-
source: 'external',
|
|
342
|
-
timestamp: Date.now(),
|
|
343
|
-
},
|
|
344
|
-
{
|
|
345
|
-
id: 'msg-2',
|
|
346
|
-
type: 'test',
|
|
347
|
-
message: { count: 2 },
|
|
348
|
-
source: 'external',
|
|
349
|
-
timestamp: Date.now() + 1,
|
|
350
|
-
},
|
|
351
|
-
{
|
|
352
|
-
id: 'msg-3',
|
|
353
|
-
type: 'test',
|
|
354
|
-
message: { count: 3 },
|
|
355
|
-
source: 'external',
|
|
356
|
-
timestamp: Date.now() + 2,
|
|
357
|
-
},
|
|
358
|
-
];
|
|
359
|
-
|
|
360
|
-
for (const msg of externalMessages) {
|
|
361
|
-
act(() => {
|
|
362
|
-
mockChannels.forEach(channel => {
|
|
363
|
-
channel.simulateMessage(msg);
|
|
364
|
-
});
|
|
365
|
-
});
|
|
366
|
-
await waitForAsync();
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
// Hook with history should have all messages
|
|
370
|
-
expect(hookWithHistory.current.messages).toHaveLength(3);
|
|
371
|
-
|
|
372
|
-
// Hook with keepLatestMessage should only have the last message
|
|
373
|
-
expect(hookLatestOnly.current.messages).toHaveLength(1);
|
|
374
|
-
expect(hookLatestOnly.current.messages[0].message.count).toBe(3);
|
|
375
|
-
});
|
|
376
|
-
});
|
|
377
|
-
|
|
378
|
-
describe('6. Different registeredTypes with Same Source', () => {
|
|
379
|
-
it('should filter messages based on registeredTypes independently', async () => {
|
|
380
|
-
const { result: hookTypeA } = renderHook(() =>
|
|
381
|
-
useBroadcastChannel(CHANNEL_NAME, {
|
|
382
|
-
...testOptions,
|
|
383
|
-
sourceName: 'test-source',
|
|
384
|
-
registeredTypes: ['typeA'],
|
|
385
|
-
})
|
|
386
|
-
);
|
|
387
|
-
|
|
388
|
-
const { result: hookTypeB } = renderHook(() =>
|
|
389
|
-
useBroadcastChannel(CHANNEL_NAME, {
|
|
390
|
-
...testOptions,
|
|
391
|
-
sourceName: 'test-source',
|
|
392
|
-
registeredTypes: ['typeB'],
|
|
393
|
-
})
|
|
394
|
-
);
|
|
395
|
-
|
|
396
|
-
await waitForAsync();
|
|
397
|
-
|
|
398
|
-
// Send messages of different types
|
|
399
|
-
const messages = [
|
|
400
|
-
{
|
|
401
|
-
id: 'msg-a',
|
|
402
|
-
type: 'typeA',
|
|
403
|
-
message: { data: 'for-a' },
|
|
404
|
-
source: 'external',
|
|
405
|
-
timestamp: Date.now(),
|
|
406
|
-
},
|
|
407
|
-
{
|
|
408
|
-
id: 'msg-b',
|
|
409
|
-
type: 'typeB',
|
|
410
|
-
message: { data: 'for-b' },
|
|
411
|
-
source: 'external',
|
|
412
|
-
timestamp: Date.now(),
|
|
413
|
-
},
|
|
414
|
-
{
|
|
415
|
-
id: 'msg-c',
|
|
416
|
-
type: 'typeC',
|
|
417
|
-
message: { data: 'for-neither' },
|
|
418
|
-
source: 'external',
|
|
419
|
-
timestamp: Date.now(),
|
|
420
|
-
},
|
|
421
|
-
];
|
|
422
|
-
|
|
423
|
-
for (const msg of messages) {
|
|
424
|
-
act(() => {
|
|
425
|
-
mockChannels.forEach(channel => {
|
|
426
|
-
channel.simulateMessage(msg);
|
|
427
|
-
});
|
|
428
|
-
});
|
|
429
|
-
await waitForAsync();
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
// Each hook should only receive messages of its registered type
|
|
433
|
-
expect(hookTypeA.current.messages).toHaveLength(1);
|
|
434
|
-
expect(hookTypeA.current.messages[0].type).toBe('typeA');
|
|
435
|
-
|
|
436
|
-
expect(hookTypeB.current.messages).toHaveLength(1);
|
|
437
|
-
expect(hookTypeB.current.messages[0].type).toBe('typeB');
|
|
438
|
-
});
|
|
439
|
-
});
|
|
440
|
-
|
|
441
|
-
describe('7. Same Source, Same Type, Multiple Listeners', () => {
|
|
442
|
-
it('should handle multiple hooks with same source and overlapping types', async () => {
|
|
443
|
-
const { result: hook1 } = renderHook(() =>
|
|
444
|
-
useBroadcastChannel(CHANNEL_NAME, {
|
|
445
|
-
...testOptions,
|
|
446
|
-
sourceName: 'same-source',
|
|
447
|
-
registeredTypes: ['shared-type'],
|
|
448
|
-
})
|
|
449
|
-
);
|
|
450
|
-
|
|
451
|
-
const { result: hook2 } = renderHook(() =>
|
|
452
|
-
useBroadcastChannel(CHANNEL_NAME, {
|
|
453
|
-
...testOptions,
|
|
454
|
-
sourceName: 'same-source',
|
|
455
|
-
registeredTypes: ['shared-type'],
|
|
456
|
-
})
|
|
457
|
-
);
|
|
458
|
-
|
|
459
|
-
await waitForAsync();
|
|
460
|
-
|
|
461
|
-
// Send a message from external source
|
|
462
|
-
const message = {
|
|
463
|
-
id: 'shared-msg',
|
|
464
|
-
type: 'shared-type',
|
|
465
|
-
message: { data: 'shared' },
|
|
466
|
-
source: 'external',
|
|
467
|
-
timestamp: Date.now(),
|
|
468
|
-
};
|
|
469
|
-
|
|
470
|
-
act(() => {
|
|
471
|
-
mockChannels.forEach(channel => {
|
|
472
|
-
channel.simulateMessage(message);
|
|
473
|
-
});
|
|
474
|
-
});
|
|
475
|
-
|
|
476
|
-
await waitForAsync();
|
|
477
|
-
|
|
478
|
-
// Both hooks should receive the message
|
|
479
|
-
expect(hook1.current.messages).toHaveLength(1);
|
|
480
|
-
expect(hook2.current.messages).toHaveLength(1);
|
|
481
|
-
expect(hook1.current.messages[0].message).toEqual({ data: 'shared' });
|
|
482
|
-
expect(hook2.current.messages[0].message).toEqual({ data: 'shared' });
|
|
483
|
-
});
|
|
484
|
-
});
|
|
485
|
-
|
|
486
|
-
describe('8. Same Source, One with registeredTypes, One Without', () => {
|
|
487
|
-
it('should handle mixed type registration correctly', async () => {
|
|
488
|
-
const { result: hookSpecific } = renderHook(() =>
|
|
489
|
-
useBroadcastChannel(CHANNEL_NAME, {
|
|
490
|
-
...testOptions,
|
|
491
|
-
sourceName: 'test-source',
|
|
492
|
-
registeredTypes: ['specific-type'],
|
|
493
|
-
})
|
|
494
|
-
);
|
|
495
|
-
|
|
496
|
-
const { result: hookAll } = renderHook(() =>
|
|
497
|
-
useBroadcastChannel(CHANNEL_NAME, {
|
|
498
|
-
...testOptions,
|
|
499
|
-
sourceName: 'test-source',
|
|
500
|
-
registeredTypes: [], // Empty array means listen to all
|
|
501
|
-
})
|
|
502
|
-
);
|
|
503
|
-
|
|
504
|
-
await waitForAsync();
|
|
505
|
-
|
|
506
|
-
// Send messages of different types
|
|
507
|
-
const messages = [
|
|
508
|
-
{
|
|
509
|
-
id: 'msg-specific',
|
|
510
|
-
type: 'specific-type',
|
|
511
|
-
message: { data: 'specific' },
|
|
512
|
-
source: 'external',
|
|
513
|
-
timestamp: Date.now(),
|
|
514
|
-
},
|
|
515
|
-
{
|
|
516
|
-
id: 'msg-other',
|
|
517
|
-
type: 'other-type',
|
|
518
|
-
message: { data: 'other' },
|
|
519
|
-
source: 'external',
|
|
520
|
-
timestamp: Date.now(),
|
|
521
|
-
},
|
|
522
|
-
];
|
|
523
|
-
|
|
524
|
-
for (const msg of messages) {
|
|
525
|
-
act(() => {
|
|
526
|
-
mockChannels.forEach(channel => {
|
|
527
|
-
channel.simulateMessage(msg);
|
|
528
|
-
});
|
|
529
|
-
});
|
|
530
|
-
await waitForAsync();
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
// Specific hook should only receive specific-type
|
|
534
|
-
expect(hookSpecific.current.messages).toHaveLength(1);
|
|
535
|
-
expect(hookSpecific.current.messages[0].type).toBe('specific-type');
|
|
536
|
-
|
|
537
|
-
// Hook listening to all should receive both
|
|
538
|
-
expect(hookAll.current.messages).toHaveLength(2);
|
|
539
|
-
});
|
|
540
|
-
});
|
|
541
|
-
|
|
542
|
-
describe('9. Rapid Message Burst Between Hooks', () => {
|
|
543
|
-
it('should handle rapid message bursts correctly', async () => {
|
|
544
|
-
const { result: sender } = renderHook(() =>
|
|
545
|
-
useBroadcastChannel(CHANNEL_NAME, {
|
|
546
|
-
...testOptions,
|
|
547
|
-
sourceName: 'sender',
|
|
548
|
-
})
|
|
549
|
-
);
|
|
550
|
-
|
|
551
|
-
const { result: receiver } = renderHook(() =>
|
|
552
|
-
useBroadcastChannel(CHANNEL_NAME, {
|
|
553
|
-
...testOptions,
|
|
554
|
-
sourceName: 'receiver',
|
|
555
|
-
})
|
|
556
|
-
);
|
|
557
|
-
|
|
558
|
-
await waitForAsync();
|
|
559
|
-
|
|
560
|
-
// Send rapid burst of messages
|
|
561
|
-
act(() => {
|
|
562
|
-
for (let i = 0; i < 10; i++) {
|
|
563
|
-
sender.current.postMessage('burst', { count: i });
|
|
564
|
-
}
|
|
565
|
-
});
|
|
566
|
-
|
|
567
|
-
await waitForAsync();
|
|
568
|
-
|
|
569
|
-
// Receiver should get all messages
|
|
570
|
-
expect(receiver.current.messages).toHaveLength(10);
|
|
571
|
-
expect(sender.current.sentMessages).toHaveLength(10);
|
|
572
|
-
});
|
|
573
|
-
|
|
574
|
-
it('should respect deduplication during rapid bursts', async () => {
|
|
575
|
-
const { result: hook } = renderHook(() =>
|
|
576
|
-
useBroadcastChannel(CHANNEL_NAME, {
|
|
577
|
-
...testOptions,
|
|
578
|
-
sourceName: 'test-source',
|
|
579
|
-
})
|
|
580
|
-
);
|
|
581
|
-
|
|
582
|
-
await waitForAsync();
|
|
583
|
-
|
|
584
|
-
// Simulate rapid duplicate messages
|
|
585
|
-
const duplicateMessage = {
|
|
586
|
-
id: 'rapid-duplicate',
|
|
587
|
-
type: 'test',
|
|
588
|
-
message: { data: 'duplicate' },
|
|
589
|
-
source: 'external',
|
|
590
|
-
timestamp: Date.now(),
|
|
591
|
-
};
|
|
592
|
-
|
|
593
|
-
act(() => {
|
|
594
|
-
for (let i = 0; i < 5; i++) {
|
|
595
|
-
mockChannels.forEach(channel => {
|
|
596
|
-
channel.simulateMessage(duplicateMessage);
|
|
597
|
-
});
|
|
598
|
-
}
|
|
599
|
-
});
|
|
600
|
-
|
|
601
|
-
await waitForAsync();
|
|
602
|
-
|
|
603
|
-
// Should only receive one message due to deduplication
|
|
604
|
-
expect(hook.current.messages).toHaveLength(1);
|
|
605
|
-
});
|
|
606
|
-
});
|
|
607
|
-
|
|
608
|
-
describe('10. Source Reinitialization with Same Name', () => {
|
|
609
|
-
it('should handle remounting with same source name cleanly', async () => {
|
|
610
|
-
const { result: hook1, unmount } = renderHook(() =>
|
|
611
|
-
useBroadcastChannel(CHANNEL_NAME, {
|
|
612
|
-
...testOptions,
|
|
613
|
-
sourceName: 'reusable-source',
|
|
614
|
-
keepLatestMessage: false,
|
|
615
|
-
})
|
|
616
|
-
);
|
|
617
|
-
|
|
618
|
-
await waitForAsync();
|
|
619
|
-
|
|
620
|
-
// Send initial message
|
|
621
|
-
act(() => {
|
|
622
|
-
hook1.current.postMessage('initial', { phase: 1 });
|
|
623
|
-
});
|
|
624
|
-
|
|
625
|
-
expect(hook1.current.sentMessages).toHaveLength(1);
|
|
626
|
-
|
|
627
|
-
// Unmount and remount with different config
|
|
628
|
-
unmount();
|
|
629
|
-
|
|
630
|
-
const { result: hook2 } = renderHook(() =>
|
|
631
|
-
useBroadcastChannel(CHANNEL_NAME, {
|
|
632
|
-
...testOptions,
|
|
633
|
-
sourceName: 'reusable-source',
|
|
634
|
-
keepLatestMessage: true,
|
|
635
|
-
})
|
|
636
|
-
);
|
|
637
|
-
|
|
638
|
-
await waitForAsync();
|
|
639
|
-
|
|
640
|
-
// New instance should start fresh
|
|
641
|
-
expect(hook2.current.sentMessages).toHaveLength(0);
|
|
642
|
-
expect(hook2.current.messages).toHaveLength(0);
|
|
643
|
-
|
|
644
|
-
// Should work normally
|
|
645
|
-
act(() => {
|
|
646
|
-
hook2.current.postMessage('after-remount', { phase: 2 });
|
|
647
|
-
});
|
|
648
|
-
|
|
649
|
-
expect(hook2.current.sentMessages).toHaveLength(1);
|
|
650
|
-
});
|
|
651
|
-
});
|
|
652
|
-
|
|
653
|
-
describe('11. Hook Sends Message to Itself', () => {
|
|
654
|
-
it('should correctly handle self-originated messages', async () => {
|
|
655
|
-
const { result: hook } = renderHook(() =>
|
|
656
|
-
useBroadcastChannel(CHANNEL_NAME, {
|
|
657
|
-
...testOptions,
|
|
658
|
-
sourceName: 'self-sender',
|
|
659
|
-
registeredTypes: ['self-type'],
|
|
660
|
-
})
|
|
661
|
-
);
|
|
662
|
-
|
|
663
|
-
await waitForAsync();
|
|
664
|
-
|
|
665
|
-
// Hook sends a message that matches its own registered types
|
|
666
|
-
act(() => {
|
|
667
|
-
hook.current.postMessage('self-type', { self: true });
|
|
668
|
-
});
|
|
669
|
-
|
|
670
|
-
await waitForAsync();
|
|
671
|
-
|
|
672
|
-
// Should not receive its own message
|
|
673
|
-
expect(hook.current.messages).toHaveLength(0);
|
|
674
|
-
// But should have it in sent messages
|
|
675
|
-
expect(hook.current.sentMessages).toHaveLength(1);
|
|
676
|
-
expect(hook.current.sentMessages[0].type).toBe('self-type');
|
|
677
|
-
});
|
|
678
|
-
});
|
|
679
|
-
|
|
680
|
-
describe('Additional Edge Cases', () => {
|
|
681
|
-
it('should handle clearMessagesByType coordination in same tab', async () => {
|
|
682
|
-
const { result: hook1 } = renderHook(() =>
|
|
683
|
-
useBroadcastChannel(CHANNEL_NAME, {
|
|
684
|
-
...testOptions,
|
|
685
|
-
sourceName: 'source-1',
|
|
686
|
-
})
|
|
687
|
-
);
|
|
688
|
-
|
|
689
|
-
const { result: hook2 } = renderHook(() =>
|
|
690
|
-
useBroadcastChannel(CHANNEL_NAME, {
|
|
691
|
-
...testOptions,
|
|
692
|
-
sourceName: 'source-2',
|
|
693
|
-
})
|
|
694
|
-
);
|
|
695
|
-
|
|
696
|
-
await waitForAsync();
|
|
697
|
-
|
|
698
|
-
// Send messages of different types from different sources
|
|
699
|
-
act(() => {
|
|
700
|
-
hook1.current.postMessage('typeA', { from: 'hook1' });
|
|
701
|
-
hook1.current.postMessage('typeB', { from: 'hook1' });
|
|
702
|
-
});
|
|
703
|
-
|
|
704
|
-
await waitForAsync();
|
|
705
|
-
|
|
706
|
-
// Both messages should be received by hook2
|
|
707
|
-
expect(hook2.current.messages).toHaveLength(2);
|
|
708
|
-
|
|
709
|
-
// Clear specific type from hook2
|
|
710
|
-
act(() => {
|
|
711
|
-
hook2.current.clearReceivedMessages({ types: ['typeA'] });
|
|
712
|
-
});
|
|
713
|
-
|
|
714
|
-
// Hook2 should only have typeB message left
|
|
715
|
-
expect(hook2.current.messages).toHaveLength(1);
|
|
716
|
-
expect(hook2.current.messages[0].type).toBe('typeB');
|
|
717
|
-
|
|
718
|
-
// Hook1's messages should be unaffected
|
|
719
|
-
expect(hook1.current.sentMessages).toHaveLength(2);
|
|
720
|
-
});
|
|
721
|
-
|
|
722
|
-
it('should handle namespace isolation correctly', async () => {
|
|
723
|
-
const { result: hook1 } = renderHook(() =>
|
|
724
|
-
useBroadcastChannel(CHANNEL_NAME, {
|
|
725
|
-
...testOptions,
|
|
726
|
-
sourceName: 'source-1',
|
|
727
|
-
namespace: 'ns1',
|
|
728
|
-
})
|
|
729
|
-
);
|
|
730
|
-
|
|
731
|
-
const { result: hook2 } = renderHook(() =>
|
|
732
|
-
useBroadcastChannel(CHANNEL_NAME, {
|
|
733
|
-
...testOptions,
|
|
734
|
-
sourceName: 'source-2',
|
|
735
|
-
namespace: 'ns2',
|
|
736
|
-
})
|
|
737
|
-
);
|
|
738
|
-
|
|
739
|
-
await waitForAsync();
|
|
740
|
-
|
|
741
|
-
// Hooks should be on different resolved channels
|
|
742
|
-
expect(hook1.current.channelName).toBe(`${CHANNEL_NAME}-ns1`);
|
|
743
|
-
expect(hook2.current.channelName).toBe(`${CHANNEL_NAME}-ns2`);
|
|
744
|
-
|
|
745
|
-
// Messages should not cross namespaces
|
|
746
|
-
act(() => {
|
|
747
|
-
hook1.current.postMessage('test', { from: 'hook1' });
|
|
748
|
-
});
|
|
749
|
-
|
|
750
|
-
await waitForAsync();
|
|
751
|
-
|
|
752
|
-
// Hook2 should not receive the message (different namespace)
|
|
753
|
-
expect(hook2.current.messages).toHaveLength(0);
|
|
754
|
-
expect(hook1.current.sentMessages).toHaveLength(1);
|
|
755
|
-
});
|
|
756
|
-
|
|
757
|
-
it('should handle different keepLatestMessage settings with same source name', async () => {
|
|
758
|
-
const { result: hookWithHistory } = renderHook(() =>
|
|
759
|
-
useBroadcastChannel(CHANNEL_NAME, {
|
|
760
|
-
...testOptions,
|
|
761
|
-
sourceName: 'same-source',
|
|
762
|
-
keepLatestMessage: false,
|
|
763
|
-
})
|
|
764
|
-
);
|
|
765
|
-
|
|
766
|
-
const { result: hookLatestOnly } = renderHook(() =>
|
|
767
|
-
useBroadcastChannel(CHANNEL_NAME, {
|
|
768
|
-
...testOptions,
|
|
769
|
-
sourceName: 'same-source',
|
|
770
|
-
keepLatestMessage: true,
|
|
771
|
-
})
|
|
772
|
-
);
|
|
773
|
-
|
|
774
|
-
await waitForAsync();
|
|
775
|
-
|
|
776
|
-
// Send multiple messages from external source
|
|
777
|
-
const externalMessages = [
|
|
778
|
-
{
|
|
779
|
-
id: 'msg-1',
|
|
780
|
-
type: 'test',
|
|
781
|
-
message: { count: 1 },
|
|
782
|
-
source: 'external',
|
|
783
|
-
timestamp: Date.now(),
|
|
784
|
-
},
|
|
785
|
-
{
|
|
786
|
-
id: 'msg-2',
|
|
787
|
-
type: 'test',
|
|
788
|
-
message: { count: 2 },
|
|
789
|
-
source: 'external',
|
|
790
|
-
timestamp: Date.now() + 1,
|
|
791
|
-
},
|
|
792
|
-
{
|
|
793
|
-
id: 'msg-3',
|
|
794
|
-
type: 'test',
|
|
795
|
-
message: { count: 3 },
|
|
796
|
-
source: 'external',
|
|
797
|
-
timestamp: Date.now() + 2,
|
|
798
|
-
},
|
|
799
|
-
];
|
|
800
|
-
|
|
801
|
-
for (const msg of externalMessages) {
|
|
802
|
-
act(() => {
|
|
803
|
-
mockChannels.forEach(channel => {
|
|
804
|
-
channel.simulateMessage(msg);
|
|
805
|
-
});
|
|
806
|
-
});
|
|
807
|
-
await waitForAsync();
|
|
808
|
-
}
|
|
809
|
-
|
|
810
|
-
// Hook with history should have all messages
|
|
811
|
-
expect(hookWithHistory.current.messages).toHaveLength(3);
|
|
812
|
-
|
|
813
|
-
// Hook with keepLatestMessage should only have the last message
|
|
814
|
-
expect(hookLatestOnly.current.messages).toHaveLength(1);
|
|
815
|
-
expect(hookLatestOnly.current.messages[0].message.count).toBe(3);
|
|
816
|
-
});
|
|
817
|
-
|
|
818
|
-
it('should explicitly block self-originated messages even with matching registered types', async () => {
|
|
819
|
-
const { result: hook } = renderHook(() =>
|
|
820
|
-
useBroadcastChannel(CHANNEL_NAME, {
|
|
821
|
-
...testOptions,
|
|
822
|
-
sourceName: 'self-sender',
|
|
823
|
-
registeredTypes: ['self-type', 'another-type'],
|
|
824
|
-
})
|
|
825
|
-
);
|
|
826
|
-
|
|
827
|
-
await waitForAsync();
|
|
828
|
-
|
|
829
|
-
// Hook sends messages that match its own registered types
|
|
830
|
-
act(() => {
|
|
831
|
-
hook.current.postMessage('self-type', { self: true });
|
|
832
|
-
hook.current.postMessage('another-type', { self: true });
|
|
833
|
-
});
|
|
834
|
-
|
|
835
|
-
await waitForAsync();
|
|
836
|
-
|
|
837
|
-
// Should not receive its own messages despite matching registered types
|
|
838
|
-
expect(hook.current.messages).toHaveLength(0);
|
|
839
|
-
|
|
840
|
-
// But should have them in sent messages
|
|
841
|
-
expect(hook.current.sentMessages).toHaveLength(2);
|
|
842
|
-
expect(hook.current.sentMessages[0].source).toBe('self-sender');
|
|
843
|
-
expect(hook.current.sentMessages[1].source).toBe('self-sender');
|
|
844
|
-
|
|
845
|
-
// Verify that source === sourceName is the blocking condition
|
|
846
|
-
expect(hook.current.sentMessages.every(msg => msg.source === 'self-sender')).toBe(true);
|
|
847
|
-
});
|
|
848
|
-
|
|
849
|
-
it('should handle combined config differences with same source name', async () => {
|
|
850
|
-
const { result: hookSpecificLatest } = renderHook(() =>
|
|
851
|
-
useBroadcastChannel(CHANNEL_NAME, {
|
|
852
|
-
...testOptions,
|
|
853
|
-
sourceName: 'shared-source',
|
|
854
|
-
registeredTypes: ['typeA'],
|
|
855
|
-
keepLatestMessage: true,
|
|
856
|
-
})
|
|
857
|
-
);
|
|
858
|
-
|
|
859
|
-
const { result: hookAllHistory } = renderHook(() =>
|
|
860
|
-
useBroadcastChannel(CHANNEL_NAME, {
|
|
861
|
-
...testOptions,
|
|
862
|
-
sourceName: 'shared-source',
|
|
863
|
-
registeredTypes: [], // Listen to all types
|
|
864
|
-
keepLatestMessage: false,
|
|
865
|
-
})
|
|
866
|
-
);
|
|
867
|
-
|
|
868
|
-
await waitForAsync();
|
|
869
|
-
|
|
870
|
-
// Send multiple messages of different types
|
|
871
|
-
const messages = [
|
|
872
|
-
{
|
|
873
|
-
id: 'msg-a1',
|
|
874
|
-
type: 'typeA',
|
|
875
|
-
message: { data: 'a1' },
|
|
876
|
-
source: 'external',
|
|
877
|
-
timestamp: Date.now(),
|
|
878
|
-
},
|
|
879
|
-
{
|
|
880
|
-
id: 'msg-b1',
|
|
881
|
-
type: 'typeB',
|
|
882
|
-
message: { data: 'b1' },
|
|
883
|
-
source: 'external',
|
|
884
|
-
timestamp: Date.now() + 1,
|
|
885
|
-
},
|
|
886
|
-
{
|
|
887
|
-
id: 'msg-a2',
|
|
888
|
-
type: 'typeA',
|
|
889
|
-
message: { data: 'a2' },
|
|
890
|
-
source: 'external',
|
|
891
|
-
timestamp: Date.now() + 2,
|
|
892
|
-
},
|
|
893
|
-
];
|
|
894
|
-
|
|
895
|
-
for (const msg of messages) {
|
|
896
|
-
act(() => {
|
|
897
|
-
mockChannels.forEach(channel => {
|
|
898
|
-
channel.simulateMessage(msg);
|
|
899
|
-
});
|
|
900
|
-
});
|
|
901
|
-
await waitForAsync();
|
|
902
|
-
}
|
|
903
|
-
|
|
904
|
-
// Hook with specific type + keepLatest should only have latest typeA
|
|
905
|
-
expect(hookSpecificLatest.current.messages).toHaveLength(1);
|
|
906
|
-
expect(hookSpecificLatest.current.messages[0].type).toBe('typeA');
|
|
907
|
-
expect(hookSpecificLatest.current.messages[0].message.data).toBe('a2');
|
|
908
|
-
|
|
909
|
-
// Hook listening to all + history should have all messages
|
|
910
|
-
expect(hookAllHistory.current.messages).toHaveLength(3);
|
|
911
|
-
expect(hookAllHistory.current.messages.map(m => m.type)).toEqual(['typeA', 'typeB', 'typeA']);
|
|
912
|
-
});
|
|
913
|
-
|
|
914
|
-
it('should respect deduplication TTL for duplicate message IDs', async () => {
|
|
915
|
-
const { result: hook } = renderHook(() =>
|
|
916
|
-
useBroadcastChannel(CHANNEL_NAME, {
|
|
917
|
-
...testOptions,
|
|
918
|
-
deduplicationTTL: 1000, // 1 second TTL
|
|
919
|
-
})
|
|
920
|
-
);
|
|
921
|
-
|
|
922
|
-
await waitForAsync();
|
|
923
|
-
|
|
924
|
-
// Send the same message multiple times within TTL window
|
|
925
|
-
const duplicateMessage = {
|
|
926
|
-
id: 'duplicate-id',
|
|
927
|
-
type: 'test',
|
|
928
|
-
message: { data: 'duplicate' },
|
|
929
|
-
source: 'external',
|
|
930
|
-
timestamp: Date.now(),
|
|
931
|
-
};
|
|
932
|
-
|
|
933
|
-
act(() => {
|
|
934
|
-
mockChannels.forEach(channel => {
|
|
935
|
-
channel.simulateMessage(duplicateMessage);
|
|
936
|
-
channel.simulateMessage(duplicateMessage); // Immediate duplicate
|
|
937
|
-
});
|
|
938
|
-
});
|
|
939
|
-
|
|
940
|
-
await waitForAsync();
|
|
941
|
-
|
|
942
|
-
// Should only receive one message due to deduplication
|
|
943
|
-
expect(hook.current.messages).toHaveLength(1);
|
|
944
|
-
expect(hook.current.messages[0].message.data).toBe('duplicate');
|
|
945
|
-
});
|
|
946
|
-
|
|
947
|
-
it('should drop messages that just cross expiration boundary', async () => {
|
|
948
|
-
const { result: hook } = renderHook(() =>
|
|
949
|
-
useBroadcastChannel(CHANNEL_NAME, {
|
|
950
|
-
...testOptions,
|
|
951
|
-
})
|
|
952
|
-
);
|
|
953
|
-
|
|
954
|
-
await waitForAsync();
|
|
955
|
-
|
|
956
|
-
const now = Date.now();
|
|
957
|
-
|
|
958
|
-
// Message that just expired
|
|
959
|
-
const expiredMessage = {
|
|
960
|
-
id: 'just-expired',
|
|
961
|
-
type: 'test',
|
|
962
|
-
message: { data: 'expired' },
|
|
963
|
-
source: 'external',
|
|
964
|
-
timestamp: now - 1000,
|
|
965
|
-
expirationDate: now - 10, // Expired 10ms ago (more buffer to avoid timing issues)
|
|
966
|
-
};
|
|
967
|
-
|
|
968
|
-
// Message that's still valid
|
|
969
|
-
const validMessage = {
|
|
970
|
-
id: 'still-valid',
|
|
971
|
-
type: 'test',
|
|
972
|
-
message: { data: 'valid' },
|
|
973
|
-
source: 'external',
|
|
974
|
-
timestamp: now - 1000,
|
|
975
|
-
expirationDate: now + 5000, // Expires in 5 seconds (more buffer)
|
|
976
|
-
};
|
|
977
|
-
|
|
978
|
-
act(() => {
|
|
979
|
-
mockChannels.forEach(channel => {
|
|
980
|
-
channel.simulateMessage(expiredMessage);
|
|
981
|
-
channel.simulateMessage(validMessage);
|
|
982
|
-
});
|
|
983
|
-
});
|
|
984
|
-
|
|
985
|
-
await waitForAsync();
|
|
986
|
-
|
|
987
|
-
// Should only receive the valid message
|
|
988
|
-
expect(hook.current.messages).toHaveLength(1);
|
|
989
|
-
expect(hook.current.messages[0].message.data).toBe('valid');
|
|
990
|
-
});
|
|
991
|
-
});
|
|
992
|
-
});
|