react-broadcast-sync 1.3.0 → 1.5.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 +26 -0
- package/dist/hooks/useBroadcastChannel.d.ts +10 -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 +9 -0
- package/dist/utils/debug.d.ts +3 -0
- package/dist/utils/messageUtils.d.ts +2 -2
- package/package.json +1 -1
- package/src/hooks/useBroadcastChannel.ts +165 -18
- package/src/tests/BroadcastProvider.test.tsx +1 -1
- package/src/tests/useBroadcastChannel.same-tab.test.tsx +15 -4
- package/src/tests/useBroadcastChannel.test.tsx +1024 -691
- package/src/types/types.ts +14 -0
- package/src/utils/debug.ts +3 -0
- package/src/utils/messageUtils.ts +2 -2
|
@@ -5,19 +5,46 @@ import { getInternalMessageType } from '../utils/messageUtils';
|
|
|
5
5
|
let mockChannels: any[] = [];
|
|
6
6
|
|
|
7
7
|
class MockBroadcastChannel {
|
|
8
|
+
static channels: Record<string, MockBroadcastChannel[]> = {};
|
|
9
|
+
|
|
8
10
|
name: string;
|
|
9
11
|
_onmessage: ((event: MessageEvent) => void) | null = null;
|
|
10
12
|
closed = false;
|
|
11
|
-
|
|
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
|
+
|
|
12
37
|
close = jest.fn(() => {
|
|
13
38
|
this.closed = true;
|
|
14
39
|
this._onmessage = null;
|
|
15
40
|
});
|
|
41
|
+
|
|
16
42
|
addEventListener = jest.fn((event: string, callback: (event: MessageEvent) => void) => {
|
|
17
43
|
if (event === 'message') {
|
|
18
44
|
this._onmessage = callback;
|
|
19
45
|
}
|
|
20
46
|
});
|
|
47
|
+
|
|
21
48
|
removeEventListener = jest.fn((event: string, callback: (event: MessageEvent) => void) => {
|
|
22
49
|
if (event === 'message' && this._onmessage === callback) {
|
|
23
50
|
this._onmessage = null;
|
|
@@ -31,14 +58,14 @@ class MockBroadcastChannel {
|
|
|
31
58
|
this._onmessage = fn;
|
|
32
59
|
}
|
|
33
60
|
|
|
34
|
-
constructor(name: string) {
|
|
35
|
-
this.name = name;
|
|
36
|
-
mockChannels.push(this);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
61
|
simulateMessage(data: any) {
|
|
40
62
|
if (this.closed || !this._onmessage) return;
|
|
41
|
-
|
|
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
|
+
}
|
|
42
69
|
}
|
|
43
70
|
}
|
|
44
71
|
|
|
@@ -61,6 +88,7 @@ beforeAll(() => {
|
|
|
61
88
|
|
|
62
89
|
beforeEach(() => {
|
|
63
90
|
mockChannels = [];
|
|
91
|
+
MockBroadcastChannel.channels = {};
|
|
64
92
|
global.BroadcastChannel = MockBroadcastChannel as any;
|
|
65
93
|
});
|
|
66
94
|
|
|
@@ -74,837 +102,1142 @@ afterAll(() => {
|
|
|
74
102
|
});
|
|
75
103
|
|
|
76
104
|
describe('useBroadcastChannel', () => {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
};
|
|
91
|
-
channel.simulateMessage(incomingMessage);
|
|
92
|
-
});
|
|
93
|
-
expect(result.current.messages.length).toBe(1);
|
|
94
|
-
expect(result.current.messages[0].message).toEqual({ text: 'hi' });
|
|
95
|
-
});
|
|
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
|
+
});
|
|
96
118
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
const channel = mockChannels[0];
|
|
119
|
+
await waitFor(() => {
|
|
120
|
+
expect(hook2.current.messages.length).toBe(1);
|
|
121
|
+
});
|
|
101
122
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const selfMsg = result.current.sentMessages[0];
|
|
105
|
-
channel.simulateMessage(selfMsg);
|
|
123
|
+
expect(hook2.current.messages[0].message).toEqual({ text: 'hi' });
|
|
124
|
+
expect(hook2.current.messages[0].source).toBe('source-1');
|
|
106
125
|
});
|
|
107
126
|
|
|
108
|
-
|
|
109
|
-
|
|
127
|
+
it('ignores self messages', async () => {
|
|
128
|
+
const { result } = renderHook(() => useBroadcastChannel('test-channel'));
|
|
129
|
+
await waitForChannel();
|
|
110
130
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const channel = mockChannels[0];
|
|
115
|
-
|
|
116
|
-
const msg = {
|
|
117
|
-
id: '123',
|
|
118
|
-
type: 'greeting',
|
|
119
|
-
message: 'hi',
|
|
120
|
-
source: 'external',
|
|
121
|
-
timestamp: Date.now(),
|
|
122
|
-
};
|
|
131
|
+
act(() => {
|
|
132
|
+
result.current.postMessage('test', 'msg');
|
|
133
|
+
});
|
|
123
134
|
|
|
124
|
-
|
|
125
|
-
|
|
135
|
+
expect(result.current.messages).toEqual([]);
|
|
136
|
+
expect(result.current.sentMessages.length).toBe(1);
|
|
126
137
|
});
|
|
127
138
|
|
|
128
|
-
|
|
139
|
+
it('creates message with expiration duration', async () => {
|
|
140
|
+
const { result } = renderHook(() => useBroadcastChannel('test-channel'));
|
|
141
|
+
await waitForChannel();
|
|
129
142
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
143
|
+
act(() => {
|
|
144
|
+
result.current.postMessage('test', { data: 'test' }, { expirationDuration: 5000 });
|
|
145
|
+
});
|
|
133
146
|
|
|
134
|
-
|
|
147
|
+
const sentMessage = result.current.sentMessages[0];
|
|
148
|
+
expect(sentMessage.expirationDate).toBeDefined();
|
|
149
|
+
expect(sentMessage.expirationDate).toBeGreaterThan(Date.now());
|
|
150
|
+
});
|
|
135
151
|
});
|
|
136
152
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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];
|
|
141
158
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
source: 'x',
|
|
148
|
-
timestamp: Date.now(),
|
|
149
|
-
});
|
|
150
|
-
channel.simulateMessage({
|
|
151
|
-
id: '2',
|
|
152
|
-
type: 'b',
|
|
153
|
-
message: 'b',
|
|
154
|
-
source: 'x',
|
|
159
|
+
const msg = {
|
|
160
|
+
id: '123',
|
|
161
|
+
type: 'greeting',
|
|
162
|
+
message: 'hi',
|
|
163
|
+
source: 'external',
|
|
155
164
|
timestamp: Date.now(),
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
act(() => {
|
|
168
|
+
channel.simulateMessage(msg);
|
|
156
169
|
});
|
|
157
|
-
});
|
|
158
170
|
|
|
159
|
-
|
|
171
|
+
expect(result.current.messages.length).toBe(1);
|
|
160
172
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
173
|
+
act(() => {
|
|
174
|
+
result.current.clearReceivedMessages({ ids: ['123'] });
|
|
175
|
+
});
|
|
164
176
|
|
|
165
|
-
|
|
166
|
-
|
|
177
|
+
expect(result.current.messages.length).toBe(0);
|
|
178
|
+
});
|
|
167
179
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
+
});
|
|
171
201
|
|
|
172
|
-
|
|
173
|
-
result.current.postMessage('test-type', { key: 'value' });
|
|
174
|
-
});
|
|
202
|
+
expect(result.current.messages.length).toBe(2);
|
|
175
203
|
|
|
176
|
-
|
|
177
|
-
|
|
204
|
+
act(() => {
|
|
205
|
+
result.current.clearReceivedMessages();
|
|
206
|
+
});
|
|
178
207
|
|
|
179
|
-
|
|
180
|
-
result.current.clearSentMessages({ ids: [sentId] });
|
|
208
|
+
expect(result.current.messages.length).toBe(0);
|
|
181
209
|
});
|
|
182
210
|
|
|
183
|
-
|
|
184
|
-
|
|
211
|
+
it('clears a specific sent message', async () => {
|
|
212
|
+
const { result } = renderHook(() => useBroadcastChannel('test-channel'));
|
|
213
|
+
await waitForChannel();
|
|
185
214
|
|
|
186
|
-
|
|
187
|
-
|
|
215
|
+
act(() => {
|
|
216
|
+
result.current.postMessage('test-type', { key: 'value' });
|
|
217
|
+
});
|
|
188
218
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
delete global.BroadcastChannel;
|
|
219
|
+
const sentId = result.current.sentMessages[0]?.id;
|
|
220
|
+
expect(result.current.sentMessages.length).toBe(1);
|
|
192
221
|
|
|
193
|
-
|
|
222
|
+
act(() => {
|
|
223
|
+
result.current.clearSentMessages({ ids: [sentId] });
|
|
224
|
+
});
|
|
194
225
|
|
|
195
|
-
|
|
196
|
-
result.current.postMessage('test', { a: 1 });
|
|
226
|
+
expect(result.current.sentMessages.length).toBe(0);
|
|
197
227
|
});
|
|
198
228
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
);
|
|
229
|
+
it('handles internal clear message (single id) from other source', async () => {
|
|
230
|
+
const { result } = renderHook(() => useBroadcastChannel('test-channel'));
|
|
202
231
|
|
|
203
|
-
|
|
204
|
-
jest.useRealTimers();
|
|
205
|
-
});
|
|
232
|
+
await waitForChannel();
|
|
206
233
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
delete (global as any).BroadcastChannel;
|
|
234
|
+
const channel = mockChannels[0];
|
|
235
|
+
const externalSource = 'external-source';
|
|
210
236
|
|
|
211
|
-
|
|
237
|
+
const msg = {
|
|
238
|
+
id: 'to-clear',
|
|
239
|
+
type: 'test',
|
|
240
|
+
message: 'should be removed',
|
|
241
|
+
timestamp: Date.now(),
|
|
242
|
+
source: externalSource,
|
|
243
|
+
};
|
|
212
244
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
+
};
|
|
218
252
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
const channel = mockChannels[0];
|
|
253
|
+
act(() => {
|
|
254
|
+
channel.simulateMessage(msg);
|
|
255
|
+
});
|
|
223
256
|
|
|
224
|
-
|
|
225
|
-
|
|
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);
|
|
226
264
|
});
|
|
227
265
|
|
|
228
|
-
|
|
229
|
-
|
|
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';
|
|
230
271
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
+
};
|
|
244
293
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
294
|
+
act(() => {
|
|
295
|
+
channel.simulateMessage(msg1);
|
|
296
|
+
channel.simulateMessage(msg2);
|
|
297
|
+
});
|
|
248
298
|
|
|
249
|
-
|
|
250
|
-
|
|
299
|
+
await waitFor(() => {
|
|
300
|
+
expect(result.current.messages.length).toBe(2);
|
|
301
|
+
});
|
|
251
302
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
);
|
|
256
|
-
await waitForChannel();
|
|
257
|
-
const channel = mockChannels[0];
|
|
258
|
-
|
|
259
|
-
const message = {
|
|
260
|
-
id: 'duplicate-1',
|
|
261
|
-
type: 'test',
|
|
262
|
-
message: 'duplicate',
|
|
263
|
-
source: 'another-tab',
|
|
264
|
-
timestamp: Date.now(),
|
|
265
|
-
};
|
|
303
|
+
act(() => {
|
|
304
|
+
channel.simulateMessage(internalClear);
|
|
305
|
+
});
|
|
266
306
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
307
|
+
await waitFor(() => {
|
|
308
|
+
expect(result.current.messages.length).toBe(0);
|
|
309
|
+
});
|
|
270
310
|
});
|
|
271
|
-
|
|
272
|
-
expect(result.current.messages.length).toBe(1);
|
|
273
311
|
});
|
|
274
312
|
|
|
275
|
-
|
|
276
|
-
|
|
313
|
+
describe('Error Handling', () => {
|
|
314
|
+
it('handles channel not available error on postMessage', () => {
|
|
315
|
+
const original = global.BroadcastChannel;
|
|
316
|
+
delete (global as any).BroadcastChannel;
|
|
277
317
|
|
|
278
|
-
|
|
318
|
+
const { result } = renderHook(() => useBroadcastChannel('test-channel'));
|
|
279
319
|
|
|
280
|
-
|
|
281
|
-
|
|
320
|
+
act(() => {
|
|
321
|
+
result.current.postMessage('test', { a: 1 });
|
|
322
|
+
});
|
|
282
323
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
message: 'should be removed',
|
|
287
|
-
timestamp: Date.now(),
|
|
288
|
-
source: externalSource,
|
|
289
|
-
};
|
|
324
|
+
expect(result.current.error).toBe(
|
|
325
|
+
'BroadcastChannel is not supported in this browser. Please check browser compatibility.'
|
|
326
|
+
);
|
|
290
327
|
|
|
291
|
-
|
|
292
|
-
id: 'internal-id',
|
|
293
|
-
type: getInternalMessageType('CLEAR_SENT_MESSAGES', 'test-channel'),
|
|
294
|
-
message: { ids: ['to-clear'], types: [] },
|
|
295
|
-
timestamp: Date.now(),
|
|
296
|
-
source: externalSource,
|
|
297
|
-
};
|
|
298
|
-
|
|
299
|
-
act(() => {
|
|
300
|
-
channel.simulateMessage(msg);
|
|
328
|
+
global.BroadcastChannel = original;
|
|
301
329
|
});
|
|
302
330
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
331
|
+
it('sets error when BroadcastChannel is not supported on initialization', () => {
|
|
332
|
+
const original = global.BroadcastChannel;
|
|
333
|
+
delete (global as any).BroadcastChannel;
|
|
306
334
|
|
|
307
|
-
|
|
308
|
-
channel.simulateMessage(internalClear);
|
|
309
|
-
});
|
|
335
|
+
const { result } = renderHook(() => useBroadcastChannel('unsupported-channel'));
|
|
310
336
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
337
|
+
expect(result.current.error).toBe(
|
|
338
|
+
'BroadcastChannel is not supported in this browser. Please check browser compatibility.'
|
|
339
|
+
);
|
|
340
|
+
global.BroadcastChannel = original;
|
|
341
|
+
});
|
|
316
342
|
|
|
317
|
-
|
|
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];
|
|
318
349
|
|
|
319
|
-
|
|
320
|
-
|
|
350
|
+
channel.postMessage.mockImplementationOnce(() => {
|
|
351
|
+
throw new Error('Failed to post message');
|
|
352
|
+
});
|
|
321
353
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
message: 'first',
|
|
326
|
-
timestamp: Date.now(),
|
|
327
|
-
source: externalSource,
|
|
328
|
-
};
|
|
354
|
+
act(() => {
|
|
355
|
+
result.current.postMessage('test', { data: 'test' });
|
|
356
|
+
});
|
|
329
357
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
type: 'type2',
|
|
333
|
-
message: 'second',
|
|
334
|
-
timestamp: Date.now(),
|
|
335
|
-
source: externalSource,
|
|
336
|
-
};
|
|
358
|
+
expect(result.current.error).toBe('Failed to send message');
|
|
359
|
+
});
|
|
337
360
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
timestamp: Date.now(),
|
|
343
|
-
source: externalSource, // ✅ MUST match msg1/msg2
|
|
344
|
-
};
|
|
361
|
+
it('handles error when processing message fails', async () => {
|
|
362
|
+
const { result } = renderHook(() => useBroadcastChannel('test-channel'));
|
|
363
|
+
await waitForChannel();
|
|
364
|
+
const channel = mockChannels[0];
|
|
345
365
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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();
|
|
352
375
|
|
|
353
|
-
|
|
376
|
+
act(() => {
|
|
377
|
+
channel.simulateMessage(badMessage);
|
|
378
|
+
});
|
|
354
379
|
|
|
355
|
-
|
|
356
|
-
|
|
380
|
+
await waitFor(() => {
|
|
381
|
+
expect(result.current.error).toBe('Error processing broadcast message');
|
|
382
|
+
});
|
|
357
383
|
});
|
|
358
384
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
385
|
+
it('handles invalid messages', async () => {
|
|
386
|
+
const { result } = renderHook(() => useBroadcastChannel('test-channel'));
|
|
387
|
+
await waitForChannel();
|
|
388
|
+
const channel = mockChannels[0];
|
|
362
389
|
|
|
363
|
-
|
|
390
|
+
act(() => {
|
|
391
|
+
channel.simulateMessage({ invalid: 'message' });
|
|
392
|
+
});
|
|
364
393
|
|
|
365
|
-
|
|
366
|
-
expect(result.current.messages.length).toBe(0); // ✅ Both cleared
|
|
394
|
+
expect(result.current.messages.length).toBe(0);
|
|
367
395
|
});
|
|
368
396
|
});
|
|
369
397
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
act(() => {
|
|
379
|
-
channel.simulateMessage({
|
|
380
|
-
id: 'expired-2',
|
|
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',
|
|
381
406
|
type: 'test',
|
|
382
407
|
message: 'expired',
|
|
383
408
|
source: 'another-tab',
|
|
384
409
|
timestamp: Date.now() - 1000000,
|
|
385
410
|
expirationDate: Date.now() - 1000000,
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
act(() => {
|
|
414
|
+
channel.simulateMessage(expiredMessage);
|
|
386
415
|
});
|
|
416
|
+
|
|
417
|
+
expect(result.current.messages.length).toBe(0);
|
|
387
418
|
});
|
|
388
419
|
|
|
389
|
-
|
|
390
|
-
jest.
|
|
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();
|
|
391
445
|
});
|
|
392
446
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
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
|
+
});
|
|
396
468
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
useBroadcastChannel('test-channel', {
|
|
400
|
-
registeredTypes: ['allowed-type'],
|
|
401
|
-
})
|
|
402
|
-
);
|
|
403
|
-
await waitForChannel();
|
|
404
|
-
const channel = mockChannels[0];
|
|
405
|
-
|
|
406
|
-
await act(async () => {
|
|
407
|
-
channel.simulateMessage({
|
|
408
|
-
id: 'unregistered',
|
|
409
|
-
type: 'unregistered-type',
|
|
410
|
-
message: 'test',
|
|
411
|
-
source: 'another-tab',
|
|
412
|
-
timestamp: Date.now(),
|
|
469
|
+
act(() => {
|
|
470
|
+
jest.advanceTimersByTime(1500);
|
|
413
471
|
});
|
|
472
|
+
|
|
473
|
+
expect(result.current.messages.length).toBe(0);
|
|
474
|
+
jest.useRealTimers();
|
|
414
475
|
});
|
|
415
476
|
|
|
416
|
-
|
|
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];
|
|
417
483
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
message: 'test',
|
|
484
|
+
const message = {
|
|
485
|
+
id: 'duplicate-1',
|
|
486
|
+
type: 'test',
|
|
487
|
+
message: 'duplicate',
|
|
423
488
|
source: 'another-tab',
|
|
424
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);
|
|
425
535
|
});
|
|
426
536
|
});
|
|
427
537
|
|
|
428
|
-
|
|
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
|
+
|
|
429
562
|
expect(result.current.messages.length).toBe(1);
|
|
563
|
+
expect(result.current.messages[0].message).toBe('second');
|
|
430
564
|
});
|
|
431
565
|
});
|
|
432
566
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
useBroadcastChannel('test-channel'
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
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
|
+
});
|
|
439
573
|
|
|
440
|
-
|
|
441
|
-
channel
|
|
442
|
-
|
|
443
|
-
|
|
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',
|
|
444
581
|
message: 'first',
|
|
445
|
-
source: '
|
|
582
|
+
source: 'source1',
|
|
446
583
|
timestamp: Date.now(),
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
id: '
|
|
450
|
-
type: '
|
|
584
|
+
};
|
|
585
|
+
const msg2 = {
|
|
586
|
+
id: '2',
|
|
587
|
+
type: 'typeB',
|
|
451
588
|
message: 'second',
|
|
452
|
-
source: '
|
|
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',
|
|
453
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);
|
|
454
628
|
});
|
|
629
|
+
expect(result.current.getLatestMessage({ type: 'typeA' })).toEqual(
|
|
630
|
+
expect.objectContaining(msg3)
|
|
631
|
+
);
|
|
455
632
|
});
|
|
456
633
|
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
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
|
+
});
|
|
460
668
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
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);
|
|
480
698
|
});
|
|
699
|
+
expect(result.current.getLatestMessage({ type: 'typeA', source: 'source1' })).toEqual(
|
|
700
|
+
expect.objectContaining(msg3)
|
|
701
|
+
);
|
|
481
702
|
});
|
|
482
703
|
|
|
483
|
-
|
|
484
|
-
|
|
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();
|
|
485
721
|
});
|
|
486
|
-
await Promise.resolve();
|
|
487
722
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
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
|
+
});
|
|
491
741
|
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
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
|
+
});
|
|
496
768
|
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
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);
|
|
500
790
|
});
|
|
791
|
+
expect(result.current.getLatestMessage({ type: '', source: '' })).toEqual(
|
|
792
|
+
expect.objectContaining(msg2)
|
|
793
|
+
);
|
|
501
794
|
});
|
|
502
795
|
|
|
503
|
-
|
|
504
|
-
|
|
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?.();
|
|
505
823
|
});
|
|
506
824
|
|
|
507
|
-
|
|
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
|
+
});
|
|
508
860
|
});
|
|
509
861
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
badMessage.source = 'z';
|
|
524
|
-
badMessage.timestamp = Date.now();
|
|
525
|
-
|
|
526
|
-
await act(async () => {
|
|
527
|
-
channel.simulateMessage(badMessage);
|
|
528
|
-
await Promise.resolve();
|
|
529
|
-
});
|
|
530
|
-
await Promise.resolve();
|
|
531
|
-
expect(result.current.error).toBe('Error processing broadcast message');
|
|
532
|
-
jest.useRealTimers();
|
|
533
|
-
});
|
|
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
|
+
});
|
|
534
875
|
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
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];
|
|
538
882
|
|
|
539
|
-
|
|
883
|
+
act(() => {
|
|
884
|
+
result.current.closeChannel();
|
|
885
|
+
});
|
|
886
|
+
expect(channel.close).toHaveBeenCalledTimes(1);
|
|
540
887
|
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
888
|
+
act(() => {
|
|
889
|
+
result.current.closeChannel();
|
|
890
|
+
});
|
|
891
|
+
// Should not throw or call close again
|
|
892
|
+
expect(channel.close).toHaveBeenCalledTimes(1);
|
|
893
|
+
});
|
|
544
894
|
|
|
545
|
-
|
|
546
|
-
|
|
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];
|
|
547
901
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
902
|
+
act(() => {
|
|
903
|
+
result.current.closeChannel();
|
|
904
|
+
});
|
|
551
905
|
|
|
552
|
-
|
|
553
|
-
|
|
906
|
+
act(() => {
|
|
907
|
+
result.current.postMessage('test', { data: 'should not send' });
|
|
908
|
+
});
|
|
909
|
+
expect(channel.postMessage).not.toHaveBeenCalledWith(
|
|
910
|
+
expect.objectContaining({ type: 'test' })
|
|
911
|
+
);
|
|
554
912
|
});
|
|
555
913
|
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
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];
|
|
560
920
|
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
expect(result.current.getLatestMessage()).toBeNull();
|
|
565
|
-
});
|
|
921
|
+
act(() => {
|
|
922
|
+
result.current.closeChannel();
|
|
923
|
+
});
|
|
566
924
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
};
|
|
578
|
-
const msg2 = {
|
|
579
|
-
id: '2',
|
|
580
|
-
type: 'typeB',
|
|
581
|
-
message: 'second',
|
|
582
|
-
source: 'source2',
|
|
583
|
-
timestamp: Date.now() + 1,
|
|
584
|
-
};
|
|
585
|
-
act(() => {
|
|
586
|
-
channel.simulateMessage(msg1);
|
|
587
|
-
channel.simulateMessage(msg2);
|
|
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);
|
|
588
935
|
});
|
|
589
|
-
expect(result.current.getLatestMessage()).toEqual(expect.objectContaining(msg2));
|
|
590
936
|
});
|
|
591
937
|
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
const msg1 = {
|
|
597
|
-
id: '1',
|
|
598
|
-
type: 'typeA',
|
|
599
|
-
message: 'first',
|
|
600
|
-
source: 'source1',
|
|
601
|
-
timestamp: Date.now(),
|
|
602
|
-
};
|
|
603
|
-
const msg2 = {
|
|
604
|
-
id: '2',
|
|
605
|
-
type: 'typeB',
|
|
606
|
-
message: 'second',
|
|
607
|
-
source: 'source2',
|
|
608
|
-
timestamp: Date.now() + 1,
|
|
609
|
-
};
|
|
610
|
-
const msg3 = {
|
|
611
|
-
id: '3',
|
|
612
|
-
type: 'typeA',
|
|
613
|
-
message: 'third',
|
|
614
|
-
source: 'source3',
|
|
615
|
-
timestamp: Date.now() + 2,
|
|
616
|
-
};
|
|
617
|
-
act(() => {
|
|
618
|
-
channel.simulateMessage(msg1);
|
|
619
|
-
channel.simulateMessage(msg2);
|
|
620
|
-
channel.simulateMessage(msg3);
|
|
621
|
-
});
|
|
622
|
-
expect(result.current.getLatestMessage({ type: 'typeA' })).toEqual(
|
|
623
|
-
expect.objectContaining(msg3)
|
|
624
|
-
);
|
|
625
|
-
});
|
|
938
|
+
describe('Ping Functionality', () => {
|
|
939
|
+
beforeEach(() => {
|
|
940
|
+
jest.useFakeTimers();
|
|
941
|
+
});
|
|
626
942
|
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
const channel = mockChannels[0];
|
|
631
|
-
const msg1 = {
|
|
632
|
-
id: '1',
|
|
633
|
-
type: 'typeA',
|
|
634
|
-
message: 'first',
|
|
635
|
-
source: 'source1',
|
|
636
|
-
timestamp: Date.now(),
|
|
637
|
-
};
|
|
638
|
-
const msg2 = {
|
|
639
|
-
id: '2',
|
|
640
|
-
type: 'typeB',
|
|
641
|
-
message: 'second',
|
|
642
|
-
source: 'source2',
|
|
643
|
-
timestamp: Date.now() + 1,
|
|
644
|
-
};
|
|
645
|
-
const msg3 = {
|
|
646
|
-
id: '3',
|
|
647
|
-
type: 'typeA',
|
|
648
|
-
message: 'third',
|
|
649
|
-
source: 'source1',
|
|
650
|
-
timestamp: Date.now() + 2,
|
|
651
|
-
};
|
|
652
|
-
act(() => {
|
|
653
|
-
channel.simulateMessage(msg1);
|
|
654
|
-
channel.simulateMessage(msg2);
|
|
655
|
-
channel.simulateMessage(msg3);
|
|
656
|
-
});
|
|
657
|
-
expect(result.current.getLatestMessage({ source: 'source1' })).toEqual(
|
|
658
|
-
expect.objectContaining(msg3)
|
|
659
|
-
);
|
|
660
|
-
});
|
|
943
|
+
afterEach(() => {
|
|
944
|
+
jest.useRealTimers();
|
|
945
|
+
});
|
|
661
946
|
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
const channel = mockChannels[0];
|
|
666
|
-
const msg1 = {
|
|
667
|
-
id: '1',
|
|
668
|
-
type: 'typeA',
|
|
669
|
-
message: 'first',
|
|
670
|
-
source: 'source1',
|
|
671
|
-
timestamp: Date.now(),
|
|
672
|
-
};
|
|
673
|
-
const msg2 = {
|
|
674
|
-
id: '2',
|
|
675
|
-
type: 'typeB',
|
|
676
|
-
message: 'second',
|
|
677
|
-
source: 'source2',
|
|
678
|
-
timestamp: Date.now() + 1,
|
|
679
|
-
};
|
|
680
|
-
const msg3 = {
|
|
681
|
-
id: '3',
|
|
682
|
-
type: 'typeA',
|
|
683
|
-
message: 'third',
|
|
684
|
-
source: 'source1',
|
|
685
|
-
timestamp: Date.now() + 2,
|
|
686
|
-
};
|
|
687
|
-
act(() => {
|
|
688
|
-
channel.simulateMessage(msg1);
|
|
689
|
-
channel.simulateMessage(msg2);
|
|
690
|
-
channel.simulateMessage(msg3);
|
|
691
|
-
});
|
|
692
|
-
expect(result.current.getLatestMessage({ type: 'typeA', source: 'source1' })).toEqual(
|
|
693
|
-
expect.objectContaining(msg3)
|
|
694
|
-
);
|
|
695
|
-
});
|
|
947
|
+
it('should return empty array when BroadcastChannel is not available', async () => {
|
|
948
|
+
const original = global.BroadcastChannel;
|
|
949
|
+
delete (global as any).BroadcastChannel;
|
|
696
950
|
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
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;
|
|
710
964
|
});
|
|
711
|
-
expect(result.current.getLatestMessage({ type: 'typeB' })).toBeNull();
|
|
712
|
-
expect(result.current.getLatestMessage({ source: 'source2' })).toBeNull();
|
|
713
|
-
expect(result.current.getLatestMessage({ type: 'typeB', source: 'source2' })).toBeNull();
|
|
714
|
-
});
|
|
715
965
|
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
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');
|
|
731
987
|
});
|
|
732
|
-
expect(result.current.getLatestMessage()).toEqual(expect.objectContaining(msg1));
|
|
733
|
-
});
|
|
734
988
|
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
act(() => {
|
|
754
|
-
channel.simulateMessage(msg1);
|
|
755
|
-
channel.simulateMessage(msg2);
|
|
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');
|
|
756
1007
|
});
|
|
757
|
-
expect(result.current.getLatestMessage({ type: 'typeA', source: 'source1' })).toEqual(
|
|
758
|
-
expect.objectContaining(msg2)
|
|
759
|
-
);
|
|
760
|
-
});
|
|
761
1008
|
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
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');
|
|
783
1033
|
});
|
|
784
|
-
expect(result.current.getLatestMessage({ type: '', source: '' })).toEqual(
|
|
785
|
-
expect.objectContaining(msg2)
|
|
786
|
-
);
|
|
787
|
-
});
|
|
788
1034
|
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
);
|
|
815
|
-
jest.spyOn(Date, 'now').mockRestore?.();
|
|
816
|
-
});
|
|
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
|
+
});
|
|
817
1060
|
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
const msg1 = {
|
|
823
|
-
id: '1',
|
|
824
|
-
type: 'typeA',
|
|
825
|
-
message: 'first',
|
|
826
|
-
source: 'source1',
|
|
827
|
-
timestamp: 1000,
|
|
828
|
-
};
|
|
829
|
-
const msg2 = {
|
|
830
|
-
id: '2',
|
|
831
|
-
type: 'typeB',
|
|
832
|
-
message: 'second',
|
|
833
|
-
source: 'source2',
|
|
834
|
-
timestamp: 2000,
|
|
835
|
-
};
|
|
836
|
-
const msg3 = {
|
|
837
|
-
id: '3',
|
|
838
|
-
type: 'typeA',
|
|
839
|
-
message: 'third',
|
|
840
|
-
source: 'source1',
|
|
841
|
-
timestamp: 3000,
|
|
842
|
-
};
|
|
843
|
-
act(() => {
|
|
844
|
-
channel.simulateMessage(msg1);
|
|
845
|
-
channel.simulateMessage(msg2);
|
|
846
|
-
channel.simulateMessage(msg3);
|
|
847
|
-
});
|
|
848
|
-
// msg1 and msg3 both match typeA/source1 — expect msg3
|
|
849
|
-
expect(result.current.getLatestMessage({ type: 'typeA', source: 'source1' })).toEqual(
|
|
850
|
-
expect.objectContaining(msg3)
|
|
851
|
-
);
|
|
852
|
-
});
|
|
1061
|
+
expect(activeSources).toHaveLength(1);
|
|
1062
|
+
expect(activeSources).toContain('source-2');
|
|
1063
|
+
expect(activeSources).not.toContain('source-3');
|
|
1064
|
+
});
|
|
853
1065
|
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
result.current.closeChannel();
|
|
862
|
-
expect(channel.close).toHaveBeenCalled();
|
|
863
|
-
expect(channel.removeEventListener).toHaveBeenCalledWith('message', expect.any(Function));
|
|
864
|
-
});
|
|
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
|
+
);
|
|
865
1073
|
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
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);
|
|
878
1119
|
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
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
|
+
});
|
|
890
1132
|
});
|
|
891
1133
|
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
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
|
+
]);
|
|
906
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');
|
|
907
1241
|
});
|
|
908
|
-
expect(result.current.messages).toHaveLength(0);
|
|
909
1242
|
});
|
|
910
1243
|
});
|