react-broadcast-sync 1.3.0 → 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 +7 -0
- package/README.md +26 -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 +5 -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 +76 -4
- package/src/tests/useBroadcastChannel.test.tsx +902 -691
- package/src/types/types.ts +8 -0
- package/src/utils/debug.ts +3 -0
- package/src/utils/messageUtils.ts +2 -2
|
@@ -5,19 +5,41 @@ 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
|
+
channel._onmessage({ data } as MessageEvent);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
12
32
|
close = jest.fn(() => {
|
|
13
33
|
this.closed = true;
|
|
14
34
|
this._onmessage = null;
|
|
15
35
|
});
|
|
36
|
+
|
|
16
37
|
addEventListener = jest.fn((event: string, callback: (event: MessageEvent) => void) => {
|
|
17
38
|
if (event === 'message') {
|
|
18
39
|
this._onmessage = callback;
|
|
19
40
|
}
|
|
20
41
|
});
|
|
42
|
+
|
|
21
43
|
removeEventListener = jest.fn((event: string, callback: (event: MessageEvent) => void) => {
|
|
22
44
|
if (event === 'message' && this._onmessage === callback) {
|
|
23
45
|
this._onmessage = null;
|
|
@@ -31,11 +53,6 @@ class MockBroadcastChannel {
|
|
|
31
53
|
this._onmessage = fn;
|
|
32
54
|
}
|
|
33
55
|
|
|
34
|
-
constructor(name: string) {
|
|
35
|
-
this.name = name;
|
|
36
|
-
mockChannels.push(this);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
56
|
simulateMessage(data: any) {
|
|
40
57
|
if (this.closed || !this._onmessage) return;
|
|
41
58
|
this._onmessage({ data } as MessageEvent);
|
|
@@ -61,6 +78,7 @@ beforeAll(() => {
|
|
|
61
78
|
|
|
62
79
|
beforeEach(() => {
|
|
63
80
|
mockChannels = [];
|
|
81
|
+
MockBroadcastChannel.channels = {};
|
|
64
82
|
global.BroadcastChannel = MockBroadcastChannel as any;
|
|
65
83
|
});
|
|
66
84
|
|
|
@@ -74,837 +92,1030 @@ afterAll(() => {
|
|
|
74
92
|
});
|
|
75
93
|
|
|
76
94
|
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
|
-
});
|
|
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
|
+
});
|
|
96
108
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
const channel = mockChannels[0];
|
|
109
|
+
await waitFor(() => {
|
|
110
|
+
expect(hook2.current.messages.length).toBe(1);
|
|
111
|
+
});
|
|
101
112
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const selfMsg = result.current.sentMessages[0];
|
|
105
|
-
channel.simulateMessage(selfMsg);
|
|
113
|
+
expect(hook2.current.messages[0].message).toEqual({ text: 'hi' });
|
|
114
|
+
expect(hook2.current.messages[0].source).toBe('source-1');
|
|
106
115
|
});
|
|
107
116
|
|
|
108
|
-
|
|
109
|
-
|
|
117
|
+
it('ignores self messages', async () => {
|
|
118
|
+
const { result } = renderHook(() => useBroadcastChannel('test-channel'));
|
|
119
|
+
await waitForChannel();
|
|
110
120
|
|
|
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
|
-
};
|
|
121
|
+
act(() => {
|
|
122
|
+
result.current.postMessage('test', 'msg');
|
|
123
|
+
});
|
|
123
124
|
|
|
124
|
-
|
|
125
|
-
|
|
125
|
+
expect(result.current.messages).toEqual([]);
|
|
126
|
+
expect(result.current.sentMessages.length).toBe(1);
|
|
126
127
|
});
|
|
127
128
|
|
|
128
|
-
|
|
129
|
+
it('creates message with expiration duration', async () => {
|
|
130
|
+
const { result } = renderHook(() => useBroadcastChannel('test-channel'));
|
|
131
|
+
await waitForChannel();
|
|
129
132
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
+
act(() => {
|
|
134
|
+
result.current.postMessage('test', { data: 'test' }, { expirationDuration: 5000 });
|
|
135
|
+
});
|
|
133
136
|
|
|
134
|
-
|
|
137
|
+
const sentMessage = result.current.sentMessages[0];
|
|
138
|
+
expect(sentMessage.expirationDate).toBeDefined();
|
|
139
|
+
expect(sentMessage.expirationDate).toBeGreaterThan(Date.now());
|
|
140
|
+
});
|
|
135
141
|
});
|
|
136
142
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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];
|
|
141
148
|
|
|
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',
|
|
149
|
+
const msg = {
|
|
150
|
+
id: '123',
|
|
151
|
+
type: 'greeting',
|
|
152
|
+
message: 'hi',
|
|
153
|
+
source: 'external',
|
|
155
154
|
timestamp: Date.now(),
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
act(() => {
|
|
158
|
+
channel.simulateMessage(msg);
|
|
156
159
|
});
|
|
157
|
-
});
|
|
158
160
|
|
|
159
|
-
|
|
161
|
+
expect(result.current.messages.length).toBe(1);
|
|
160
162
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
163
|
+
act(() => {
|
|
164
|
+
result.current.clearReceivedMessages({ ids: ['123'] });
|
|
165
|
+
});
|
|
164
166
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
+
expect(result.current.messages.length).toBe(0);
|
|
168
|
+
});
|
|
167
169
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
+
});
|
|
171
191
|
|
|
172
|
-
|
|
173
|
-
result.current.postMessage('test-type', { key: 'value' });
|
|
174
|
-
});
|
|
192
|
+
expect(result.current.messages.length).toBe(2);
|
|
175
193
|
|
|
176
|
-
|
|
177
|
-
|
|
194
|
+
act(() => {
|
|
195
|
+
result.current.clearReceivedMessages();
|
|
196
|
+
});
|
|
178
197
|
|
|
179
|
-
|
|
180
|
-
result.current.clearSentMessages({ ids: [sentId] });
|
|
198
|
+
expect(result.current.messages.length).toBe(0);
|
|
181
199
|
});
|
|
182
200
|
|
|
183
|
-
|
|
184
|
-
|
|
201
|
+
it('clears a specific sent message', async () => {
|
|
202
|
+
const { result } = renderHook(() => useBroadcastChannel('test-channel'));
|
|
203
|
+
await waitForChannel();
|
|
185
204
|
|
|
186
|
-
|
|
187
|
-
|
|
205
|
+
act(() => {
|
|
206
|
+
result.current.postMessage('test-type', { key: 'value' });
|
|
207
|
+
});
|
|
188
208
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
delete global.BroadcastChannel;
|
|
209
|
+
const sentId = result.current.sentMessages[0]?.id;
|
|
210
|
+
expect(result.current.sentMessages.length).toBe(1);
|
|
192
211
|
|
|
193
|
-
|
|
212
|
+
act(() => {
|
|
213
|
+
result.current.clearSentMessages({ ids: [sentId] });
|
|
214
|
+
});
|
|
194
215
|
|
|
195
|
-
|
|
196
|
-
result.current.postMessage('test', { a: 1 });
|
|
216
|
+
expect(result.current.sentMessages.length).toBe(0);
|
|
197
217
|
});
|
|
198
218
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
);
|
|
219
|
+
it('handles internal clear message (single id) from other source', async () => {
|
|
220
|
+
const { result } = renderHook(() => useBroadcastChannel('test-channel'));
|
|
202
221
|
|
|
203
|
-
|
|
204
|
-
jest.useRealTimers();
|
|
205
|
-
});
|
|
222
|
+
await waitForChannel();
|
|
206
223
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
delete (global as any).BroadcastChannel;
|
|
224
|
+
const channel = mockChannels[0];
|
|
225
|
+
const externalSource = 'external-source';
|
|
210
226
|
|
|
211
|
-
|
|
227
|
+
const msg = {
|
|
228
|
+
id: 'to-clear',
|
|
229
|
+
type: 'test',
|
|
230
|
+
message: 'should be removed',
|
|
231
|
+
timestamp: Date.now(),
|
|
232
|
+
source: externalSource,
|
|
233
|
+
};
|
|
212
234
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
235
|
+
const internalClear = {
|
|
236
|
+
id: 'internal-id',
|
|
237
|
+
type: getInternalMessageType('CLEAR_SENT_MESSAGES', 'test-channel'),
|
|
238
|
+
message: { ids: ['to-clear'], types: [] },
|
|
239
|
+
timestamp: Date.now(),
|
|
240
|
+
source: externalSource,
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
act(() => {
|
|
244
|
+
channel.simulateMessage(msg);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
expect(result.current.messages.length).toBe(1);
|
|
218
248
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
const channel = mockChannels[0];
|
|
249
|
+
act(() => {
|
|
250
|
+
channel.simulateMessage(internalClear);
|
|
251
|
+
});
|
|
223
252
|
|
|
224
|
-
|
|
225
|
-
channel.simulateMessage({ invalid: 'message' });
|
|
253
|
+
expect(result.current.messages.length).toBe(0);
|
|
226
254
|
});
|
|
227
255
|
|
|
228
|
-
|
|
229
|
-
|
|
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';
|
|
230
261
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
+
};
|
|
244
283
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
284
|
+
act(() => {
|
|
285
|
+
channel.simulateMessage(msg1);
|
|
286
|
+
channel.simulateMessage(msg2);
|
|
287
|
+
});
|
|
248
288
|
|
|
249
|
-
|
|
250
|
-
|
|
289
|
+
await waitFor(() => {
|
|
290
|
+
expect(result.current.messages.length).toBe(2);
|
|
291
|
+
});
|
|
251
292
|
|
|
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
|
-
};
|
|
293
|
+
act(() => {
|
|
294
|
+
channel.simulateMessage(internalClear);
|
|
295
|
+
});
|
|
266
296
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
297
|
+
await waitFor(() => {
|
|
298
|
+
expect(result.current.messages.length).toBe(0);
|
|
299
|
+
});
|
|
270
300
|
});
|
|
271
|
-
|
|
272
|
-
expect(result.current.messages.length).toBe(1);
|
|
273
301
|
});
|
|
274
302
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
303
|
+
describe('Error Handling', () => {
|
|
304
|
+
it('handles channel not available error on postMessage', () => {
|
|
305
|
+
const original = global.BroadcastChannel;
|
|
306
|
+
delete (global as any).BroadcastChannel;
|
|
279
307
|
|
|
280
|
-
|
|
281
|
-
const externalSource = 'external-source';
|
|
308
|
+
const { result } = renderHook(() => useBroadcastChannel('test-channel'));
|
|
282
309
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
message: 'should be removed',
|
|
287
|
-
timestamp: Date.now(),
|
|
288
|
-
source: externalSource,
|
|
289
|
-
};
|
|
310
|
+
act(() => {
|
|
311
|
+
result.current.postMessage('test', { a: 1 });
|
|
312
|
+
});
|
|
290
313
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
message: { ids: ['to-clear'], types: [] },
|
|
295
|
-
timestamp: Date.now(),
|
|
296
|
-
source: externalSource,
|
|
297
|
-
};
|
|
314
|
+
expect(result.current.error).toBe(
|
|
315
|
+
'BroadcastChannel is not supported in this browser. Please check browser compatibility.'
|
|
316
|
+
);
|
|
298
317
|
|
|
299
|
-
|
|
300
|
-
channel.simulateMessage(msg);
|
|
318
|
+
global.BroadcastChannel = original;
|
|
301
319
|
});
|
|
302
320
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
321
|
+
it('sets error when BroadcastChannel is not supported on initialization', () => {
|
|
322
|
+
const original = global.BroadcastChannel;
|
|
323
|
+
delete (global as any).BroadcastChannel;
|
|
306
324
|
|
|
307
|
-
|
|
308
|
-
channel.simulateMessage(internalClear);
|
|
309
|
-
});
|
|
325
|
+
const { result } = renderHook(() => useBroadcastChannel('unsupported-channel'));
|
|
310
326
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
327
|
+
expect(result.current.error).toBe(
|
|
328
|
+
'BroadcastChannel is not supported in this browser. Please check browser compatibility.'
|
|
329
|
+
);
|
|
330
|
+
global.BroadcastChannel = original;
|
|
331
|
+
});
|
|
316
332
|
|
|
317
|
-
|
|
333
|
+
it('handles error when posting message fails', async () => {
|
|
334
|
+
const { result } = renderHook(() => useBroadcastChannel('test-channel'));
|
|
335
|
+
await waitForChannel();
|
|
336
|
+
const channel = mockChannels[0];
|
|
318
337
|
|
|
319
|
-
|
|
320
|
-
|
|
338
|
+
channel.postMessage.mockImplementationOnce(() => {
|
|
339
|
+
throw new Error('Failed to post message');
|
|
340
|
+
});
|
|
321
341
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
message: 'first',
|
|
326
|
-
timestamp: Date.now(),
|
|
327
|
-
source: externalSource,
|
|
328
|
-
};
|
|
342
|
+
act(() => {
|
|
343
|
+
result.current.postMessage('test', { data: 'test' });
|
|
344
|
+
});
|
|
329
345
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
type: 'type2',
|
|
333
|
-
message: 'second',
|
|
334
|
-
timestamp: Date.now(),
|
|
335
|
-
source: externalSource,
|
|
336
|
-
};
|
|
346
|
+
expect(result.current.error).toBe('Failed to send message');
|
|
347
|
+
});
|
|
337
348
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
timestamp: Date.now(),
|
|
343
|
-
source: externalSource, // ✅ MUST match msg1/msg2
|
|
344
|
-
};
|
|
349
|
+
it('handles error when processing message fails', async () => {
|
|
350
|
+
const { result } = renderHook(() => useBroadcastChannel('test-channel'));
|
|
351
|
+
await waitForChannel();
|
|
352
|
+
const channel = mockChannels[0];
|
|
345
353
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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();
|
|
352
363
|
|
|
353
|
-
|
|
364
|
+
act(() => {
|
|
365
|
+
channel.simulateMessage(badMessage);
|
|
366
|
+
});
|
|
354
367
|
|
|
355
|
-
|
|
356
|
-
|
|
368
|
+
await waitFor(() => {
|
|
369
|
+
expect(result.current.error).toBe('Error processing broadcast message');
|
|
370
|
+
});
|
|
357
371
|
});
|
|
358
372
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
373
|
+
it('handles invalid messages', async () => {
|
|
374
|
+
const { result } = renderHook(() => useBroadcastChannel('test-channel'));
|
|
375
|
+
await waitForChannel();
|
|
376
|
+
const channel = mockChannels[0];
|
|
362
377
|
|
|
363
|
-
|
|
378
|
+
act(() => {
|
|
379
|
+
channel.simulateMessage({ invalid: 'message' });
|
|
380
|
+
});
|
|
364
381
|
|
|
365
|
-
|
|
366
|
-
expect(result.current.messages.length).toBe(0); // ✅ Both cleared
|
|
382
|
+
expect(result.current.messages.length).toBe(0);
|
|
367
383
|
});
|
|
368
384
|
});
|
|
369
385
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
act(() => {
|
|
379
|
-
channel.simulateMessage({
|
|
380
|
-
id: 'expired-2',
|
|
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];
|
|
391
|
+
|
|
392
|
+
const expiredMessage = {
|
|
393
|
+
id: 'expired-1',
|
|
381
394
|
type: 'test',
|
|
382
395
|
message: 'expired',
|
|
383
396
|
source: 'another-tab',
|
|
384
397
|
timestamp: Date.now() - 1000000,
|
|
385
398
|
expirationDate: Date.now() - 1000000,
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
act(() => {
|
|
402
|
+
channel.simulateMessage(expiredMessage);
|
|
386
403
|
});
|
|
404
|
+
|
|
405
|
+
expect(result.current.messages.length).toBe(0);
|
|
387
406
|
});
|
|
388
407
|
|
|
389
|
-
|
|
390
|
-
jest.
|
|
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
|
+
});
|
|
426
|
+
|
|
427
|
+
act(() => {
|
|
428
|
+
jest.advanceTimersByTime(1000);
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
expect(result.current.messages.length).toBe(0);
|
|
432
|
+
jest.useRealTimers();
|
|
391
433
|
});
|
|
392
434
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
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
|
+
});
|
|
396
456
|
|
|
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(),
|
|
457
|
+
act(() => {
|
|
458
|
+
jest.advanceTimersByTime(1500);
|
|
413
459
|
});
|
|
460
|
+
|
|
461
|
+
expect(result.current.messages.length).toBe(0);
|
|
462
|
+
jest.useRealTimers();
|
|
414
463
|
});
|
|
415
464
|
|
|
416
|
-
|
|
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];
|
|
417
471
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
message: 'test',
|
|
472
|
+
const message = {
|
|
473
|
+
id: 'duplicate-1',
|
|
474
|
+
type: 'test',
|
|
475
|
+
message: 'duplicate',
|
|
423
476
|
source: 'another-tab',
|
|
424
477
|
timestamp: Date.now(),
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
act(() => {
|
|
481
|
+
channel.simulateMessage(message);
|
|
482
|
+
channel.simulateMessage(message);
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
expect(result.current.messages.length).toBe(1);
|
|
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
|
+
});
|
|
520
|
+
|
|
521
|
+
await waitFor(() => {
|
|
522
|
+
expect(result.current.messages.length).toBe(1);
|
|
425
523
|
});
|
|
426
524
|
});
|
|
427
525
|
|
|
428
|
-
|
|
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
|
+
|
|
429
550
|
expect(result.current.messages.length).toBe(1);
|
|
551
|
+
expect(result.current.messages[0].message).toBe('second');
|
|
430
552
|
});
|
|
431
553
|
});
|
|
432
554
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
useBroadcastChannel('test-channel'
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
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
|
+
});
|
|
439
561
|
|
|
440
|
-
|
|
441
|
-
channel
|
|
442
|
-
|
|
443
|
-
|
|
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',
|
|
444
569
|
message: 'first',
|
|
445
|
-
source: '
|
|
570
|
+
source: 'source1',
|
|
446
571
|
timestamp: Date.now(),
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
id: '
|
|
450
|
-
type: '
|
|
572
|
+
};
|
|
573
|
+
const msg2 = {
|
|
574
|
+
id: '2',
|
|
575
|
+
type: 'typeB',
|
|
451
576
|
message: 'second',
|
|
452
|
-
source: '
|
|
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
|
+
});
|
|
586
|
+
|
|
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',
|
|
594
|
+
message: 'first',
|
|
595
|
+
source: 'source1',
|
|
453
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);
|
|
454
616
|
});
|
|
617
|
+
expect(result.current.getLatestMessage({ type: 'typeA' })).toEqual(
|
|
618
|
+
expect.objectContaining(msg3)
|
|
619
|
+
);
|
|
455
620
|
});
|
|
456
621
|
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
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',
|
|
636
|
+
message: 'second',
|
|
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
|
+
});
|
|
460
656
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
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',
|
|
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);
|
|
480
686
|
});
|
|
687
|
+
expect(result.current.getLatestMessage({ type: 'typeA', source: 'source1' })).toEqual(
|
|
688
|
+
expect.objectContaining(msg3)
|
|
689
|
+
);
|
|
481
690
|
});
|
|
482
691
|
|
|
483
|
-
|
|
484
|
-
|
|
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();
|
|
485
709
|
});
|
|
486
|
-
await Promise.resolve();
|
|
487
710
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
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',
|
|
722
|
+
timestamp: Date.now(),
|
|
723
|
+
};
|
|
724
|
+
act(() => {
|
|
725
|
+
channel.simulateMessage(msg1);
|
|
726
|
+
});
|
|
727
|
+
expect(result.current.getLatestMessage()).toEqual(expect.objectContaining(msg1));
|
|
728
|
+
});
|
|
491
729
|
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
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
|
+
});
|
|
496
756
|
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
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);
|
|
500
778
|
});
|
|
779
|
+
expect(result.current.getLatestMessage({ type: '', source: '' })).toEqual(
|
|
780
|
+
expect.objectContaining(msg2)
|
|
781
|
+
);
|
|
501
782
|
});
|
|
502
783
|
|
|
503
|
-
|
|
504
|
-
|
|
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?.();
|
|
505
811
|
});
|
|
506
812
|
|
|
507
|
-
|
|
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
|
+
});
|
|
508
848
|
});
|
|
509
849
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
await
|
|
529
|
-
|
|
530
|
-
await Promise.resolve();
|
|
531
|
-
expect(result.current.error).toBe('Error processing broadcast message');
|
|
532
|
-
jest.useRealTimers();
|
|
533
|
-
});
|
|
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));
|
|
862
|
+
});
|
|
863
|
+
|
|
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];
|
|
534
870
|
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
871
|
+
act(() => {
|
|
872
|
+
result.current.closeChannel();
|
|
873
|
+
});
|
|
874
|
+
expect(channel.close).toHaveBeenCalledTimes(1);
|
|
875
|
+
|
|
876
|
+
act(() => {
|
|
877
|
+
result.current.closeChannel();
|
|
878
|
+
});
|
|
879
|
+
// Should not throw or call close again
|
|
880
|
+
expect(channel.close).toHaveBeenCalledTimes(1);
|
|
881
|
+
});
|
|
882
|
+
|
|
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];
|
|
889
|
+
|
|
890
|
+
act(() => {
|
|
891
|
+
result.current.closeChannel();
|
|
892
|
+
});
|
|
893
|
+
|
|
894
|
+
act(() => {
|
|
895
|
+
result.current.postMessage('test', { data: 'should not send' });
|
|
896
|
+
});
|
|
897
|
+
expect(channel.postMessage).not.toHaveBeenCalledWith(
|
|
898
|
+
expect.objectContaining({ type: 'test' })
|
|
899
|
+
);
|
|
900
|
+
});
|
|
538
901
|
|
|
539
|
-
|
|
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];
|
|
540
908
|
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
909
|
+
act(() => {
|
|
910
|
+
result.current.closeChannel();
|
|
911
|
+
});
|
|
544
912
|
|
|
545
|
-
|
|
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
|
+
});
|
|
546
924
|
});
|
|
547
925
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
926
|
+
describe('Ping Functionality', () => {
|
|
927
|
+
beforeEach(() => {
|
|
928
|
+
jest.useFakeTimers();
|
|
929
|
+
});
|
|
551
930
|
|
|
552
|
-
|
|
553
|
-
|
|
931
|
+
afterEach(() => {
|
|
932
|
+
jest.useRealTimers();
|
|
554
933
|
});
|
|
555
934
|
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
});
|
|
935
|
+
it('should return empty array when BroadcastChannel is not available', async () => {
|
|
936
|
+
const original = global.BroadcastChannel;
|
|
937
|
+
delete (global as any).BroadcastChannel;
|
|
560
938
|
|
|
561
|
-
|
|
562
|
-
const { result } = renderHook(() => useBroadcastChannel('test-channel'));
|
|
563
|
-
await waitForChannel();
|
|
564
|
-
expect(result.current.getLatestMessage()).toBeNull();
|
|
565
|
-
});
|
|
939
|
+
const { result } = renderHook(() => useBroadcastChannel('test-channel'));
|
|
566
940
|
|
|
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);
|
|
941
|
+
let sources: string[] = [];
|
|
942
|
+
await act(async () => {
|
|
943
|
+
sources = await result.current.ping();
|
|
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;
|
|
588
952
|
});
|
|
589
|
-
expect(result.current.getLatestMessage()).toEqual(expect.objectContaining(msg2));
|
|
590
|
-
});
|
|
591
953
|
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
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
|
-
});
|
|
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
|
+
});
|
|
626
970
|
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
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
|
-
});
|
|
971
|
+
expect(activeSources).toHaveLength(2);
|
|
972
|
+
expect(activeSources).toContain('source-2');
|
|
973
|
+
expect(activeSources).toContain('source-3');
|
|
974
|
+
expect(activeSources).not.toContain('source-1');
|
|
975
|
+
});
|
|
661
976
|
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
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
|
-
});
|
|
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' }));
|
|
696
982
|
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
channel.simulateMessage(msg1);
|
|
983
|
+
await waitForChannel();
|
|
984
|
+
|
|
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
|
+
});
|
|
992
|
+
|
|
993
|
+
expect(activeSources).toHaveLength(1);
|
|
994
|
+
expect(activeSources).toContain('source-2');
|
|
710
995
|
});
|
|
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
996
|
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
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');
|
|
731
1021
|
});
|
|
732
|
-
expect(result.current.getLatestMessage()).toEqual(expect.objectContaining(msg1));
|
|
733
|
-
});
|
|
734
1022
|
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
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
|
+
});
|
|
1048
|
+
|
|
1049
|
+
expect(activeSources).toHaveLength(1);
|
|
1050
|
+
expect(activeSources).toContain('source-2');
|
|
1051
|
+
expect(activeSources).not.toContain('source-3');
|
|
756
1052
|
});
|
|
757
|
-
expect(result.current.getLatestMessage({ type: 'typeA', source: 'source1' })).toEqual(
|
|
758
|
-
expect.objectContaining(msg2)
|
|
759
|
-
);
|
|
760
|
-
});
|
|
761
1053
|
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
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
|
+
);
|
|
1061
|
+
|
|
1062
|
+
await waitForChannel();
|
|
1063
|
+
|
|
1064
|
+
act(() => {
|
|
1065
|
+
hook2.current.closeChannel();
|
|
1066
|
+
});
|
|
1067
|
+
|
|
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
|
+
});
|
|
1075
|
+
|
|
1076
|
+
expect(activeSources).toHaveLength(0);
|
|
783
1077
|
});
|
|
784
|
-
expect(result.current.getLatestMessage({ type: '', source: '' })).toEqual(
|
|
785
|
-
expect.objectContaining(msg2)
|
|
786
|
-
);
|
|
787
|
-
});
|
|
788
1078
|
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
await waitForChannel();
|
|
793
|
-
const channel = mockChannels[0];
|
|
794
|
-
const msg1 = {
|
|
795
|
-
id: '1',
|
|
796
|
-
type: 'typeA',
|
|
797
|
-
message: 'first',
|
|
798
|
-
source: 'source1',
|
|
799
|
-
timestamp: 1000,
|
|
800
|
-
};
|
|
801
|
-
const msg2 = {
|
|
802
|
-
id: '2',
|
|
803
|
-
type: 'typeA',
|
|
804
|
-
message: 'second',
|
|
805
|
-
source: 'source1',
|
|
806
|
-
timestamp: 1000,
|
|
807
|
-
};
|
|
808
|
-
act(() => {
|
|
809
|
-
channel.simulateMessage(msg1);
|
|
810
|
-
channel.simulateMessage(msg2);
|
|
811
|
-
});
|
|
812
|
-
expect(result.current.getLatestMessage({ type: 'typeA', source: 'source1' })).toEqual(
|
|
813
|
-
expect.objectContaining(msg2)
|
|
814
|
-
);
|
|
815
|
-
jest.spyOn(Date, 'now').mockRestore?.();
|
|
816
|
-
});
|
|
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' }));
|
|
817
1082
|
|
|
818
|
-
|
|
819
|
-
const { result } = renderHook(() => useBroadcastChannel('test-channel'));
|
|
820
|
-
await waitForChannel();
|
|
821
|
-
const channel = mockChannels[0];
|
|
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
|
-
});
|
|
1083
|
+
await waitForChannel();
|
|
853
1084
|
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
expect(channel.removeEventListener).toHaveBeenCalledWith('message', expect.any(Function));
|
|
864
|
-
});
|
|
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
|
+
});
|
|
865
1094
|
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
);
|
|
870
|
-
await waitForChannel();
|
|
871
|
-
const channel = mockChannels[0];
|
|
872
|
-
result.current.closeChannel();
|
|
873
|
-
expect(channel.close).toHaveBeenCalledTimes(1);
|
|
874
|
-
result.current.closeChannel();
|
|
875
|
-
// Should not throw or call close again
|
|
876
|
-
expect(channel.close).toHaveBeenCalledTimes(1);
|
|
877
|
-
});
|
|
1095
|
+
expect(res1).toContain('B');
|
|
1096
|
+
expect(res2).toHaveLength(0);
|
|
1097
|
+
});
|
|
878
1098
|
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
await waitForChannel();
|
|
884
|
-
const channel = mockChannels[0];
|
|
885
|
-
result.current.closeChannel();
|
|
886
|
-
act(() => {
|
|
887
|
-
result.current.postMessage('test', { data: 'should not send' });
|
|
888
|
-
});
|
|
889
|
-
expect(channel.postMessage).not.toHaveBeenCalledWith(expect.objectContaining({ type: 'test' }));
|
|
890
|
-
});
|
|
1099
|
+
it('should expose isPingInProgress state', async () => {
|
|
1100
|
+
const { result } = renderHook(() =>
|
|
1101
|
+
useBroadcastChannel('test-channel', { sourceName: 'source-1' })
|
|
1102
|
+
);
|
|
891
1103
|
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
message: 'no',
|
|
904
|
-
source: 'external',
|
|
905
|
-
timestamp: Date.now(),
|
|
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;
|
|
906
1115
|
});
|
|
1116
|
+
|
|
1117
|
+
expect(Array.isArray(sources)).toBe(true);
|
|
1118
|
+
expect(result.current.isPingInProgress).toBe(false);
|
|
907
1119
|
});
|
|
908
|
-
expect(result.current.messages).toHaveLength(0);
|
|
909
1120
|
});
|
|
910
1121
|
});
|