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