react-broadcast-sync 1.6.0 → 1.9.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 +83 -13
- package/dist/BroadcastProvider.d.ts +2 -1
- 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,1243 +0,0 @@
|
|
|
1
|
-
import { renderHook, act, waitFor } from '@testing-library/react';
|
|
2
|
-
import { useBroadcastChannel } from '../hooks/useBroadcastChannel';
|
|
3
|
-
import { getInternalMessageType } from '../utils/messageUtils';
|
|
4
|
-
|
|
5
|
-
let mockChannels: any[] = [];
|
|
6
|
-
|
|
7
|
-
class MockBroadcastChannel {
|
|
8
|
-
static channels: Record<string, MockBroadcastChannel[]> = {};
|
|
9
|
-
|
|
10
|
-
name: string;
|
|
11
|
-
_onmessage: ((event: MessageEvent) => void) | null = null;
|
|
12
|
-
closed = false;
|
|
13
|
-
|
|
14
|
-
constructor(name: string) {
|
|
15
|
-
this.name = name;
|
|
16
|
-
if (!MockBroadcastChannel.channels[name]) {
|
|
17
|
-
MockBroadcastChannel.channels[name] = [];
|
|
18
|
-
}
|
|
19
|
-
MockBroadcastChannel.channels[name].push(this);
|
|
20
|
-
mockChannels.push(this); // keep your existing tracking
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
postMessage = jest.fn((data: any) => {
|
|
24
|
-
const others = MockBroadcastChannel.channels[this.name] || [];
|
|
25
|
-
for (const channel of others) {
|
|
26
|
-
if (channel !== this && !channel.closed && channel._onmessage) {
|
|
27
|
-
// Support both single and batched (array) messages
|
|
28
|
-
if (Array.isArray(data)) {
|
|
29
|
-
data.forEach(msg => channel._onmessage!({ data: msg } as MessageEvent));
|
|
30
|
-
} else {
|
|
31
|
-
channel._onmessage({ data } as MessageEvent);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
close = jest.fn(() => {
|
|
38
|
-
this.closed = true;
|
|
39
|
-
this._onmessage = null;
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
addEventListener = jest.fn((event: string, callback: (event: MessageEvent) => void) => {
|
|
43
|
-
if (event === 'message') {
|
|
44
|
-
this._onmessage = callback;
|
|
45
|
-
}
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
removeEventListener = jest.fn((event: string, callback: (event: MessageEvent) => void) => {
|
|
49
|
-
if (event === 'message' && this._onmessage === callback) {
|
|
50
|
-
this._onmessage = null;
|
|
51
|
-
}
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
get onmessage() {
|
|
55
|
-
return this._onmessage;
|
|
56
|
-
}
|
|
57
|
-
set onmessage(fn) {
|
|
58
|
-
this._onmessage = fn;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
simulateMessage(data: any) {
|
|
62
|
-
if (this.closed || !this._onmessage) return;
|
|
63
|
-
// Support both single and batched (array) messages
|
|
64
|
-
if (Array.isArray(data)) {
|
|
65
|
-
data.forEach(msg => this._onmessage!({ data: msg } as MessageEvent));
|
|
66
|
-
} else {
|
|
67
|
-
this._onmessage({ data } as MessageEvent);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const waitForChannel = async () => {
|
|
73
|
-
return new Promise<void>(resolve => {
|
|
74
|
-
const checkChannel = () => {
|
|
75
|
-
if (mockChannels.length > 0) {
|
|
76
|
-
resolve();
|
|
77
|
-
} else {
|
|
78
|
-
setTimeout(checkChannel, 0);
|
|
79
|
-
}
|
|
80
|
-
};
|
|
81
|
-
checkChannel();
|
|
82
|
-
});
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
beforeAll(() => {
|
|
86
|
-
global.BroadcastChannel = MockBroadcastChannel as any;
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
beforeEach(() => {
|
|
90
|
-
mockChannels = [];
|
|
91
|
-
MockBroadcastChannel.channels = {};
|
|
92
|
-
global.BroadcastChannel = MockBroadcastChannel as any;
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
afterEach(() => {
|
|
96
|
-
jest.clearAllMocks();
|
|
97
|
-
global.BroadcastChannel = MockBroadcastChannel as any;
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
afterAll(() => {
|
|
101
|
-
delete (global as any).BroadcastChannel;
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
describe('useBroadcastChannel', () => {
|
|
105
|
-
describe('Core Functionality', () => {
|
|
106
|
-
it('posts and receives message', async () => {
|
|
107
|
-
const { result: hook1 } = renderHook(() =>
|
|
108
|
-
useBroadcastChannel('test-channel', { sourceName: 'source-1' })
|
|
109
|
-
);
|
|
110
|
-
const { result: hook2 } = renderHook(() =>
|
|
111
|
-
useBroadcastChannel('test-channel', { sourceName: 'source-2' })
|
|
112
|
-
);
|
|
113
|
-
await waitForChannel();
|
|
114
|
-
|
|
115
|
-
act(() => {
|
|
116
|
-
hook1.current.postMessage('greeting', { text: 'hi' });
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
await waitFor(() => {
|
|
120
|
-
expect(hook2.current.messages.length).toBe(1);
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
expect(hook2.current.messages[0].message).toEqual({ text: 'hi' });
|
|
124
|
-
expect(hook2.current.messages[0].source).toBe('source-1');
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
it('ignores self messages', async () => {
|
|
128
|
-
const { result } = renderHook(() => useBroadcastChannel('test-channel'));
|
|
129
|
-
await waitForChannel();
|
|
130
|
-
|
|
131
|
-
act(() => {
|
|
132
|
-
result.current.postMessage('test', 'msg');
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
expect(result.current.messages).toEqual([]);
|
|
136
|
-
expect(result.current.sentMessages.length).toBe(1);
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
it('creates message with expiration duration', async () => {
|
|
140
|
-
const { result } = renderHook(() => useBroadcastChannel('test-channel'));
|
|
141
|
-
await waitForChannel();
|
|
142
|
-
|
|
143
|
-
act(() => {
|
|
144
|
-
result.current.postMessage('test', { data: 'test' }, { expirationDuration: 5000 });
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
const sentMessage = result.current.sentMessages[0];
|
|
148
|
-
expect(sentMessage.expirationDate).toBeDefined();
|
|
149
|
-
expect(sentMessage.expirationDate).toBeGreaterThan(Date.now());
|
|
150
|
-
});
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
describe('Message Clearing', () => {
|
|
154
|
-
it('clears a specific received message', async () => {
|
|
155
|
-
const { result } = renderHook(() => useBroadcastChannel('test-channel'));
|
|
156
|
-
await waitForChannel();
|
|
157
|
-
const channel = mockChannels[0];
|
|
158
|
-
|
|
159
|
-
const msg = {
|
|
160
|
-
id: '123',
|
|
161
|
-
type: 'greeting',
|
|
162
|
-
message: 'hi',
|
|
163
|
-
source: 'external',
|
|
164
|
-
timestamp: Date.now(),
|
|
165
|
-
};
|
|
166
|
-
|
|
167
|
-
act(() => {
|
|
168
|
-
channel.simulateMessage(msg);
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
expect(result.current.messages.length).toBe(1);
|
|
172
|
-
|
|
173
|
-
act(() => {
|
|
174
|
-
result.current.clearReceivedMessages({ ids: ['123'] });
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
expect(result.current.messages.length).toBe(0);
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
it('clears all received messages', async () => {
|
|
181
|
-
const { result } = renderHook(() => useBroadcastChannel('test-channel'));
|
|
182
|
-
await waitForChannel();
|
|
183
|
-
const channel = mockChannels[0];
|
|
184
|
-
|
|
185
|
-
act(() => {
|
|
186
|
-
channel.simulateMessage({
|
|
187
|
-
id: '1',
|
|
188
|
-
type: 'a',
|
|
189
|
-
message: 'a',
|
|
190
|
-
source: 'x',
|
|
191
|
-
timestamp: Date.now(),
|
|
192
|
-
});
|
|
193
|
-
channel.simulateMessage({
|
|
194
|
-
id: '2',
|
|
195
|
-
type: 'b',
|
|
196
|
-
message: 'b',
|
|
197
|
-
source: 'x',
|
|
198
|
-
timestamp: Date.now(),
|
|
199
|
-
});
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
expect(result.current.messages.length).toBe(2);
|
|
203
|
-
|
|
204
|
-
act(() => {
|
|
205
|
-
result.current.clearReceivedMessages();
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
expect(result.current.messages.length).toBe(0);
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
it('clears a specific sent message', async () => {
|
|
212
|
-
const { result } = renderHook(() => useBroadcastChannel('test-channel'));
|
|
213
|
-
await waitForChannel();
|
|
214
|
-
|
|
215
|
-
act(() => {
|
|
216
|
-
result.current.postMessage('test-type', { key: 'value' });
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
const sentId = result.current.sentMessages[0]?.id;
|
|
220
|
-
expect(result.current.sentMessages.length).toBe(1);
|
|
221
|
-
|
|
222
|
-
act(() => {
|
|
223
|
-
result.current.clearSentMessages({ ids: [sentId] });
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
expect(result.current.sentMessages.length).toBe(0);
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
it('handles internal clear message (single id) from other source', async () => {
|
|
230
|
-
const { result } = renderHook(() => useBroadcastChannel('test-channel'));
|
|
231
|
-
|
|
232
|
-
await waitForChannel();
|
|
233
|
-
|
|
234
|
-
const channel = mockChannels[0];
|
|
235
|
-
const externalSource = 'external-source';
|
|
236
|
-
|
|
237
|
-
const msg = {
|
|
238
|
-
id: 'to-clear',
|
|
239
|
-
type: 'test',
|
|
240
|
-
message: 'should be removed',
|
|
241
|
-
timestamp: Date.now(),
|
|
242
|
-
source: externalSource,
|
|
243
|
-
};
|
|
244
|
-
|
|
245
|
-
const internalClear = {
|
|
246
|
-
id: 'internal-id',
|
|
247
|
-
type: getInternalMessageType('CLEAR_SENT_MESSAGES', 'test-channel'),
|
|
248
|
-
message: { ids: ['to-clear'], types: [] },
|
|
249
|
-
timestamp: Date.now(),
|
|
250
|
-
source: externalSource,
|
|
251
|
-
};
|
|
252
|
-
|
|
253
|
-
act(() => {
|
|
254
|
-
channel.simulateMessage(msg);
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
expect(result.current.messages.length).toBe(1);
|
|
258
|
-
|
|
259
|
-
act(() => {
|
|
260
|
-
channel.simulateMessage(internalClear);
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
expect(result.current.messages.length).toBe(0);
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
it('handles internal clear all messages (wildcard) from other source', async () => {
|
|
267
|
-
const { result } = renderHook(() => useBroadcastChannel('test-channel'));
|
|
268
|
-
await waitForChannel();
|
|
269
|
-
const channel = mockChannels[0];
|
|
270
|
-
const externalSource = 'external-source';
|
|
271
|
-
|
|
272
|
-
const msg1 = {
|
|
273
|
-
id: 'msg1',
|
|
274
|
-
type: 'type1',
|
|
275
|
-
message: 'first',
|
|
276
|
-
timestamp: Date.now(),
|
|
277
|
-
source: externalSource,
|
|
278
|
-
};
|
|
279
|
-
const msg2 = {
|
|
280
|
-
id: 'msg2',
|
|
281
|
-
type: 'type2',
|
|
282
|
-
message: 'second',
|
|
283
|
-
timestamp: Date.now(),
|
|
284
|
-
source: externalSource,
|
|
285
|
-
};
|
|
286
|
-
const internalClear = {
|
|
287
|
-
id: 'internal-id',
|
|
288
|
-
type: getInternalMessageType('CLEAR_SENT_MESSAGES', 'test-channel'),
|
|
289
|
-
message: { ids: [], types: [] }, // wildcard
|
|
290
|
-
timestamp: Date.now(),
|
|
291
|
-
source: externalSource,
|
|
292
|
-
};
|
|
293
|
-
|
|
294
|
-
act(() => {
|
|
295
|
-
channel.simulateMessage(msg1);
|
|
296
|
-
channel.simulateMessage(msg2);
|
|
297
|
-
});
|
|
298
|
-
|
|
299
|
-
await waitFor(() => {
|
|
300
|
-
expect(result.current.messages.length).toBe(2);
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
act(() => {
|
|
304
|
-
channel.simulateMessage(internalClear);
|
|
305
|
-
});
|
|
306
|
-
|
|
307
|
-
await waitFor(() => {
|
|
308
|
-
expect(result.current.messages.length).toBe(0);
|
|
309
|
-
});
|
|
310
|
-
});
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
describe('Error Handling', () => {
|
|
314
|
-
it('handles channel not available error on postMessage', () => {
|
|
315
|
-
const original = global.BroadcastChannel;
|
|
316
|
-
delete (global as any).BroadcastChannel;
|
|
317
|
-
|
|
318
|
-
const { result } = renderHook(() => useBroadcastChannel('test-channel'));
|
|
319
|
-
|
|
320
|
-
act(() => {
|
|
321
|
-
result.current.postMessage('test', { a: 1 });
|
|
322
|
-
});
|
|
323
|
-
|
|
324
|
-
expect(result.current.error).toBe(
|
|
325
|
-
'BroadcastChannel is not supported in this browser. Please check browser compatibility.'
|
|
326
|
-
);
|
|
327
|
-
|
|
328
|
-
global.BroadcastChannel = original;
|
|
329
|
-
});
|
|
330
|
-
|
|
331
|
-
it('sets error when BroadcastChannel is not supported on initialization', () => {
|
|
332
|
-
const original = global.BroadcastChannel;
|
|
333
|
-
delete (global as any).BroadcastChannel;
|
|
334
|
-
|
|
335
|
-
const { result } = renderHook(() => useBroadcastChannel('unsupported-channel'));
|
|
336
|
-
|
|
337
|
-
expect(result.current.error).toBe(
|
|
338
|
-
'BroadcastChannel is not supported in this browser. Please check browser compatibility.'
|
|
339
|
-
);
|
|
340
|
-
global.BroadcastChannel = original;
|
|
341
|
-
});
|
|
342
|
-
|
|
343
|
-
it('handles error when posting message fails', async () => {
|
|
344
|
-
const { result } = renderHook(() =>
|
|
345
|
-
useBroadcastChannel('test-channel', { batchingDelayMs: 0 })
|
|
346
|
-
);
|
|
347
|
-
await waitForChannel();
|
|
348
|
-
const channel = mockChannels[0];
|
|
349
|
-
|
|
350
|
-
channel.postMessage.mockImplementationOnce(() => {
|
|
351
|
-
throw new Error('Failed to post message');
|
|
352
|
-
});
|
|
353
|
-
|
|
354
|
-
act(() => {
|
|
355
|
-
result.current.postMessage('test', { data: 'test' });
|
|
356
|
-
});
|
|
357
|
-
|
|
358
|
-
expect(result.current.error).toBe('Failed to send message');
|
|
359
|
-
});
|
|
360
|
-
|
|
361
|
-
it('handles error when processing message fails', async () => {
|
|
362
|
-
const { result } = renderHook(() => useBroadcastChannel('test-channel'));
|
|
363
|
-
await waitForChannel();
|
|
364
|
-
const channel = mockChannels[0];
|
|
365
|
-
|
|
366
|
-
const badMessage: any = {};
|
|
367
|
-
Object.defineProperty(badMessage, 'id', {
|
|
368
|
-
get() {
|
|
369
|
-
throw new Error('fail');
|
|
370
|
-
},
|
|
371
|
-
});
|
|
372
|
-
badMessage.type = 'test';
|
|
373
|
-
badMessage.source = 'z';
|
|
374
|
-
badMessage.timestamp = Date.now();
|
|
375
|
-
|
|
376
|
-
act(() => {
|
|
377
|
-
channel.simulateMessage(badMessage);
|
|
378
|
-
});
|
|
379
|
-
|
|
380
|
-
await waitFor(() => {
|
|
381
|
-
expect(result.current.error).toBe('Error processing broadcast message');
|
|
382
|
-
});
|
|
383
|
-
});
|
|
384
|
-
|
|
385
|
-
it('handles invalid messages', async () => {
|
|
386
|
-
const { result } = renderHook(() => useBroadcastChannel('test-channel'));
|
|
387
|
-
await waitForChannel();
|
|
388
|
-
const channel = mockChannels[0];
|
|
389
|
-
|
|
390
|
-
act(() => {
|
|
391
|
-
channel.simulateMessage({ invalid: 'message' });
|
|
392
|
-
});
|
|
393
|
-
|
|
394
|
-
expect(result.current.messages.length).toBe(0);
|
|
395
|
-
});
|
|
396
|
-
});
|
|
397
|
-
|
|
398
|
-
describe('Message Lifecycle & Cleanup', () => {
|
|
399
|
-
it('handles expired messages', async () => {
|
|
400
|
-
const { result } = renderHook(() => useBroadcastChannel('test-channel'));
|
|
401
|
-
await waitForChannel();
|
|
402
|
-
const channel = mockChannels[0];
|
|
403
|
-
|
|
404
|
-
const expiredMessage = {
|
|
405
|
-
id: 'expired-1',
|
|
406
|
-
type: 'test',
|
|
407
|
-
message: 'expired',
|
|
408
|
-
source: 'another-tab',
|
|
409
|
-
timestamp: Date.now() - 1000000,
|
|
410
|
-
expirationDate: Date.now() - 1000000,
|
|
411
|
-
};
|
|
412
|
-
|
|
413
|
-
act(() => {
|
|
414
|
-
channel.simulateMessage(expiredMessage);
|
|
415
|
-
});
|
|
416
|
-
|
|
417
|
-
expect(result.current.messages.length).toBe(0);
|
|
418
|
-
});
|
|
419
|
-
|
|
420
|
-
it('performs cleanup of expired messages', async () => {
|
|
421
|
-
jest.useFakeTimers();
|
|
422
|
-
const { result } = renderHook(() =>
|
|
423
|
-
useBroadcastChannel('test-channel', { cleaningInterval: 1000 })
|
|
424
|
-
);
|
|
425
|
-
await waitForChannel();
|
|
426
|
-
const channel = mockChannels[0];
|
|
427
|
-
|
|
428
|
-
act(() => {
|
|
429
|
-
channel.simulateMessage({
|
|
430
|
-
id: 'expired-2',
|
|
431
|
-
type: 'test',
|
|
432
|
-
message: 'expired',
|
|
433
|
-
source: 'another-tab',
|
|
434
|
-
timestamp: Date.now() - 1000000,
|
|
435
|
-
expirationDate: Date.now() - 1000000,
|
|
436
|
-
});
|
|
437
|
-
});
|
|
438
|
-
|
|
439
|
-
act(() => {
|
|
440
|
-
jest.advanceTimersByTime(1000);
|
|
441
|
-
});
|
|
442
|
-
|
|
443
|
-
expect(result.current.messages.length).toBe(0);
|
|
444
|
-
jest.useRealTimers();
|
|
445
|
-
});
|
|
446
|
-
|
|
447
|
-
it('handles cleanup debounce', async () => {
|
|
448
|
-
jest.useFakeTimers();
|
|
449
|
-
const { result } = renderHook(() =>
|
|
450
|
-
useBroadcastChannel('test-channel', {
|
|
451
|
-
cleaningInterval: 1000,
|
|
452
|
-
cleanupDebounceMs: 500,
|
|
453
|
-
})
|
|
454
|
-
);
|
|
455
|
-
await waitForChannel();
|
|
456
|
-
const channel = mockChannels[0];
|
|
457
|
-
|
|
458
|
-
act(() => {
|
|
459
|
-
channel.simulateMessage({
|
|
460
|
-
id: 'expired-3',
|
|
461
|
-
type: 'test',
|
|
462
|
-
message: 'expired',
|
|
463
|
-
source: 'another-tab',
|
|
464
|
-
timestamp: Date.now() - 1000000,
|
|
465
|
-
expirationDate: Date.now() - 1000000,
|
|
466
|
-
});
|
|
467
|
-
});
|
|
468
|
-
|
|
469
|
-
act(() => {
|
|
470
|
-
jest.advanceTimersByTime(1500);
|
|
471
|
-
});
|
|
472
|
-
|
|
473
|
-
expect(result.current.messages.length).toBe(0);
|
|
474
|
-
jest.useRealTimers();
|
|
475
|
-
});
|
|
476
|
-
|
|
477
|
-
it('handles duplicate messages within TTL', async () => {
|
|
478
|
-
const { result } = renderHook(() =>
|
|
479
|
-
useBroadcastChannel('test-channel', { deduplicationTTL: 5000 })
|
|
480
|
-
);
|
|
481
|
-
await waitForChannel();
|
|
482
|
-
const channel = mockChannels[0];
|
|
483
|
-
|
|
484
|
-
const message = {
|
|
485
|
-
id: 'duplicate-1',
|
|
486
|
-
type: 'test',
|
|
487
|
-
message: 'duplicate',
|
|
488
|
-
source: 'another-tab',
|
|
489
|
-
timestamp: Date.now(),
|
|
490
|
-
};
|
|
491
|
-
|
|
492
|
-
act(() => {
|
|
493
|
-
channel.simulateMessage(message);
|
|
494
|
-
channel.simulateMessage(message);
|
|
495
|
-
});
|
|
496
|
-
|
|
497
|
-
expect(result.current.messages.length).toBe(1);
|
|
498
|
-
});
|
|
499
|
-
});
|
|
500
|
-
|
|
501
|
-
describe('Hook Options', () => {
|
|
502
|
-
it('handles registered types filtering', async () => {
|
|
503
|
-
const { result } = renderHook(() =>
|
|
504
|
-
useBroadcastChannel('test-channel', {
|
|
505
|
-
registeredTypes: ['allowed-type'],
|
|
506
|
-
})
|
|
507
|
-
);
|
|
508
|
-
await waitForChannel();
|
|
509
|
-
const channel = mockChannels[0];
|
|
510
|
-
|
|
511
|
-
act(() => {
|
|
512
|
-
channel.simulateMessage({
|
|
513
|
-
id: 'unregistered',
|
|
514
|
-
type: 'unregistered-type',
|
|
515
|
-
message: 'test',
|
|
516
|
-
source: 'another-tab',
|
|
517
|
-
timestamp: Date.now(),
|
|
518
|
-
});
|
|
519
|
-
});
|
|
520
|
-
|
|
521
|
-
expect(result.current.messages.length).toBe(0);
|
|
522
|
-
|
|
523
|
-
act(() => {
|
|
524
|
-
channel.simulateMessage({
|
|
525
|
-
id: 'registered',
|
|
526
|
-
type: 'allowed-type',
|
|
527
|
-
message: 'test',
|
|
528
|
-
source: 'another-tab',
|
|
529
|
-
timestamp: Date.now(),
|
|
530
|
-
});
|
|
531
|
-
});
|
|
532
|
-
|
|
533
|
-
await waitFor(() => {
|
|
534
|
-
expect(result.current.messages.length).toBe(1);
|
|
535
|
-
});
|
|
536
|
-
});
|
|
537
|
-
|
|
538
|
-
it('handles keepLatestMessage option', async () => {
|
|
539
|
-
const { result } = renderHook(() =>
|
|
540
|
-
useBroadcastChannel('test-channel', { keepLatestMessage: true })
|
|
541
|
-
);
|
|
542
|
-
await waitForChannel();
|
|
543
|
-
const channel = mockChannels[0];
|
|
544
|
-
|
|
545
|
-
act(() => {
|
|
546
|
-
channel.simulateMessage({
|
|
547
|
-
id: 'msg1',
|
|
548
|
-
type: 'test',
|
|
549
|
-
message: 'first',
|
|
550
|
-
source: 'another-tab',
|
|
551
|
-
timestamp: Date.now(),
|
|
552
|
-
});
|
|
553
|
-
channel.simulateMessage({
|
|
554
|
-
id: 'msg2',
|
|
555
|
-
type: 'test',
|
|
556
|
-
message: 'second',
|
|
557
|
-
source: 'another-tab',
|
|
558
|
-
timestamp: Date.now(),
|
|
559
|
-
});
|
|
560
|
-
});
|
|
561
|
-
|
|
562
|
-
expect(result.current.messages.length).toBe(1);
|
|
563
|
-
expect(result.current.messages[0].message).toBe('second');
|
|
564
|
-
});
|
|
565
|
-
});
|
|
566
|
-
|
|
567
|
-
describe('getLatestMessage', () => {
|
|
568
|
-
it('returns null if there are no messages', async () => {
|
|
569
|
-
const { result } = renderHook(() => useBroadcastChannel('test-channel'));
|
|
570
|
-
await waitForChannel();
|
|
571
|
-
expect(result.current.getLatestMessage()).toBeNull();
|
|
572
|
-
});
|
|
573
|
-
|
|
574
|
-
it('returns the latest message if no options are provided', async () => {
|
|
575
|
-
const { result } = renderHook(() => useBroadcastChannel('test-channel'));
|
|
576
|
-
await waitForChannel();
|
|
577
|
-
const channel = mockChannels[0];
|
|
578
|
-
const msg1 = {
|
|
579
|
-
id: '1',
|
|
580
|
-
type: 'typeA',
|
|
581
|
-
message: 'first',
|
|
582
|
-
source: 'source1',
|
|
583
|
-
timestamp: Date.now(),
|
|
584
|
-
};
|
|
585
|
-
const msg2 = {
|
|
586
|
-
id: '2',
|
|
587
|
-
type: 'typeB',
|
|
588
|
-
message: 'second',
|
|
589
|
-
source: 'source2',
|
|
590
|
-
timestamp: Date.now() + 1,
|
|
591
|
-
};
|
|
592
|
-
act(() => {
|
|
593
|
-
channel.simulateMessage(msg1);
|
|
594
|
-
channel.simulateMessage(msg2);
|
|
595
|
-
});
|
|
596
|
-
expect(result.current.getLatestMessage()).toEqual(expect.objectContaining(msg2));
|
|
597
|
-
});
|
|
598
|
-
|
|
599
|
-
it('returns the latest message matching type', async () => {
|
|
600
|
-
const { result } = renderHook(() => useBroadcastChannel('test-channel'));
|
|
601
|
-
await waitForChannel();
|
|
602
|
-
const channel = mockChannels[0];
|
|
603
|
-
const msg1 = {
|
|
604
|
-
id: '1',
|
|
605
|
-
type: 'typeA',
|
|
606
|
-
message: 'first',
|
|
607
|
-
source: 'source1',
|
|
608
|
-
timestamp: Date.now(),
|
|
609
|
-
};
|
|
610
|
-
const msg2 = {
|
|
611
|
-
id: '2',
|
|
612
|
-
type: 'typeB',
|
|
613
|
-
message: 'second',
|
|
614
|
-
source: 'source2',
|
|
615
|
-
timestamp: Date.now() + 1,
|
|
616
|
-
};
|
|
617
|
-
const msg3 = {
|
|
618
|
-
id: '3',
|
|
619
|
-
type: 'typeA',
|
|
620
|
-
message: 'third',
|
|
621
|
-
source: 'source3',
|
|
622
|
-
timestamp: Date.now() + 2,
|
|
623
|
-
};
|
|
624
|
-
act(() => {
|
|
625
|
-
channel.simulateMessage(msg1);
|
|
626
|
-
channel.simulateMessage(msg2);
|
|
627
|
-
channel.simulateMessage(msg3);
|
|
628
|
-
});
|
|
629
|
-
expect(result.current.getLatestMessage({ type: 'typeA' })).toEqual(
|
|
630
|
-
expect.objectContaining(msg3)
|
|
631
|
-
);
|
|
632
|
-
});
|
|
633
|
-
|
|
634
|
-
it('returns the latest message matching source', async () => {
|
|
635
|
-
const { result } = renderHook(() => useBroadcastChannel('test-channel'));
|
|
636
|
-
await waitForChannel();
|
|
637
|
-
const channel = mockChannels[0];
|
|
638
|
-
const msg1 = {
|
|
639
|
-
id: '1',
|
|
640
|
-
type: 'typeA',
|
|
641
|
-
message: 'first',
|
|
642
|
-
source: 'source1',
|
|
643
|
-
timestamp: Date.now(),
|
|
644
|
-
};
|
|
645
|
-
const msg2 = {
|
|
646
|
-
id: '2',
|
|
647
|
-
type: 'typeB',
|
|
648
|
-
message: 'second',
|
|
649
|
-
source: 'source2',
|
|
650
|
-
timestamp: Date.now() + 1,
|
|
651
|
-
};
|
|
652
|
-
const msg3 = {
|
|
653
|
-
id: '3',
|
|
654
|
-
type: 'typeA',
|
|
655
|
-
message: 'third',
|
|
656
|
-
source: 'source1',
|
|
657
|
-
timestamp: Date.now() + 2,
|
|
658
|
-
};
|
|
659
|
-
act(() => {
|
|
660
|
-
channel.simulateMessage(msg1);
|
|
661
|
-
channel.simulateMessage(msg2);
|
|
662
|
-
channel.simulateMessage(msg3);
|
|
663
|
-
});
|
|
664
|
-
expect(result.current.getLatestMessage({ source: 'source1' })).toEqual(
|
|
665
|
-
expect.objectContaining(msg3)
|
|
666
|
-
);
|
|
667
|
-
});
|
|
668
|
-
|
|
669
|
-
it('returns the latest message matching both type and source', async () => {
|
|
670
|
-
const { result } = renderHook(() => useBroadcastChannel('test-channel'));
|
|
671
|
-
await waitForChannel();
|
|
672
|
-
const channel = mockChannels[0];
|
|
673
|
-
const msg1 = {
|
|
674
|
-
id: '1',
|
|
675
|
-
type: 'typeA',
|
|
676
|
-
message: 'first',
|
|
677
|
-
source: 'source1',
|
|
678
|
-
timestamp: Date.now(),
|
|
679
|
-
};
|
|
680
|
-
const msg2 = {
|
|
681
|
-
id: '2',
|
|
682
|
-
type: 'typeB',
|
|
683
|
-
message: 'second',
|
|
684
|
-
source: 'source2',
|
|
685
|
-
timestamp: Date.now() + 1,
|
|
686
|
-
};
|
|
687
|
-
const msg3 = {
|
|
688
|
-
id: '3',
|
|
689
|
-
type: 'typeA',
|
|
690
|
-
message: 'third',
|
|
691
|
-
source: 'source1',
|
|
692
|
-
timestamp: Date.now() + 2,
|
|
693
|
-
};
|
|
694
|
-
act(() => {
|
|
695
|
-
channel.simulateMessage(msg1);
|
|
696
|
-
channel.simulateMessage(msg2);
|
|
697
|
-
channel.simulateMessage(msg3);
|
|
698
|
-
});
|
|
699
|
-
expect(result.current.getLatestMessage({ type: 'typeA', source: 'source1' })).toEqual(
|
|
700
|
-
expect.objectContaining(msg3)
|
|
701
|
-
);
|
|
702
|
-
});
|
|
703
|
-
|
|
704
|
-
it('returns null if no message matches the filter', async () => {
|
|
705
|
-
const { result } = renderHook(() => useBroadcastChannel('test-channel'));
|
|
706
|
-
await waitForChannel();
|
|
707
|
-
const channel = mockChannels[0];
|
|
708
|
-
const msg1 = {
|
|
709
|
-
id: '1',
|
|
710
|
-
type: 'typeA',
|
|
711
|
-
message: 'first',
|
|
712
|
-
source: 'source1',
|
|
713
|
-
timestamp: Date.now(),
|
|
714
|
-
};
|
|
715
|
-
act(() => {
|
|
716
|
-
channel.simulateMessage(msg1);
|
|
717
|
-
});
|
|
718
|
-
expect(result.current.getLatestMessage({ type: 'typeB' })).toBeNull();
|
|
719
|
-
expect(result.current.getLatestMessage({ source: 'source2' })).toBeNull();
|
|
720
|
-
expect(result.current.getLatestMessage({ type: 'typeB', source: 'source2' })).toBeNull();
|
|
721
|
-
});
|
|
722
|
-
|
|
723
|
-
it('works with keepLatestMessage=true (only one message)', async () => {
|
|
724
|
-
const { result } = renderHook(() =>
|
|
725
|
-
useBroadcastChannel('test-channel', { keepLatestMessage: true })
|
|
726
|
-
);
|
|
727
|
-
await waitForChannel();
|
|
728
|
-
const channel = mockChannels[0];
|
|
729
|
-
const msg1 = {
|
|
730
|
-
id: '1',
|
|
731
|
-
type: 'typeA',
|
|
732
|
-
message: 'first',
|
|
733
|
-
source: 'source1',
|
|
734
|
-
timestamp: Date.now(),
|
|
735
|
-
};
|
|
736
|
-
act(() => {
|
|
737
|
-
channel.simulateMessage(msg1);
|
|
738
|
-
});
|
|
739
|
-
expect(result.current.getLatestMessage()).toEqual(expect.objectContaining(msg1));
|
|
740
|
-
});
|
|
741
|
-
|
|
742
|
-
it('returns the latest message in correct order (last one)', async () => {
|
|
743
|
-
const { result } = renderHook(() => useBroadcastChannel('test-channel'));
|
|
744
|
-
await waitForChannel();
|
|
745
|
-
const channel = mockChannels[0];
|
|
746
|
-
const msg1 = {
|
|
747
|
-
id: '1',
|
|
748
|
-
type: 'typeA',
|
|
749
|
-
message: 'first',
|
|
750
|
-
source: 'source1',
|
|
751
|
-
timestamp: Date.now(),
|
|
752
|
-
};
|
|
753
|
-
const msg2 = {
|
|
754
|
-
id: '2',
|
|
755
|
-
type: 'typeA',
|
|
756
|
-
message: 'second',
|
|
757
|
-
source: 'source1',
|
|
758
|
-
timestamp: Date.now() + 1,
|
|
759
|
-
};
|
|
760
|
-
act(() => {
|
|
761
|
-
channel.simulateMessage(msg1);
|
|
762
|
-
channel.simulateMessage(msg2);
|
|
763
|
-
});
|
|
764
|
-
expect(result.current.getLatestMessage({ type: 'typeA', source: 'source1' })).toEqual(
|
|
765
|
-
expect.objectContaining(msg2)
|
|
766
|
-
);
|
|
767
|
-
});
|
|
768
|
-
|
|
769
|
-
it('treats empty string filters as no filter (returns latest message)', async () => {
|
|
770
|
-
const { result } = renderHook(() => useBroadcastChannel('test-channel'));
|
|
771
|
-
await waitForChannel();
|
|
772
|
-
const channel = mockChannels[0];
|
|
773
|
-
const msg1 = {
|
|
774
|
-
id: '1',
|
|
775
|
-
type: 'typeA',
|
|
776
|
-
message: 'first',
|
|
777
|
-
source: 'source1',
|
|
778
|
-
timestamp: 1000,
|
|
779
|
-
};
|
|
780
|
-
const msg2 = {
|
|
781
|
-
id: '2',
|
|
782
|
-
type: 'typeB',
|
|
783
|
-
message: 'second',
|
|
784
|
-
source: 'source2',
|
|
785
|
-
timestamp: 2000,
|
|
786
|
-
};
|
|
787
|
-
act(() => {
|
|
788
|
-
channel.simulateMessage(msg1);
|
|
789
|
-
channel.simulateMessage(msg2);
|
|
790
|
-
});
|
|
791
|
-
expect(result.current.getLatestMessage({ type: '', source: '' })).toEqual(
|
|
792
|
-
expect.objectContaining(msg2)
|
|
793
|
-
);
|
|
794
|
-
});
|
|
795
|
-
|
|
796
|
-
it('returns the last message in array if timestamps are the same', async () => {
|
|
797
|
-
jest.spyOn(Date, 'now').mockReturnValue(1000);
|
|
798
|
-
const { result } = renderHook(() => useBroadcastChannel('test-channel'));
|
|
799
|
-
await waitForChannel();
|
|
800
|
-
const channel = mockChannels[0];
|
|
801
|
-
const msg1 = {
|
|
802
|
-
id: '1',
|
|
803
|
-
type: 'typeA',
|
|
804
|
-
message: 'first',
|
|
805
|
-
source: 'source1',
|
|
806
|
-
timestamp: 1000,
|
|
807
|
-
};
|
|
808
|
-
const msg2 = {
|
|
809
|
-
id: '2',
|
|
810
|
-
type: 'typeA',
|
|
811
|
-
message: 'second',
|
|
812
|
-
source: 'source1',
|
|
813
|
-
timestamp: 1000,
|
|
814
|
-
};
|
|
815
|
-
act(() => {
|
|
816
|
-
channel.simulateMessage(msg1);
|
|
817
|
-
channel.simulateMessage(msg2);
|
|
818
|
-
});
|
|
819
|
-
expect(result.current.getLatestMessage({ type: 'typeA', source: 'source1' })).toEqual(
|
|
820
|
-
expect.objectContaining(msg2)
|
|
821
|
-
);
|
|
822
|
-
jest.spyOn(Date, 'now').mockRestore?.();
|
|
823
|
-
});
|
|
824
|
-
|
|
825
|
-
it('returns the last matching message if multiple match the filters', async () => {
|
|
826
|
-
const { result } = renderHook(() => useBroadcastChannel('test-channel'));
|
|
827
|
-
await waitForChannel();
|
|
828
|
-
const channel = mockChannels[0];
|
|
829
|
-
const msg1 = {
|
|
830
|
-
id: '1',
|
|
831
|
-
type: 'typeA',
|
|
832
|
-
message: 'first',
|
|
833
|
-
source: 'source1',
|
|
834
|
-
timestamp: 1000,
|
|
835
|
-
};
|
|
836
|
-
const msg2 = {
|
|
837
|
-
id: '2',
|
|
838
|
-
type: 'typeB',
|
|
839
|
-
message: 'second',
|
|
840
|
-
source: 'source2',
|
|
841
|
-
timestamp: 2000,
|
|
842
|
-
};
|
|
843
|
-
const msg3 = {
|
|
844
|
-
id: '3',
|
|
845
|
-
type: 'typeA',
|
|
846
|
-
message: 'third',
|
|
847
|
-
source: 'source1',
|
|
848
|
-
timestamp: 3000,
|
|
849
|
-
};
|
|
850
|
-
act(() => {
|
|
851
|
-
channel.simulateMessage(msg1);
|
|
852
|
-
channel.simulateMessage(msg2);
|
|
853
|
-
channel.simulateMessage(msg3);
|
|
854
|
-
});
|
|
855
|
-
// msg1 and msg3 both match typeA/source1 — expect msg3
|
|
856
|
-
expect(result.current.getLatestMessage({ type: 'typeA', source: 'source1' })).toEqual(
|
|
857
|
-
expect.objectContaining(msg3)
|
|
858
|
-
);
|
|
859
|
-
});
|
|
860
|
-
});
|
|
861
|
-
|
|
862
|
-
describe('Channel Management', () => {
|
|
863
|
-
it('closes the BroadcastChannel and removes event listener', async () => {
|
|
864
|
-
const { unmount } = renderHook(() =>
|
|
865
|
-
useBroadcastChannel('test-channel', { sourceName: 'another-tab' })
|
|
866
|
-
);
|
|
867
|
-
await waitForChannel();
|
|
868
|
-
const channel = mockChannels[0];
|
|
869
|
-
expect(channel.close).not.toHaveBeenCalled();
|
|
870
|
-
|
|
871
|
-
unmount();
|
|
872
|
-
expect(channel.close).toHaveBeenCalled();
|
|
873
|
-
expect(channel.removeEventListener).toHaveBeenCalledWith('message', expect.any(Function));
|
|
874
|
-
});
|
|
875
|
-
|
|
876
|
-
it('is idempotent (safe to call multiple times)', async () => {
|
|
877
|
-
const { result } = renderHook(() =>
|
|
878
|
-
useBroadcastChannel('test-channel', { sourceName: 'another-tab' })
|
|
879
|
-
);
|
|
880
|
-
await waitForChannel();
|
|
881
|
-
const channel = mockChannels[0];
|
|
882
|
-
|
|
883
|
-
act(() => {
|
|
884
|
-
result.current.closeChannel();
|
|
885
|
-
});
|
|
886
|
-
expect(channel.close).toHaveBeenCalledTimes(1);
|
|
887
|
-
|
|
888
|
-
act(() => {
|
|
889
|
-
result.current.closeChannel();
|
|
890
|
-
});
|
|
891
|
-
// Should not throw or call close again
|
|
892
|
-
expect(channel.close).toHaveBeenCalledTimes(1);
|
|
893
|
-
});
|
|
894
|
-
|
|
895
|
-
it('does not send messages after closeChannel is called', async () => {
|
|
896
|
-
const { result } = renderHook(() =>
|
|
897
|
-
useBroadcastChannel('test-channel', { sourceName: 'another-tab' })
|
|
898
|
-
);
|
|
899
|
-
await waitForChannel();
|
|
900
|
-
const channel = mockChannels[0];
|
|
901
|
-
|
|
902
|
-
act(() => {
|
|
903
|
-
result.current.closeChannel();
|
|
904
|
-
});
|
|
905
|
-
|
|
906
|
-
act(() => {
|
|
907
|
-
result.current.postMessage('test', { data: 'should not send' });
|
|
908
|
-
});
|
|
909
|
-
expect(channel.postMessage).not.toHaveBeenCalledWith(
|
|
910
|
-
expect.objectContaining({ type: 'test' })
|
|
911
|
-
);
|
|
912
|
-
});
|
|
913
|
-
|
|
914
|
-
it('does not receive messages after closeChannel is called', async () => {
|
|
915
|
-
const { result } = renderHook(() =>
|
|
916
|
-
useBroadcastChannel('test-channel', { sourceName: 'another-tab' })
|
|
917
|
-
);
|
|
918
|
-
await waitForChannel();
|
|
919
|
-
const channel = mockChannels[0];
|
|
920
|
-
|
|
921
|
-
act(() => {
|
|
922
|
-
result.current.closeChannel();
|
|
923
|
-
});
|
|
924
|
-
|
|
925
|
-
act(() => {
|
|
926
|
-
channel.simulateMessage({
|
|
927
|
-
id: 'should-not-receive',
|
|
928
|
-
type: 'test',
|
|
929
|
-
message: 'no',
|
|
930
|
-
source: 'external',
|
|
931
|
-
timestamp: Date.now(),
|
|
932
|
-
});
|
|
933
|
-
});
|
|
934
|
-
expect(result.current.messages).toHaveLength(0);
|
|
935
|
-
});
|
|
936
|
-
});
|
|
937
|
-
|
|
938
|
-
describe('Ping Functionality', () => {
|
|
939
|
-
beforeEach(() => {
|
|
940
|
-
jest.useFakeTimers();
|
|
941
|
-
});
|
|
942
|
-
|
|
943
|
-
afterEach(() => {
|
|
944
|
-
jest.useRealTimers();
|
|
945
|
-
});
|
|
946
|
-
|
|
947
|
-
it('should return empty array when BroadcastChannel is not available', async () => {
|
|
948
|
-
const original = global.BroadcastChannel;
|
|
949
|
-
delete (global as any).BroadcastChannel;
|
|
950
|
-
|
|
951
|
-
const { result } = renderHook(() => useBroadcastChannel('test-channel'));
|
|
952
|
-
|
|
953
|
-
let sources: string[] = [];
|
|
954
|
-
await act(async () => {
|
|
955
|
-
sources = await result.current.ping();
|
|
956
|
-
});
|
|
957
|
-
|
|
958
|
-
expect(sources).toEqual([]);
|
|
959
|
-
expect(result.current.error).toBe(
|
|
960
|
-
'BroadcastChannel is not supported in this browser. Please check browser compatibility.'
|
|
961
|
-
);
|
|
962
|
-
|
|
963
|
-
global.BroadcastChannel = original;
|
|
964
|
-
});
|
|
965
|
-
|
|
966
|
-
it('should collect active sources within timeout period', async () => {
|
|
967
|
-
const { result: hook1 } = renderHook(() =>
|
|
968
|
-
useBroadcastChannel('test-channel', { sourceName: 'source-1' })
|
|
969
|
-
);
|
|
970
|
-
renderHook(() => useBroadcastChannel('test-channel', { sourceName: 'source-2' }));
|
|
971
|
-
renderHook(() => useBroadcastChannel('test-channel', { sourceName: 'source-3' }));
|
|
972
|
-
|
|
973
|
-
await waitForChannel();
|
|
974
|
-
|
|
975
|
-
let activeSources: string[] = [];
|
|
976
|
-
await act(async () => {
|
|
977
|
-
const pingPromise = hook1.current.ping(100);
|
|
978
|
-
await Promise.resolve();
|
|
979
|
-
jest.advanceTimersByTime(100);
|
|
980
|
-
activeSources = await pingPromise;
|
|
981
|
-
});
|
|
982
|
-
|
|
983
|
-
expect(activeSources).toHaveLength(2);
|
|
984
|
-
expect(activeSources).toContain('source-2');
|
|
985
|
-
expect(activeSources).toContain('source-3');
|
|
986
|
-
expect(activeSources).not.toContain('source-1');
|
|
987
|
-
});
|
|
988
|
-
|
|
989
|
-
it('should respect custom timeout duration', async () => {
|
|
990
|
-
const { result: hook1 } = renderHook(() =>
|
|
991
|
-
useBroadcastChannel('test-channel', { sourceName: 'source-1' })
|
|
992
|
-
);
|
|
993
|
-
renderHook(() => useBroadcastChannel('test-channel', { sourceName: 'source-2' }));
|
|
994
|
-
|
|
995
|
-
await waitForChannel();
|
|
996
|
-
|
|
997
|
-
let activeSources: string[] = [];
|
|
998
|
-
await act(async () => {
|
|
999
|
-
const pingPromise = hook1.current.ping(500);
|
|
1000
|
-
await Promise.resolve();
|
|
1001
|
-
jest.advanceTimersByTime(500);
|
|
1002
|
-
activeSources = await pingPromise;
|
|
1003
|
-
});
|
|
1004
|
-
|
|
1005
|
-
expect(activeSources).toHaveLength(1);
|
|
1006
|
-
expect(activeSources).toContain('source-2');
|
|
1007
|
-
});
|
|
1008
|
-
|
|
1009
|
-
it('should handle multiple concurrent pings', async () => {
|
|
1010
|
-
const { result: hook1 } = renderHook(() =>
|
|
1011
|
-
useBroadcastChannel('test-channel', { sourceName: 'source-1' })
|
|
1012
|
-
);
|
|
1013
|
-
const { result: hook2 } = renderHook(() =>
|
|
1014
|
-
useBroadcastChannel('test-channel', { sourceName: 'source-2' })
|
|
1015
|
-
);
|
|
1016
|
-
|
|
1017
|
-
await waitForChannel();
|
|
1018
|
-
|
|
1019
|
-
let sources1: string[] = [];
|
|
1020
|
-
let sources2: string[] = [];
|
|
1021
|
-
await act(async () => {
|
|
1022
|
-
const p1 = hook1.current.ping(200);
|
|
1023
|
-
const p2 = hook2.current.ping(200);
|
|
1024
|
-
await Promise.resolve();
|
|
1025
|
-
jest.advanceTimersByTime(201);
|
|
1026
|
-
[sources1, sources2] = await Promise.all([p1, p2]);
|
|
1027
|
-
});
|
|
1028
|
-
|
|
1029
|
-
expect(sources1).toHaveLength(1);
|
|
1030
|
-
expect(sources1).toContain('source-2');
|
|
1031
|
-
expect(sources2).toHaveLength(1);
|
|
1032
|
-
expect(sources2).toContain('source-1');
|
|
1033
|
-
});
|
|
1034
|
-
|
|
1035
|
-
it('should not collect responses after timeout', async () => {
|
|
1036
|
-
const { result: hook1 } = renderHook(() =>
|
|
1037
|
-
useBroadcastChannel('test-channel', { sourceName: 'source-1' })
|
|
1038
|
-
);
|
|
1039
|
-
renderHook(() => useBroadcastChannel('test-channel', { sourceName: 'source-2' }));
|
|
1040
|
-
|
|
1041
|
-
await waitForChannel();
|
|
1042
|
-
const hook1Channel = mockChannels[0];
|
|
1043
|
-
|
|
1044
|
-
let activeSources: string[] = [];
|
|
1045
|
-
await act(async () => {
|
|
1046
|
-
const pingPromise = hook1.current.ping(500);
|
|
1047
|
-
await Promise.resolve();
|
|
1048
|
-
// Simulate a late response after timeout
|
|
1049
|
-
jest.advanceTimersByTime(501);
|
|
1050
|
-
await Promise.resolve();
|
|
1051
|
-
hook1Channel.simulateMessage({
|
|
1052
|
-
id: 'late-pong',
|
|
1053
|
-
type: getInternalMessageType('PONG', 'test-channel'),
|
|
1054
|
-
message: null,
|
|
1055
|
-
source: 'source-3',
|
|
1056
|
-
timestamp: Date.now(),
|
|
1057
|
-
});
|
|
1058
|
-
activeSources = await pingPromise;
|
|
1059
|
-
});
|
|
1060
|
-
|
|
1061
|
-
expect(activeSources).toHaveLength(1);
|
|
1062
|
-
expect(activeSources).toContain('source-2');
|
|
1063
|
-
expect(activeSources).not.toContain('source-3');
|
|
1064
|
-
});
|
|
1065
|
-
|
|
1066
|
-
it('should handle closed channels during ping', async () => {
|
|
1067
|
-
const { result: hook1 } = renderHook(() =>
|
|
1068
|
-
useBroadcastChannel('test-channel', { sourceName: 'source-1' })
|
|
1069
|
-
);
|
|
1070
|
-
const { result: hook2 } = renderHook(() =>
|
|
1071
|
-
useBroadcastChannel('test-channel', { sourceName: 'source-2' })
|
|
1072
|
-
);
|
|
1073
|
-
|
|
1074
|
-
await waitForChannel();
|
|
1075
|
-
|
|
1076
|
-
act(() => {
|
|
1077
|
-
hook2.current.closeChannel();
|
|
1078
|
-
});
|
|
1079
|
-
|
|
1080
|
-
let activeSources: string[] = [];
|
|
1081
|
-
await act(async () => {
|
|
1082
|
-
const pingPromise = hook1.current.ping(100);
|
|
1083
|
-
await Promise.resolve();
|
|
1084
|
-
jest.advanceTimersByTime(150);
|
|
1085
|
-
activeSources = await pingPromise;
|
|
1086
|
-
});
|
|
1087
|
-
|
|
1088
|
-
expect(activeSources).toHaveLength(0);
|
|
1089
|
-
});
|
|
1090
|
-
|
|
1091
|
-
it('should ignore second ping call if one is in progress', async () => {
|
|
1092
|
-
const { result } = renderHook(() => useBroadcastChannel('test-channel', { sourceName: 'A' }));
|
|
1093
|
-
renderHook(() => useBroadcastChannel('test-channel', { sourceName: 'B' }));
|
|
1094
|
-
|
|
1095
|
-
await waitForChannel();
|
|
1096
|
-
|
|
1097
|
-
let res1: string[] = [];
|
|
1098
|
-
let res2: string[] = [];
|
|
1099
|
-
await act(async () => {
|
|
1100
|
-
const p1 = result.current.ping(200);
|
|
1101
|
-
await Promise.resolve();
|
|
1102
|
-
const p2 = result.current.ping(200);
|
|
1103
|
-
jest.advanceTimersByTime(201);
|
|
1104
|
-
[res1, res2] = await Promise.all([p1, p2]);
|
|
1105
|
-
});
|
|
1106
|
-
|
|
1107
|
-
expect(res1).toContain('B');
|
|
1108
|
-
expect(res2).toHaveLength(0);
|
|
1109
|
-
});
|
|
1110
|
-
|
|
1111
|
-
it('should expose isPingInProgress state', async () => {
|
|
1112
|
-
const { result } = renderHook(() =>
|
|
1113
|
-
useBroadcastChannel('test-channel', { sourceName: 'source-1' })
|
|
1114
|
-
);
|
|
1115
|
-
|
|
1116
|
-
await waitForChannel();
|
|
1117
|
-
|
|
1118
|
-
expect(result.current.isPingInProgress).toBe(false);
|
|
1119
|
-
|
|
1120
|
-
let sources: string[] = [];
|
|
1121
|
-
await act(async () => {
|
|
1122
|
-
const pingPromise = result.current.ping(300);
|
|
1123
|
-
await Promise.resolve();
|
|
1124
|
-
expect(result.current.isPingInProgress).toBe(true);
|
|
1125
|
-
jest.advanceTimersByTime(301);
|
|
1126
|
-
sources = await pingPromise;
|
|
1127
|
-
});
|
|
1128
|
-
|
|
1129
|
-
expect(Array.isArray(sources)).toBe(true);
|
|
1130
|
-
expect(result.current.isPingInProgress).toBe(false);
|
|
1131
|
-
});
|
|
1132
|
-
});
|
|
1133
|
-
|
|
1134
|
-
describe('Batching', () => {
|
|
1135
|
-
it('batches multiple messages sent within the delay', async () => {
|
|
1136
|
-
jest.useFakeTimers();
|
|
1137
|
-
const { result: hook1 } = renderHook(() =>
|
|
1138
|
-
useBroadcastChannel('batch-channel', { batchingDelayMs: 50, sourceName: 'A' })
|
|
1139
|
-
);
|
|
1140
|
-
const { result: hook2 } = renderHook(() =>
|
|
1141
|
-
useBroadcastChannel('batch-channel', { sourceName: 'B' })
|
|
1142
|
-
);
|
|
1143
|
-
await waitForChannel();
|
|
1144
|
-
act(() => {
|
|
1145
|
-
hook1.current.postMessage('type1', { a: 1 });
|
|
1146
|
-
hook1.current.postMessage('type2', { b: 2 });
|
|
1147
|
-
});
|
|
1148
|
-
// Fast-forward batching delay
|
|
1149
|
-
act(() => {
|
|
1150
|
-
jest.advanceTimersByTime(51);
|
|
1151
|
-
});
|
|
1152
|
-
// Both messages should be received as a batch (array)
|
|
1153
|
-
await waitFor(() => {
|
|
1154
|
-
expect(hook2.current.messages.length).toBe(2);
|
|
1155
|
-
expect(hook2.current.messages[0].type).toBe('type1');
|
|
1156
|
-
expect(hook2.current.messages[1].type).toBe('type2');
|
|
1157
|
-
});
|
|
1158
|
-
jest.useRealTimers();
|
|
1159
|
-
});
|
|
1160
|
-
|
|
1161
|
-
it('excluded types are sent immediately', async () => {
|
|
1162
|
-
const { result: hook1 } = renderHook(() =>
|
|
1163
|
-
useBroadcastChannel('batch-channel', {
|
|
1164
|
-
batchingDelayMs: 50,
|
|
1165
|
-
excludedBatchMessageTypes: ['urgent'],
|
|
1166
|
-
sourceName: 'A',
|
|
1167
|
-
})
|
|
1168
|
-
);
|
|
1169
|
-
const { result: hook2 } = renderHook(() =>
|
|
1170
|
-
useBroadcastChannel('batch-channel', { sourceName: 'B' })
|
|
1171
|
-
);
|
|
1172
|
-
await waitForChannel();
|
|
1173
|
-
act(() => {
|
|
1174
|
-
hook1.current.postMessage('urgent', { now: true });
|
|
1175
|
-
});
|
|
1176
|
-
// Should be received immediately, not batched
|
|
1177
|
-
await waitFor(() => {
|
|
1178
|
-
expect(hook2.current.messages.length).toBe(1);
|
|
1179
|
-
expect(hook2.current.messages[0].type).toBe('urgent');
|
|
1180
|
-
});
|
|
1181
|
-
});
|
|
1182
|
-
|
|
1183
|
-
it('flushes unsent batched messages on unmount', async () => {
|
|
1184
|
-
const { result, unmount } = renderHook(() =>
|
|
1185
|
-
useBroadcastChannel('batch-channel', { batchingDelayMs: 0, sourceName: 'A' })
|
|
1186
|
-
);
|
|
1187
|
-
const { result: hook2 } = renderHook(() =>
|
|
1188
|
-
useBroadcastChannel('batch-channel', { sourceName: 'B' })
|
|
1189
|
-
);
|
|
1190
|
-
await waitForChannel();
|
|
1191
|
-
act(() => {
|
|
1192
|
-
result.current.postMessage('type1', { a: 1 });
|
|
1193
|
-
});
|
|
1194
|
-
// Unmount (should not affect immediate send)
|
|
1195
|
-
unmount();
|
|
1196
|
-
await waitFor(() => {
|
|
1197
|
-
expect(hook2.current.messages.length).toBe(1);
|
|
1198
|
-
expect(hook2.current.messages[0].type).toBe('type1');
|
|
1199
|
-
});
|
|
1200
|
-
});
|
|
1201
|
-
|
|
1202
|
-
it('handles both single and batch messages in the event handler', async () => {
|
|
1203
|
-
const { result } = renderHook(() =>
|
|
1204
|
-
useBroadcastChannel('batch-channel', { sourceName: 'A' })
|
|
1205
|
-
);
|
|
1206
|
-
await waitForChannel();
|
|
1207
|
-
const channel = mockChannels[0];
|
|
1208
|
-
// Simulate receiving a single message
|
|
1209
|
-
act(() => {
|
|
1210
|
-
channel.simulateMessage({
|
|
1211
|
-
id: '1',
|
|
1212
|
-
type: 'single',
|
|
1213
|
-
message: 'one',
|
|
1214
|
-
source: 'B',
|
|
1215
|
-
timestamp: Date.now(),
|
|
1216
|
-
});
|
|
1217
|
-
});
|
|
1218
|
-
// Simulate receiving a batch (array)
|
|
1219
|
-
act(() => {
|
|
1220
|
-
channel.simulateMessage([
|
|
1221
|
-
{
|
|
1222
|
-
id: '2',
|
|
1223
|
-
type: 'batch1',
|
|
1224
|
-
message: 'two',
|
|
1225
|
-
source: 'B',
|
|
1226
|
-
timestamp: Date.now(),
|
|
1227
|
-
},
|
|
1228
|
-
{
|
|
1229
|
-
id: '3',
|
|
1230
|
-
type: 'batch2',
|
|
1231
|
-
message: 'three',
|
|
1232
|
-
source: 'B',
|
|
1233
|
-
timestamp: Date.now(),
|
|
1234
|
-
},
|
|
1235
|
-
]);
|
|
1236
|
-
});
|
|
1237
|
-
expect(result.current.messages.length).toBe(3);
|
|
1238
|
-
expect(result.current.messages[0].type).toBe('single');
|
|
1239
|
-
expect(result.current.messages[1].type).toBe('batch1');
|
|
1240
|
-
expect(result.current.messages[2].type).toBe('batch2');
|
|
1241
|
-
});
|
|
1242
|
-
});
|
|
1243
|
-
});
|