react-broadcast-sync 1.5.1 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +59 -11
- package/dist/BroadcastProvider.d.ts +2 -7
- package/dist/index.cjs.js +1 -1
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.esm.js +1 -1
- package/dist/index.esm.js.map +1 -1
- package/dist/types/types.d.ts +17 -0
- package/dist/utils/debug.d.ts +7 -1
- package/package.json +6 -1
- package/.eslintignore +0 -5
- package/.github/workflows/ci.yml +0 -61
- package/.github/workflows/release.yml +0 -39
- package/.husky/pre-commit +0 -5
- package/.nvmrc +0 -1
- package/.prettierignore +0 -4
- package/.prettierrc +0 -9
- package/.releaserc.json +0 -22
- package/CHANGELOG.md +0 -90
- package/assets/react-broadcast-sync-logo.png +0 -0
- package/commitlint.config.js +0 -3
- package/demo/react-broadcast-sync-demo/README.md +0 -130
- package/demo/react-broadcast-sync-demo/index.html +0 -14
- package/demo/react-broadcast-sync-demo/package-lock.json +0 -6379
- package/demo/react-broadcast-sync-demo/package.json +0 -42
- package/demo/react-broadcast-sync-demo/public/assets/react-broadcast-sync-demo-video.gif +0 -0
- package/demo/react-broadcast-sync-demo/public/assets/react-broadcast-sync-logo.png +0 -0
- package/demo/react-broadcast-sync-demo/src/App.tsx +0 -277
- package/demo/react-broadcast-sync-demo/src/components/Counter.tsx +0 -63
- package/demo/react-broadcast-sync-demo/src/components/TextSync.tsx +0 -61
- package/demo/react-broadcast-sync-demo/src/components/TodoList.tsx +0 -186
- package/demo/react-broadcast-sync-demo/src/hooks/useCounterBroadcast.ts +0 -96
- package/demo/react-broadcast-sync-demo/src/hooks/useTextBroadcast.ts +0 -37
- package/demo/react-broadcast-sync-demo/src/hooks/useTodoBroadcast.ts +0 -249
- package/demo/react-broadcast-sync-demo/src/main.tsx +0 -11
- package/demo/react-broadcast-sync-demo/src/theme.ts +0 -62
- package/demo/react-broadcast-sync-demo/tsconfig.json +0 -26
- package/demo/react-broadcast-sync-demo/tsconfig.node.json +0 -10
- package/demo/react-broadcast-sync-demo/vite.config.ts +0 -12
- package/eslint.config.js +0 -38
- package/jest.config.cjs +0 -9
- package/jest.setup.ts +0 -1
- package/rollup.config.mjs +0 -98
- package/src/BroadcastProvider.tsx +0 -80
- package/src/hooks/useBroadcastChannel.ts +0 -464
- package/src/index.ts +0 -9
- package/src/tests/BroadcastProvider.test.tsx +0 -163
- package/src/tests/debug.test.ts +0 -239
- package/src/tests/messageUtils.test.ts +0 -252
- package/src/tests/useBroadcastChannel.same-tab.test.tsx +0 -992
- package/src/tests/useBroadcastChannel.test.tsx +0 -1243
- package/src/types/types.ts +0 -124
- package/src/utils/debug.ts +0 -54
- package/src/utils/messageUtils.ts +0 -122
- package/tsconfig.json +0 -36
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
import { render, screen, act } from '@testing-library/react';
|
|
2
|
-
import '@testing-library/jest-dom';
|
|
3
|
-
import { BroadcastProvider, useBroadcastProvider } from '../BroadcastProvider';
|
|
4
|
-
|
|
5
|
-
// Mock BroadcastChannel
|
|
6
|
-
let mockChannels: any[] = [];
|
|
7
|
-
|
|
8
|
-
class MockBroadcastChannel {
|
|
9
|
-
name: string;
|
|
10
|
-
onmessage: ((event: MessageEvent) => void) | null = null;
|
|
11
|
-
postMessage = jest.fn();
|
|
12
|
-
close = jest.fn();
|
|
13
|
-
addEventListener = jest.fn((event: string, callback: (event: MessageEvent) => void) => {
|
|
14
|
-
if (event === 'message') {
|
|
15
|
-
this.onmessage = callback;
|
|
16
|
-
}
|
|
17
|
-
});
|
|
18
|
-
removeEventListener = jest.fn();
|
|
19
|
-
|
|
20
|
-
constructor(name: string) {
|
|
21
|
-
this.name = name;
|
|
22
|
-
mockChannels.push(this);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
simulateMessage(data: any) {
|
|
26
|
-
if (this.onmessage) {
|
|
27
|
-
this.onmessage({ data } as MessageEvent);
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// Test component that uses the provider
|
|
33
|
-
const TestComponent = () => {
|
|
34
|
-
const { messages, error } = useBroadcastProvider();
|
|
35
|
-
return (
|
|
36
|
-
<div>
|
|
37
|
-
<div data-testid="error">{error}</div>
|
|
38
|
-
<div data-testid="messages">
|
|
39
|
-
{messages.map(msg => (
|
|
40
|
-
<div key={msg.id} data-testid={`message-${msg.id}`}>
|
|
41
|
-
{msg.message.text}
|
|
42
|
-
</div>
|
|
43
|
-
))}
|
|
44
|
-
</div>
|
|
45
|
-
</div>
|
|
46
|
-
);
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
describe('BroadcastProvider', () => {
|
|
50
|
-
beforeEach(() => {
|
|
51
|
-
mockChannels = [];
|
|
52
|
-
global.BroadcastChannel = MockBroadcastChannel as any;
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
afterEach(() => {
|
|
56
|
-
jest.clearAllMocks();
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it('creates a broadcast channel with the provided name', () => {
|
|
60
|
-
render(
|
|
61
|
-
<BroadcastProvider channelName="test-channel">
|
|
62
|
-
<TestComponent />
|
|
63
|
-
</BroadcastProvider>
|
|
64
|
-
);
|
|
65
|
-
|
|
66
|
-
// The provider creates two channels - one in the provider and one in useBroadcastChannel
|
|
67
|
-
expect(mockChannels).toHaveLength(2);
|
|
68
|
-
expect(mockChannels[0].name).toBe('test-channel');
|
|
69
|
-
expect(mockChannels[1].name).toBe('test-channel');
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it('provides context values to children', () => {
|
|
73
|
-
render(
|
|
74
|
-
<BroadcastProvider channelName="test-channel">
|
|
75
|
-
<TestComponent />
|
|
76
|
-
</BroadcastProvider>
|
|
77
|
-
);
|
|
78
|
-
|
|
79
|
-
expect(screen.getByTestId('error')).toHaveTextContent('');
|
|
80
|
-
expect(screen.getByTestId('messages')).toBeEmptyDOMElement();
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it('handles messages through the provider', () => {
|
|
84
|
-
render(
|
|
85
|
-
<BroadcastProvider channelName="test-channel">
|
|
86
|
-
<TestComponent />
|
|
87
|
-
</BroadcastProvider>
|
|
88
|
-
);
|
|
89
|
-
|
|
90
|
-
const message = {
|
|
91
|
-
id: 'test-1',
|
|
92
|
-
type: 'test',
|
|
93
|
-
message: { text: 'Hello' },
|
|
94
|
-
source: 'other-tab',
|
|
95
|
-
timestamp: Date.now(),
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
act(() => {
|
|
99
|
-
mockChannels[0].simulateMessage(message);
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
expect(screen.getByTestId('message-test-1')).toHaveTextContent('Hello');
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it('handles errors when BroadcastChannel is not supported', () => {
|
|
106
|
-
const originalBroadcastChannel = global.BroadcastChannel;
|
|
107
|
-
delete (global as any).BroadcastChannel;
|
|
108
|
-
|
|
109
|
-
render(
|
|
110
|
-
<BroadcastProvider channelName="test-channel">
|
|
111
|
-
<TestComponent />
|
|
112
|
-
</BroadcastProvider>
|
|
113
|
-
);
|
|
114
|
-
|
|
115
|
-
expect(screen.getByTestId('error')).toHaveTextContent(
|
|
116
|
-
'BroadcastChannel is not supported in this browser. Please check browser compatibility.'
|
|
117
|
-
);
|
|
118
|
-
|
|
119
|
-
// Restore BroadcastChannel for other tests
|
|
120
|
-
global.BroadcastChannel = originalBroadcastChannel;
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
it('cleans up channel on unmount', () => {
|
|
124
|
-
const { unmount } = render(
|
|
125
|
-
<BroadcastProvider channelName="test-channel">
|
|
126
|
-
<TestComponent />
|
|
127
|
-
</BroadcastProvider>
|
|
128
|
-
);
|
|
129
|
-
|
|
130
|
-
unmount();
|
|
131
|
-
|
|
132
|
-
expect(mockChannels[0].close).toHaveBeenCalled();
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
it('handles error boundary when child component throws', () => {
|
|
136
|
-
const ThrowComponent = () => {
|
|
137
|
-
throw new Error('Test error');
|
|
138
|
-
};
|
|
139
|
-
|
|
140
|
-
const consoleError = jest.spyOn(console, 'error').mockImplementation(() => {});
|
|
141
|
-
|
|
142
|
-
render(
|
|
143
|
-
<BroadcastProvider channelName="test-channel">
|
|
144
|
-
<ThrowComponent />
|
|
145
|
-
</BroadcastProvider>
|
|
146
|
-
);
|
|
147
|
-
|
|
148
|
-
expect(screen.getByText('Error occurred in BroadcastProvider')).toBeInTheDocument();
|
|
149
|
-
expect(consoleError).toHaveBeenCalledWith('BroadcastChannel error:', expect.any(Error));
|
|
150
|
-
|
|
151
|
-
consoleError.mockRestore();
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
it('throws error when useBroadcastProvider is used outside provider', () => {
|
|
155
|
-
const consoleError = jest.spyOn(console, 'error').mockImplementation(() => {});
|
|
156
|
-
|
|
157
|
-
expect(() => {
|
|
158
|
-
render(<TestComponent />);
|
|
159
|
-
}).toThrow('useBroadcastProvider must be used within a BroadcastProvider');
|
|
160
|
-
|
|
161
|
-
consoleError.mockRestore();
|
|
162
|
-
});
|
|
163
|
-
});
|
package/src/tests/debug.test.ts
DELETED
|
@@ -1,239 +0,0 @@
|
|
|
1
|
-
// Set debug mode before requiring the debug module
|
|
2
|
-
process.env.REACT_APP_DEBUG_BROADCAST = 'true';
|
|
3
|
-
import { debug } from '../utils/debug';
|
|
4
|
-
|
|
5
|
-
describe('debug', () => {
|
|
6
|
-
const originalEnv = process.env;
|
|
7
|
-
const originalConsole = {
|
|
8
|
-
log: console.log,
|
|
9
|
-
warn: console.warn,
|
|
10
|
-
error: console.error,
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
beforeEach(() => {
|
|
14
|
-
process.env = { ...originalEnv };
|
|
15
|
-
console.log = jest.fn();
|
|
16
|
-
console.warn = jest.fn();
|
|
17
|
-
console.error = jest.fn();
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
afterEach(() => {
|
|
21
|
-
console.log = originalConsole.log;
|
|
22
|
-
console.warn = originalConsole.warn;
|
|
23
|
-
console.error = originalConsole.error;
|
|
24
|
-
process.env = originalEnv;
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
describe('when debug mode is disabled', () => {
|
|
28
|
-
beforeEach(() => {
|
|
29
|
-
process.env.REACT_APP_DEBUG_BROADCAST = 'false';
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it('does not log anything', () => {
|
|
33
|
-
debug.channel.created('test-channel');
|
|
34
|
-
debug.message.sent({ id: '123' });
|
|
35
|
-
debug.error('test error');
|
|
36
|
-
|
|
37
|
-
expect(console.log).not.toHaveBeenCalled();
|
|
38
|
-
expect(console.warn).not.toHaveBeenCalled();
|
|
39
|
-
expect(console.error).not.toHaveBeenCalled();
|
|
40
|
-
});
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
describe('when debug mode is enabled', () => {
|
|
44
|
-
beforeEach(() => {
|
|
45
|
-
process.env.REACT_APP_DEBUG_BROADCAST = 'true';
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it('logs channel events', () => {
|
|
49
|
-
debug.channel.created('test-channel');
|
|
50
|
-
expect(console.log).toHaveBeenCalledWith(
|
|
51
|
-
expect.stringMatching(
|
|
52
|
-
/\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z\] \[react-broadcast-sync\]/
|
|
53
|
-
),
|
|
54
|
-
'Channel created:',
|
|
55
|
-
'test-channel'
|
|
56
|
-
);
|
|
57
|
-
|
|
58
|
-
debug.channel.closed('test-channel');
|
|
59
|
-
expect(console.log).toHaveBeenCalledWith(
|
|
60
|
-
expect.stringMatching(
|
|
61
|
-
/\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z\] \[react-broadcast-sync\]/
|
|
62
|
-
),
|
|
63
|
-
'Channel closed:',
|
|
64
|
-
'test-channel'
|
|
65
|
-
);
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it('logs message events', () => {
|
|
69
|
-
const testMessage = { id: '123', type: 'test' };
|
|
70
|
-
|
|
71
|
-
debug.message.sent(testMessage);
|
|
72
|
-
expect(console.log).toHaveBeenCalledWith(
|
|
73
|
-
expect.stringMatching(
|
|
74
|
-
/\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z\] \[react-broadcast-sync\]/
|
|
75
|
-
),
|
|
76
|
-
'Message sent:',
|
|
77
|
-
testMessage
|
|
78
|
-
);
|
|
79
|
-
|
|
80
|
-
debug.message.received(testMessage);
|
|
81
|
-
expect(console.log).toHaveBeenCalledWith(
|
|
82
|
-
expect.stringMatching(
|
|
83
|
-
/\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z\] \[react-broadcast-sync\]/
|
|
84
|
-
),
|
|
85
|
-
'Message received:',
|
|
86
|
-
testMessage
|
|
87
|
-
);
|
|
88
|
-
|
|
89
|
-
debug.message.cleared('123');
|
|
90
|
-
expect(console.log).toHaveBeenCalledWith(
|
|
91
|
-
expect.stringMatching(
|
|
92
|
-
/\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z\] \[react-broadcast-sync\]/
|
|
93
|
-
),
|
|
94
|
-
'Message cleared:',
|
|
95
|
-
'123'
|
|
96
|
-
);
|
|
97
|
-
|
|
98
|
-
debug.message.expired('123');
|
|
99
|
-
expect(console.log).toHaveBeenCalledWith(
|
|
100
|
-
expect.stringMatching(
|
|
101
|
-
/\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z\] \[react-broadcast-sync\]/
|
|
102
|
-
),
|
|
103
|
-
'Message expired:',
|
|
104
|
-
'123'
|
|
105
|
-
);
|
|
106
|
-
|
|
107
|
-
debug.message.duplicate('123');
|
|
108
|
-
expect(console.warn).toHaveBeenCalledWith(
|
|
109
|
-
expect.stringMatching(
|
|
110
|
-
/\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z\] \[react-broadcast-sync\]/
|
|
111
|
-
),
|
|
112
|
-
'Duplicate message ignored:',
|
|
113
|
-
'123'
|
|
114
|
-
);
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it('logs cleanup events', () => {
|
|
118
|
-
debug.cleanup.started();
|
|
119
|
-
expect(console.log).toHaveBeenCalledWith(
|
|
120
|
-
expect.stringMatching(
|
|
121
|
-
/\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z\] \[react-broadcast-sync\]/
|
|
122
|
-
),
|
|
123
|
-
'Cleanup started',
|
|
124
|
-
''
|
|
125
|
-
);
|
|
126
|
-
|
|
127
|
-
debug.cleanup.completed(5);
|
|
128
|
-
expect(console.log).toHaveBeenCalledWith(
|
|
129
|
-
expect.stringMatching(
|
|
130
|
-
/\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z\] \[react-broadcast-sync\]/
|
|
131
|
-
),
|
|
132
|
-
'Cleanup completed:',
|
|
133
|
-
{ removedCount: 5 }
|
|
134
|
-
);
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
it('logs errors', () => {
|
|
138
|
-
debug.error('test error');
|
|
139
|
-
expect(console.error).toHaveBeenCalledWith(
|
|
140
|
-
expect.stringMatching(
|
|
141
|
-
/\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z\] \[react-broadcast-sync\]/
|
|
142
|
-
),
|
|
143
|
-
'Error:',
|
|
144
|
-
'test error'
|
|
145
|
-
);
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
it('logs info with no data', () => {
|
|
149
|
-
debug.channel.created('test-channel');
|
|
150
|
-
expect(console.log).toHaveBeenCalledWith(
|
|
151
|
-
expect.stringMatching(/\[.*react-broadcast-sync\]/),
|
|
152
|
-
'Channel created:',
|
|
153
|
-
'test-channel'
|
|
154
|
-
);
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
it('logs warn with no data', () => {
|
|
158
|
-
debug.message.duplicate('123');
|
|
159
|
-
expect(console.warn).toHaveBeenCalledWith(
|
|
160
|
-
expect.stringMatching(/\[.*react-broadcast-sync\]/),
|
|
161
|
-
'Duplicate message ignored:',
|
|
162
|
-
'123'
|
|
163
|
-
);
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
it('logs error with no data', () => {
|
|
167
|
-
debug.error('error!');
|
|
168
|
-
expect(console.error).toHaveBeenCalledWith(
|
|
169
|
-
expect.stringMatching(/\[.*react-broadcast-sync\]/),
|
|
170
|
-
'Error:',
|
|
171
|
-
'error!'
|
|
172
|
-
);
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
it('logs message with no data', () => {
|
|
176
|
-
debug.cleanup.started();
|
|
177
|
-
expect(console.log).toHaveBeenCalledWith(
|
|
178
|
-
expect.stringContaining('[react-broadcast-sync]'),
|
|
179
|
-
'Cleanup started',
|
|
180
|
-
''
|
|
181
|
-
);
|
|
182
|
-
});
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
describe('when process.env is undefined', () => {
|
|
186
|
-
beforeEach(() => {
|
|
187
|
-
// @ts-expect-error - Testing invalid input
|
|
188
|
-
process.env = undefined;
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
it('does not log anything', () => {
|
|
192
|
-
debug.channel.created('test-channel');
|
|
193
|
-
expect(console.log).not.toHaveBeenCalled();
|
|
194
|
-
});
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
it('logs info messages', () => {
|
|
198
|
-
debug.channel.created('test-channel');
|
|
199
|
-
expect(console.log).toHaveBeenCalledWith(
|
|
200
|
-
expect.stringContaining('[react-broadcast-sync]'),
|
|
201
|
-
'Channel created:',
|
|
202
|
-
'test-channel'
|
|
203
|
-
);
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
it('logs warning messages', () => {
|
|
207
|
-
debug.message.duplicate('test-id');
|
|
208
|
-
expect(console.warn).toHaveBeenCalledWith(
|
|
209
|
-
expect.stringContaining('[react-broadcast-sync]'),
|
|
210
|
-
'Duplicate message ignored:',
|
|
211
|
-
'test-id'
|
|
212
|
-
);
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
it('logs error messages', () => {
|
|
216
|
-
debug.error('Test error');
|
|
217
|
-
expect(console.error).toHaveBeenCalledWith(
|
|
218
|
-
expect.stringContaining('[react-broadcast-sync]'),
|
|
219
|
-
'Error:',
|
|
220
|
-
'Test error'
|
|
221
|
-
);
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
it('logs cleanup information', () => {
|
|
225
|
-
debug.cleanup.started();
|
|
226
|
-
expect(console.log).toHaveBeenCalledWith(
|
|
227
|
-
expect.stringContaining('[react-broadcast-sync]'),
|
|
228
|
-
'Cleanup started',
|
|
229
|
-
''
|
|
230
|
-
);
|
|
231
|
-
|
|
232
|
-
debug.cleanup.completed(2);
|
|
233
|
-
expect(console.log).toHaveBeenCalledWith(
|
|
234
|
-
expect.stringContaining('[react-broadcast-sync]'),
|
|
235
|
-
'Cleanup completed:',
|
|
236
|
-
{ removedCount: 2 }
|
|
237
|
-
);
|
|
238
|
-
});
|
|
239
|
-
});
|
|
@@ -1,252 +0,0 @@
|
|
|
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_SENT_MESSAGES', 'chanA');
|
|
69
|
-
const two = getInternalMessageType('CLEAR_SENT_MESSAGES', 'chanB');
|
|
70
|
-
expect(one).not.toBe(two);
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it('getInternalMessageType generates consistent hash string', () => {
|
|
74
|
-
const type = getInternalMessageType('CLEAR_SENT_MESSAGES', 'my-channel', 'ns');
|
|
75
|
-
expect(type).toContain('__INTERNAL__');
|
|
76
|
-
expect(type).toContain('CLEAR_SENT_MESSAGES');
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it('isInternalType detects internal message types', () => {
|
|
80
|
-
const type = getInternalMessageType('CLEAR_SENT_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_SENT_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 = {
|
|
116
|
-
value: 0,
|
|
117
|
-
increment: function () {
|
|
118
|
-
this.value++;
|
|
119
|
-
},
|
|
120
|
-
};
|
|
121
|
-
const debounced = debounce(obj.increment.bind(obj), 100);
|
|
122
|
-
|
|
123
|
-
debounced();
|
|
124
|
-
debounced();
|
|
125
|
-
debounced.flush();
|
|
126
|
-
expect(obj.value).toBe(1);
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
it('debounce flush returns undefined when no pending call', () => {
|
|
130
|
-
const fn = jest.fn();
|
|
131
|
-
const debounced = debounce(fn, 100);
|
|
132
|
-
|
|
133
|
-
expect(debounced.flush()).toBeUndefined();
|
|
134
|
-
expect(fn).not.toHaveBeenCalled();
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
it('debounce handles function with return value', () => {
|
|
138
|
-
const fn = jest.fn().mockReturnValue('test');
|
|
139
|
-
const debounced = debounce(fn, 100);
|
|
140
|
-
|
|
141
|
-
debounced();
|
|
142
|
-
expect(debounced.flush()).toBe('test');
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
it('debounce.cancel cancels the timeout', () => {
|
|
146
|
-
const fn = jest.fn();
|
|
147
|
-
const debounced = debounce(fn, 100);
|
|
148
|
-
debounced();
|
|
149
|
-
debounced.cancel();
|
|
150
|
-
jest.advanceTimersByTime(200);
|
|
151
|
-
expect(fn).not.toHaveBeenCalled();
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
it('debounce.flush executes immediately', () => {
|
|
155
|
-
const fn = jest.fn();
|
|
156
|
-
const debounced = debounce(fn, 100);
|
|
157
|
-
debounced('test');
|
|
158
|
-
debounced.flush();
|
|
159
|
-
expect(fn).toHaveBeenCalledWith('test');
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
it('debounce handles multiple calls with different arguments', () => {
|
|
163
|
-
const fn = jest.fn();
|
|
164
|
-
const debounced = debounce(fn, 100);
|
|
165
|
-
|
|
166
|
-
debounced('first');
|
|
167
|
-
debounced('second');
|
|
168
|
-
debounced('third');
|
|
169
|
-
|
|
170
|
-
expect(fn).not.toHaveBeenCalled();
|
|
171
|
-
debounced.flush();
|
|
172
|
-
expect(fn).toHaveBeenCalledTimes(1);
|
|
173
|
-
expect(fn).toHaveBeenCalledWith('third');
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
it('debounce handles cancel and flush in sequence', () => {
|
|
177
|
-
const fn = jest.fn();
|
|
178
|
-
const debounced = debounce(fn, 100);
|
|
179
|
-
|
|
180
|
-
debounced('test');
|
|
181
|
-
debounced.cancel();
|
|
182
|
-
expect(debounced.flush()).toBeUndefined();
|
|
183
|
-
expect(fn).not.toHaveBeenCalled();
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
it('debounce.flush executes the function with latest arguments immediately', () => {
|
|
187
|
-
jest.useFakeTimers();
|
|
188
|
-
|
|
189
|
-
const fn = jest.fn((x: number) => x + 1);
|
|
190
|
-
const debounced = debounce(fn, 200);
|
|
191
|
-
|
|
192
|
-
debounced(41);
|
|
193
|
-
|
|
194
|
-
expect(fn).not.toHaveBeenCalled();
|
|
195
|
-
|
|
196
|
-
const flushResult = debounced.flush();
|
|
197
|
-
|
|
198
|
-
expect(fn).toHaveBeenCalledTimes(1);
|
|
199
|
-
expect(fn).toHaveBeenCalledWith(41);
|
|
200
|
-
expect(flushResult).toBe(42);
|
|
201
|
-
|
|
202
|
-
jest.useRealTimers();
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
it('debounce.flush does nothing if timeout is set but lastArgs were reset manually', () => {
|
|
206
|
-
const fn = jest.fn();
|
|
207
|
-
|
|
208
|
-
const debounced = debounce(fn, 100);
|
|
209
|
-
|
|
210
|
-
// Call once, schedule the debounce
|
|
211
|
-
debounced('first');
|
|
212
|
-
|
|
213
|
-
// Simulate internal cleanup (cannot access lastArgs directly, so instead cancel it)
|
|
214
|
-
debounced.cancel(); // This will clear lastArgs and lastThis but not call fn
|
|
215
|
-
|
|
216
|
-
const result = debounced.flush(); // Should do nothing
|
|
217
|
-
|
|
218
|
-
expect(fn).not.toHaveBeenCalled();
|
|
219
|
-
expect(result).toBeUndefined();
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
it('debounce.flush handles case where timeoutId exists but lastArgs is null', () => {
|
|
223
|
-
const fn = jest.fn();
|
|
224
|
-
const debounced = debounce(fn, 100);
|
|
225
|
-
|
|
226
|
-
// Schedule a call
|
|
227
|
-
debounced('test');
|
|
228
|
-
|
|
229
|
-
// Force the timeout to be set but clear lastArgs
|
|
230
|
-
jest.advanceTimersByTime(50); // Advance partially through the timeout
|
|
231
|
-
debounced.cancel(); // This clears lastArgs but might not clear timeoutId immediately
|
|
232
|
-
|
|
233
|
-
// Call flush when timeoutId exists but lastArgs is null
|
|
234
|
-
const result = debounced.flush();
|
|
235
|
-
expect(result).toBeUndefined();
|
|
236
|
-
expect(fn).not.toHaveBeenCalled();
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
it('debounce.flush handles multiple calls with different arguments and preserves return value', () => {
|
|
240
|
-
const fn = jest.fn((x: number) => x * 2);
|
|
241
|
-
const debounced = debounce(fn, 100);
|
|
242
|
-
|
|
243
|
-
debounced(1);
|
|
244
|
-
debounced(2);
|
|
245
|
-
debounced(3);
|
|
246
|
-
|
|
247
|
-
const result = debounced.flush();
|
|
248
|
-
expect(result).toBe(6); // 3 * 2
|
|
249
|
-
expect(fn).toHaveBeenCalledTimes(1);
|
|
250
|
-
expect(fn).toHaveBeenCalledWith(3);
|
|
251
|
-
});
|
|
252
|
-
});
|