react-broadcast-sync 1.0.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.
@@ -0,0 +1,247 @@
1
+ import {
2
+ generateRandomPart,
3
+ generateSourceName,
4
+ generateMessageId,
5
+ isValidMessage,
6
+ isMessageExpired,
7
+ createMessage,
8
+ getInternalMessageType,
9
+ isInternalType,
10
+ isValidInternalClearMessage,
11
+ debounce
12
+ } from '../utils/messageUtils';
13
+
14
+ beforeEach(() => {
15
+ jest.useFakeTimers();
16
+ });
17
+ afterEach(() => {
18
+ jest.useRealTimers();
19
+ });
20
+
21
+ describe('messageUtils', () => {
22
+ it('generateRandomPart returns a non-empty string', () => {
23
+ const part = generateRandomPart();
24
+ expect(typeof part).toBe('string');
25
+ expect(part).toHaveLength(9);
26
+ });
27
+
28
+ it('generateSourceName includes "tab-" prefix', () => {
29
+ expect(generateSourceName()).toMatch(/^tab-/);
30
+ });
31
+
32
+ it('generateMessageId returns a base64 string', () => {
33
+ const id = generateMessageId('source-1', Date.now());
34
+ expect(typeof id).toBe('string');
35
+ expect(id).toMatch(/^[A-Za-z0-9+/]+={0,2}$/);
36
+ });
37
+
38
+ it('isValidMessage identifies a valid message object', () => {
39
+ expect(isValidMessage({ id: '123' })).toBe(true);
40
+ expect(isValidMessage(null)).toBe(false);
41
+ expect(isValidMessage({})).toBe(false);
42
+ });
43
+
44
+ it('isMessageExpired works correctly', () => {
45
+ const now = Date.now();
46
+ expect(isMessageExpired({ expirationDate: now - 1000 })).toBe(true);
47
+ expect(isMessageExpired({ expirationDate: now + 1000 })).toBe(false);
48
+ expect(isMessageExpired({})).toBe(false);
49
+ });
50
+
51
+ it('createMessage returns a valid message object', () => {
52
+ const source = 'tab-abc';
53
+ const msg = createMessage('type1', { hello: 'world' }, source, { expirationDuration: 1000 });
54
+ expect(msg).toHaveProperty('id');
55
+ expect(msg).toHaveProperty('type', 'type1');
56
+ expect(msg).toHaveProperty('message.hello', 'world');
57
+ expect(msg).toHaveProperty('source', source);
58
+ expect(msg.expirationDate).toBeGreaterThan(Date.now());
59
+ });
60
+
61
+ it('createMessage uses provided expirationDate when given', () => {
62
+ const expirationDate = Date.now() + 5000;
63
+ const message = createMessage('type', {}, 'source', { expirationDate });
64
+ expect(message.expirationDate).toBe(expirationDate);
65
+ });
66
+
67
+ it('getInternalMessageType produces unique hashes for different channels', () => {
68
+ const one = getInternalMessageType('CLEAR_MESSAGE', 'chanA');
69
+ const two = getInternalMessageType('CLEAR_MESSAGE', 'chanB');
70
+ expect(one).not.toBe(two);
71
+ });
72
+
73
+ it('getInternalMessageType generates consistent hash string', () => {
74
+ const type = getInternalMessageType('CLEAR_MESSAGE', 'my-channel', 'ns');
75
+ expect(type).toContain('__INTERNAL__');
76
+ expect(type).toContain('CLEAR_MESSAGE');
77
+ });
78
+
79
+ it('isInternalType detects internal message types', () => {
80
+ const type = getInternalMessageType('CLEAR_ALL_MESSAGES', 'chan');
81
+ expect(isInternalType(type)).toBe(true);
82
+ expect(isInternalType('normal-type')).toBe(false);
83
+ });
84
+
85
+ it('isValidInternalClearMessage detects valid structure', () => {
86
+ const valid = {
87
+ id: 'abc',
88
+ type: getInternalMessageType('CLEAR_ALL_MESSAGES', 'ch'),
89
+ source: 'tab-1'
90
+ };
91
+ const invalid = { id: 123, type: 'bad', source: 'tab-1' };
92
+ expect(isValidInternalClearMessage(valid)).toBe(true);
93
+ expect(isValidInternalClearMessage(invalid)).toBe(false);
94
+ });
95
+ });
96
+
97
+ describe('debounce', () => {
98
+ it('debounce delays function call and can cancel/flush', () => {
99
+ const fn = jest.fn();
100
+ const debounced = debounce(fn, 100);
101
+
102
+ debounced();
103
+ expect(fn).not.toHaveBeenCalled();
104
+
105
+ debounced.flush();
106
+ expect(fn).toHaveBeenCalled();
107
+
108
+ debounced();
109
+ debounced.cancel();
110
+ jest.advanceTimersByTime(150);
111
+ expect(fn).toHaveBeenCalledTimes(1);
112
+ });
113
+
114
+ it('debounce handles multiple calls and preserves this context', () => {
115
+ const obj = { value: 0, increment: function() { this.value++; } };
116
+ const debounced = debounce(obj.increment.bind(obj), 100);
117
+
118
+ debounced();
119
+ debounced();
120
+ debounced.flush();
121
+ expect(obj.value).toBe(1);
122
+ });
123
+
124
+ it('debounce flush returns undefined when no pending call', () => {
125
+ const fn = jest.fn();
126
+ const debounced = debounce(fn, 100);
127
+
128
+ expect(debounced.flush()).toBeUndefined();
129
+ expect(fn).not.toHaveBeenCalled();
130
+ });
131
+
132
+ it('debounce handles function with return value', () => {
133
+ const fn = jest.fn().mockReturnValue('test');
134
+ const debounced = debounce(fn, 100);
135
+
136
+ debounced();
137
+ expect(debounced.flush()).toBe('test');
138
+ });
139
+
140
+ it('debounce.cancel cancels the timeout', () => {
141
+ const fn = jest.fn();
142
+ const debounced = debounce(fn, 100);
143
+ debounced();
144
+ debounced.cancel();
145
+ jest.advanceTimersByTime(200);
146
+ expect(fn).not.toHaveBeenCalled();
147
+ });
148
+
149
+ it('debounce.flush executes immediately', () => {
150
+ const fn = jest.fn();
151
+ const debounced = debounce(fn, 100);
152
+ debounced('test');
153
+ debounced.flush();
154
+ expect(fn).toHaveBeenCalledWith('test');
155
+ });
156
+
157
+ it('debounce handles multiple calls with different arguments', () => {
158
+ const fn = jest.fn();
159
+ const debounced = debounce(fn, 100);
160
+
161
+ debounced('first');
162
+ debounced('second');
163
+ debounced('third');
164
+
165
+ expect(fn).not.toHaveBeenCalled();
166
+ debounced.flush();
167
+ expect(fn).toHaveBeenCalledTimes(1);
168
+ expect(fn).toHaveBeenCalledWith('third');
169
+ });
170
+
171
+ it('debounce handles cancel and flush in sequence', () => {
172
+ const fn = jest.fn();
173
+ const debounced = debounce(fn, 100);
174
+
175
+ debounced('test');
176
+ debounced.cancel();
177
+ expect(debounced.flush()).toBeUndefined();
178
+ expect(fn).not.toHaveBeenCalled();
179
+ });
180
+
181
+ it('debounce.flush executes the function with latest arguments immediately', () => {
182
+ jest.useFakeTimers();
183
+
184
+ const fn = jest.fn((x: number) => x + 1);
185
+ const debounced = debounce(fn, 200);
186
+
187
+ debounced(41);
188
+
189
+ expect(fn).not.toHaveBeenCalled();
190
+
191
+ const flushResult = debounced.flush();
192
+
193
+ expect(fn).toHaveBeenCalledTimes(1);
194
+ expect(fn).toHaveBeenCalledWith(41);
195
+ expect(flushResult).toBe(42);
196
+
197
+ jest.useRealTimers();
198
+ });
199
+
200
+ it('debounce.flush does nothing if timeout is set but lastArgs were reset manually', () => {
201
+ const fn = jest.fn();
202
+
203
+ const debounced = debounce(fn, 100);
204
+
205
+ // Call once, schedule the debounce
206
+ debounced('first');
207
+
208
+ // Simulate internal cleanup (cannot access lastArgs directly, so instead cancel it)
209
+ debounced.cancel(); // This will clear lastArgs and lastThis but not call fn
210
+
211
+ const result = debounced.flush(); // Should do nothing
212
+
213
+ expect(fn).not.toHaveBeenCalled();
214
+ expect(result).toBeUndefined();
215
+ });
216
+
217
+ it('debounce.flush handles case where timeoutId exists but lastArgs is null', () => {
218
+ const fn = jest.fn();
219
+ const debounced = debounce(fn, 100);
220
+
221
+ // Schedule a call
222
+ debounced('test');
223
+
224
+ // Force the timeout to be set but clear lastArgs
225
+ jest.advanceTimersByTime(50); // Advance partially through the timeout
226
+ debounced.cancel(); // This clears lastArgs but might not clear timeoutId immediately
227
+
228
+ // Call flush when timeoutId exists but lastArgs is null
229
+ const result = debounced.flush();
230
+ expect(result).toBeUndefined();
231
+ expect(fn).not.toHaveBeenCalled();
232
+ });
233
+
234
+ it('debounce.flush handles multiple calls with different arguments and preserves return value', () => {
235
+ const fn = jest.fn((x: number) => x * 2);
236
+ const debounced = debounce(fn, 100);
237
+
238
+ debounced(1);
239
+ debounced(2);
240
+ debounced(3);
241
+
242
+ const result = debounced.flush();
243
+ expect(result).toBe(6); // 3 * 2
244
+ expect(fn).toHaveBeenCalledTimes(1);
245
+ expect(fn).toHaveBeenCalledWith(3);
246
+ });
247
+ });