request-iframe 0.0.1 → 0.0.2

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.
Files changed (68) hide show
  1. package/README.CN.md +2 -0
  2. package/README.md +2 -0
  3. package/library/__tests__/interceptors.test.ts +22 -0
  4. package/library/__tests__/requestIframe.test.ts +190 -0
  5. package/library/api/client.d.js +5 -0
  6. package/library/api/client.d.ts.map +1 -1
  7. package/library/api/server.d.js +5 -0
  8. package/library/api/server.d.ts.map +1 -1
  9. package/library/constants/index.d.js +36 -0
  10. package/library/constants/index.d.ts.map +1 -1
  11. package/library/constants/messages.d.js +5 -0
  12. package/library/constants/messages.d.ts.map +1 -1
  13. package/library/core/client.d.js +5 -0
  14. package/library/core/client.d.ts +16 -0
  15. package/library/core/client.d.ts.map +1 -1
  16. package/library/core/client.js +39 -0
  17. package/library/core/request.d.js +5 -0
  18. package/library/core/request.d.ts.map +1 -1
  19. package/library/core/response.d.js +5 -0
  20. package/library/core/response.d.ts.map +1 -1
  21. package/library/core/server-client.d.js +5 -0
  22. package/library/core/server-client.d.ts.map +1 -1
  23. package/library/core/server.d.js +5 -0
  24. package/library/core/server.d.ts +2 -2
  25. package/library/core/server.d.ts.map +1 -1
  26. package/library/core/server.js +16 -2
  27. package/library/interceptors/index.d.js +5 -0
  28. package/library/interceptors/index.d.ts +4 -0
  29. package/library/interceptors/index.d.ts.map +1 -1
  30. package/library/interceptors/index.js +7 -0
  31. package/library/message/channel.d.js +5 -0
  32. package/library/message/channel.d.ts.map +1 -1
  33. package/library/message/dispatcher.d.js +5 -0
  34. package/library/message/dispatcher.d.ts.map +1 -1
  35. package/library/message/index.d.js +25 -0
  36. package/library/message/index.d.ts.map +1 -1
  37. package/library/stream/file-stream.d.js +4 -0
  38. package/library/stream/file-stream.d.ts.map +1 -1
  39. package/library/stream/index.d.js +58 -0
  40. package/library/stream/index.d.ts.map +1 -1
  41. package/library/stream/readable-stream.d.js +5 -0
  42. package/library/stream/readable-stream.d.ts.map +1 -1
  43. package/library/stream/types.d.js +5 -0
  44. package/library/stream/types.d.ts.map +1 -1
  45. package/library/stream/writable-stream.d.js +5 -0
  46. package/library/stream/writable-stream.d.ts.map +1 -1
  47. package/library/types/index.d.js +5 -0
  48. package/library/types/index.d.ts +12 -4
  49. package/library/types/index.d.ts.map +1 -1
  50. package/library/utils/cache.d.js +5 -0
  51. package/library/utils/cache.d.ts.map +1 -1
  52. package/library/utils/cookie.d.js +5 -0
  53. package/library/utils/cookie.d.ts.map +1 -1
  54. package/library/utils/debug.d.js +5 -0
  55. package/library/utils/debug.d.ts.map +1 -1
  56. package/library/utils/index.d.js +94 -0
  57. package/library/utils/index.d.ts.map +1 -1
  58. package/library/utils/path-match.d.js +5 -0
  59. package/library/utils/path-match.d.ts.map +1 -1
  60. package/library/utils/protocol.d.js +5 -0
  61. package/library/utils/protocol.d.ts.map +1 -1
  62. package/package.json +14 -2
  63. package/react/library/__tests__/index.test.d.ts +2 -0
  64. package/react/library/__tests__/index.test.d.ts.map +1 -0
  65. package/react/library/__tests__/index.test.tsx +799 -0
  66. package/react/library/index.d.ts +117 -0
  67. package/react/library/index.d.ts.map +1 -0
  68. package/react/library/index.js +223 -0
@@ -0,0 +1,799 @@
1
+ import { renderHook, waitFor } from '@testing-library/react';
2
+ import { useRef } from 'react';
3
+ import { useClient, useServer, useServerHandler, useServerHandlerMap } from '../index';
4
+ import { requestIframeClient, clearRequestIframeClientCache } from '../../../library/api/client';
5
+ import { requestIframeServer, clearRequestIframeServerCache } from '../../../library/api/server';
6
+
7
+ /**
8
+ * Create test iframe
9
+ */
10
+ function createTestIframe(origin: string): HTMLIFrameElement {
11
+ const iframe = document.createElement('iframe');
12
+ iframe.src = `${origin}/test.html`;
13
+ document.body.appendChild(iframe);
14
+ return iframe;
15
+ }
16
+
17
+ /**
18
+ * Cleanup test iframe
19
+ */
20
+ function cleanupIframe(iframe: HTMLIFrameElement): void {
21
+ if (iframe.parentNode) {
22
+ iframe.parentNode.removeChild(iframe);
23
+ }
24
+ }
25
+
26
+ describe('React Hooks', () => {
27
+ beforeEach(() => {
28
+ clearRequestIframeClientCache();
29
+ clearRequestIframeServerCache();
30
+ document.querySelectorAll('iframe').forEach((iframe) => {
31
+ if (iframe.parentNode) {
32
+ iframe.parentNode.removeChild(iframe);
33
+ }
34
+ });
35
+ });
36
+
37
+ afterEach(() => {
38
+ clearRequestIframeClientCache();
39
+ clearRequestIframeServerCache();
40
+ document.querySelectorAll('iframe').forEach((iframe) => {
41
+ if (iframe.parentNode) {
42
+ iframe.parentNode.removeChild(iframe);
43
+ }
44
+ });
45
+ });
46
+
47
+ describe('useClient', () => {
48
+ it('should return null when getTarget returns null', () => {
49
+ const { result } = renderHook(() => useClient(() => null));
50
+ expect(result.current).toBeNull();
51
+ });
52
+
53
+ it('should create client when getTarget returns valid target', async () => {
54
+ const iframe = createTestIframe('https://example.com');
55
+ const mockContentWindow = {
56
+ postMessage: jest.fn()
57
+ };
58
+ Object.defineProperty(iframe, 'contentWindow', {
59
+ value: mockContentWindow,
60
+ writable: true,
61
+ configurable: true
62
+ });
63
+
64
+ const { result } = renderHook(() => useClient(() => iframe));
65
+
66
+ await waitFor(() => {
67
+ expect(result.current).toBeDefined();
68
+ expect(result.current).not.toBeNull();
69
+ }, { timeout: 2000 });
70
+
71
+ cleanupIframe(iframe);
72
+ });
73
+
74
+ it('should create client with options', async () => {
75
+ const iframe = createTestIframe('https://example.com');
76
+ const mockContentWindow = {
77
+ postMessage: jest.fn()
78
+ };
79
+ Object.defineProperty(iframe, 'contentWindow', {
80
+ value: mockContentWindow,
81
+ writable: true,
82
+ configurable: true
83
+ });
84
+
85
+ const options = { secretKey: 'test-key', timeout: 1000 };
86
+ const { result } = renderHook(() => useClient(() => iframe, options));
87
+
88
+ await waitFor(() => {
89
+ expect(result.current).toBeDefined();
90
+ if (result.current) {
91
+ expect(result.current.isOpen).toBe(true);
92
+ }
93
+ }, { timeout: 2000 });
94
+
95
+ cleanupIframe(iframe);
96
+ });
97
+
98
+ it('should destroy client on unmount', async () => {
99
+ const iframe = createTestIframe('https://example.com');
100
+ const mockContentWindow = {
101
+ postMessage: jest.fn()
102
+ };
103
+ Object.defineProperty(iframe, 'contentWindow', {
104
+ value: mockContentWindow,
105
+ writable: true,
106
+ configurable: true
107
+ });
108
+
109
+ const { result, unmount } = renderHook(() => useClient(() => iframe));
110
+
111
+ await waitFor(() => {
112
+ expect(result.current).toBeDefined();
113
+ }, { timeout: 2000 });
114
+
115
+ const client = result.current;
116
+ expect(client).toBeDefined();
117
+
118
+ unmount();
119
+
120
+ // Client should be destroyed
121
+ if (client) {
122
+ expect(client.isOpen).toBe(false);
123
+ }
124
+
125
+ cleanupIframe(iframe);
126
+ });
127
+
128
+ it('should recreate client when getTarget function changes', async () => {
129
+ const iframe1 = createTestIframe('https://example.com');
130
+ const mockContentWindow1 = {
131
+ postMessage: jest.fn()
132
+ };
133
+ Object.defineProperty(iframe1, 'contentWindow', {
134
+ value: mockContentWindow1,
135
+ writable: true,
136
+ configurable: true
137
+ });
138
+
139
+ const { result, rerender } = renderHook(
140
+ (props: { getTarget: () => HTMLIFrameElement | Window | null }) => useClient(props.getTarget),
141
+ { initialProps: { getTarget: () => iframe1 } }
142
+ );
143
+
144
+ await waitFor(() => {
145
+ expect(result.current).toBeDefined();
146
+ }, { timeout: 2000 });
147
+
148
+ const client1 = result.current;
149
+ expect(client1).toBeDefined();
150
+
151
+ const iframe2 = createTestIframe('https://example2.com');
152
+ const mockContentWindow2 = {
153
+ postMessage: jest.fn()
154
+ };
155
+ Object.defineProperty(iframe2, 'contentWindow', {
156
+ value: mockContentWindow2,
157
+ writable: true,
158
+ configurable: true
159
+ });
160
+
161
+ rerender({ getTarget: () => iframe2 });
162
+
163
+ await waitFor(() => {
164
+ // Previous client should be destroyed
165
+ if (client1) {
166
+ expect(client1.isOpen).toBe(false);
167
+ }
168
+ // New client should be created
169
+ expect(result.current).toBeDefined();
170
+ expect(result.current).not.toBe(client1);
171
+ }, { timeout: 2000 });
172
+
173
+ cleanupIframe(iframe1);
174
+ cleanupIframe(iframe2);
175
+ });
176
+
177
+ it('should handle getTarget returning null after initial mount', async () => {
178
+ const iframe = createTestIframe('https://example.com');
179
+ const mockContentWindow = {
180
+ postMessage: jest.fn()
181
+ };
182
+ Object.defineProperty(iframe, 'contentWindow', {
183
+ value: mockContentWindow,
184
+ writable: true,
185
+ configurable: true
186
+ });
187
+
188
+ type Props = { getTarget: () => HTMLIFrameElement | Window | null };
189
+ const { result, rerender } = renderHook(
190
+ (props: Props) => useClient(props.getTarget),
191
+ { initialProps: { getTarget: () => iframe } as Props }
192
+ );
193
+
194
+ await waitFor(() => {
195
+ expect(result.current).toBeDefined();
196
+ }, { timeout: 2000 });
197
+
198
+ // Change getTarget to return null
199
+ rerender({ getTarget: () => null } as Props);
200
+
201
+ // Wait for cleanup
202
+ await new Promise(resolve => setTimeout(resolve, 100));
203
+
204
+ // Note: clientRef.current may still hold the old client until next render
205
+ // This is expected behavior with useRef - the component needs to re-render
206
+ // to reflect the change. In real usage, this would trigger a re-render.
207
+
208
+ cleanupIframe(iframe);
209
+ });
210
+
211
+ it('should work with function pattern', async () => {
212
+ const iframeRef = { current: null as HTMLIFrameElement | null };
213
+ const iframe = createTestIframe('https://example.com');
214
+ const mockContentWindow = {
215
+ postMessage: jest.fn()
216
+ };
217
+ Object.defineProperty(iframe, 'contentWindow', {
218
+ value: mockContentWindow,
219
+ writable: true,
220
+ configurable: true
221
+ });
222
+
223
+ const { result, rerender } = renderHook(() => useClient(() => iframeRef.current));
224
+
225
+ // Initially null (ref not set)
226
+ expect(result.current).toBeNull();
227
+
228
+ // Set ref
229
+ iframeRef.current = iframe;
230
+ rerender();
231
+
232
+ await waitFor(() => {
233
+ expect(result.current).toBeDefined();
234
+ }, { timeout: 2000 });
235
+
236
+ cleanupIframe(iframe);
237
+ });
238
+
239
+ it('should work with ref object directly', async () => {
240
+ const iframe = createTestIframe('https://example.com');
241
+ const mockContentWindow = {
242
+ postMessage: jest.fn()
243
+ };
244
+ Object.defineProperty(iframe, 'contentWindow', {
245
+ value: mockContentWindow,
246
+ writable: true,
247
+ configurable: true
248
+ });
249
+
250
+ const { result } = renderHook(() => {
251
+ const iframeRef = useRef<HTMLIFrameElement | null>(iframe);
252
+ return useClient(iframeRef);
253
+ });
254
+
255
+ await waitFor(() => {
256
+ expect(result.current).toBeDefined();
257
+ expect(result.current).not.toBeNull();
258
+ }, { timeout: 2000 });
259
+
260
+ cleanupIframe(iframe);
261
+ });
262
+
263
+ it('should recreate client when deps change', async () => {
264
+ const iframe = createTestIframe('https://example.com');
265
+ const mockContentWindow = {
266
+ postMessage: jest.fn()
267
+ };
268
+ Object.defineProperty(iframe, 'contentWindow', {
269
+ value: mockContentWindow,
270
+ writable: true,
271
+ configurable: true
272
+ });
273
+
274
+ let userId = 1;
275
+ const { result, rerender } = renderHook(() => {
276
+ return useClient(() => iframe, { secretKey: `key-${userId}` }, [userId]);
277
+ });
278
+
279
+ await waitFor(() => {
280
+ expect(result.current).toBeDefined();
281
+ }, { timeout: 2000 });
282
+
283
+ const client1 = result.current;
284
+
285
+ // Change dependency
286
+ userId = 2;
287
+ rerender();
288
+
289
+ await waitFor(() => {
290
+ // Previous client should be destroyed
291
+ if (client1) {
292
+ expect(client1.isOpen).toBe(false);
293
+ }
294
+ // New client should be created
295
+ expect(result.current).toBeDefined();
296
+ expect(result.current).not.toBe(client1);
297
+ }, { timeout: 2000 });
298
+
299
+ cleanupIframe(iframe);
300
+ });
301
+ });
302
+
303
+ describe('useServer', () => {
304
+ it('should create server instance', () => {
305
+ const { result } = renderHook(() => useServer());
306
+
307
+ expect(result.current).toBeDefined();
308
+ if (result.current) {
309
+ expect(result.current.isOpen).toBe(true);
310
+ }
311
+ });
312
+
313
+ it('should create server with options', () => {
314
+ const options = { secretKey: 'test-key', ackTimeout: 1000 };
315
+ const { result } = renderHook(() => useServer(options));
316
+
317
+ expect(result.current).toBeDefined();
318
+ if (result.current) {
319
+ expect(result.current.secretKey).toBe('test-key');
320
+ }
321
+ });
322
+
323
+ it('should destroy server on unmount', async () => {
324
+ const { result, unmount } = renderHook(() => useServer());
325
+ const server = result.current;
326
+
327
+ expect(server).toBeDefined();
328
+
329
+ unmount();
330
+
331
+ // Server should be destroyed
332
+ if (server) {
333
+ expect(server.isOpen).toBe(false);
334
+ }
335
+ });
336
+
337
+ it('should create server only once on mount', () => {
338
+ const { result, rerender } = renderHook(() => useServer());
339
+ const server1 = result.current;
340
+
341
+ expect(server1).toBeDefined();
342
+
343
+ rerender();
344
+
345
+ // Should return the same instance (not null)
346
+ expect(result.current).toBeDefined();
347
+ expect(result.current).toBe(server1);
348
+ });
349
+
350
+ it('should recreate server when deps change', async () => {
351
+ let userId = 1;
352
+ const { result, rerender } = renderHook(() => {
353
+ return useServer({ secretKey: `key-${userId}` }, [userId]);
354
+ });
355
+
356
+ const server1 = result.current;
357
+ expect(server1).toBeDefined();
358
+
359
+ // Change dependency
360
+ userId = 2;
361
+ rerender();
362
+
363
+ await waitFor(() => {
364
+ // Previous server should be destroyed
365
+ if (server1) {
366
+ expect(server1.isOpen).toBe(false);
367
+ }
368
+ // New server should be created
369
+ expect(result.current).toBeDefined();
370
+ expect(result.current).not.toBe(server1);
371
+ }, { timeout: 2000 });
372
+ });
373
+ });
374
+
375
+ describe('useServerHandler', () => {
376
+ it('should register handler when server is available', async () => {
377
+ const origin = 'https://example.com';
378
+ const iframe = createTestIframe(origin);
379
+ const mockContentWindow = {
380
+ postMessage: jest.fn((msg: any) => {
381
+ if (msg.type === 'request') {
382
+ // Send ACK
383
+ window.dispatchEvent(
384
+ new MessageEvent('message', {
385
+ data: {
386
+ __requestIframe__: 1,
387
+ type: 'ack',
388
+ requestId: msg.requestId,
389
+ path: msg.path,
390
+ timestamp: Date.now()
391
+ },
392
+ origin,
393
+ source: mockContentWindow as any
394
+ })
395
+ );
396
+ // Send response
397
+ setTimeout(() => {
398
+ window.dispatchEvent(
399
+ new MessageEvent('message', {
400
+ data: {
401
+ __requestIframe__: 1,
402
+ type: 'response',
403
+ requestId: msg.requestId,
404
+ data: { success: true },
405
+ status: 200,
406
+ statusText: 'OK',
407
+ timestamp: Date.now()
408
+ },
409
+ origin,
410
+ source: mockContentWindow as any
411
+ })
412
+ );
413
+ }, 10);
414
+ }
415
+ })
416
+ };
417
+ Object.defineProperty(iframe, 'contentWindow', {
418
+ value: mockContentWindow,
419
+ writable: true,
420
+ configurable: true
421
+ });
422
+
423
+ const handler = jest.fn((req, res) => {
424
+ res.send({ success: true });
425
+ });
426
+
427
+ renderHook(() => {
428
+ const server = useServer();
429
+ useServerHandler(server, 'api/test', handler);
430
+ });
431
+
432
+ // Wait for handler to be registered
433
+ await new Promise(resolve => setTimeout(resolve, 100));
434
+
435
+ // Send a test request
436
+ const client = requestIframeClient(iframe);
437
+ await client.send('api/test', {});
438
+
439
+ await waitFor(() => {
440
+ expect(handler).toHaveBeenCalled();
441
+ }, { timeout: 3000 });
442
+
443
+ client.destroy();
444
+ cleanupIframe(iframe);
445
+ });
446
+
447
+ it('should not register handler when server is null', () => {
448
+ const handler = jest.fn();
449
+
450
+ renderHook(() => {
451
+ useServerHandler(null, 'api/test', handler);
452
+ });
453
+
454
+ // Should not throw
455
+ expect(handler).not.toHaveBeenCalled();
456
+ });
457
+
458
+ it('should unregister handler on unmount', async () => {
459
+ const origin = 'https://example.com';
460
+ const iframe = createTestIframe(origin);
461
+ const mockContentWindow = {
462
+ postMessage: jest.fn()
463
+ };
464
+ Object.defineProperty(iframe, 'contentWindow', {
465
+ value: mockContentWindow,
466
+ writable: true,
467
+ configurable: true
468
+ });
469
+
470
+ const handler = jest.fn();
471
+
472
+ const { unmount } = renderHook(() => {
473
+ const server = useServer();
474
+ useServerHandler(server, 'api/test', handler);
475
+ });
476
+
477
+ unmount();
478
+
479
+ // Handler should be unregistered
480
+ const client = requestIframeClient(iframe);
481
+ const server = requestIframeServer();
482
+
483
+ // Try to send request - should get METHOD_NOT_FOUND
484
+ client.send('api/test', {}).catch(() => {});
485
+
486
+ await waitFor(() => {
487
+ expect(handler).not.toHaveBeenCalled();
488
+ }, { timeout: 1000 });
489
+
490
+ client.destroy();
491
+ server.destroy();
492
+ cleanupIframe(iframe);
493
+ });
494
+
495
+ it('should re-register handler when dependencies change', async () => {
496
+ const origin = 'https://example.com';
497
+ const iframe = createTestIframe(origin);
498
+ const mockContentWindow = {
499
+ postMessage: jest.fn((msg: any) => {
500
+ if (msg.type === 'request') {
501
+ window.dispatchEvent(
502
+ new MessageEvent('message', {
503
+ data: {
504
+ __requestIframe__: 1,
505
+ type: 'ack',
506
+ requestId: msg.requestId,
507
+ path: msg.path,
508
+ timestamp: Date.now()
509
+ },
510
+ origin,
511
+ source: mockContentWindow as any
512
+ })
513
+ );
514
+ setTimeout(() => {
515
+ window.dispatchEvent(
516
+ new MessageEvent('message', {
517
+ data: {
518
+ __requestIframe__: 1,
519
+ type: 'response',
520
+ requestId: msg.requestId,
521
+ data: { success: true },
522
+ status: 200,
523
+ statusText: 'OK',
524
+ timestamp: Date.now()
525
+ },
526
+ origin,
527
+ source: mockContentWindow as any
528
+ })
529
+ );
530
+ }, 10);
531
+ }
532
+ })
533
+ };
534
+ Object.defineProperty(iframe, 'contentWindow', {
535
+ value: mockContentWindow,
536
+ writable: true,
537
+ configurable: true
538
+ });
539
+
540
+ let userId = 1;
541
+ const handler = jest.fn((req, res) => {
542
+ res.send({ userId });
543
+ });
544
+
545
+ const { rerender } = renderHook(() => {
546
+ const server = useServer();
547
+ useServerHandler(server, 'api/test', handler, [userId]);
548
+ });
549
+
550
+ // Wait for initial registration
551
+ await new Promise(resolve => setTimeout(resolve, 100));
552
+
553
+ const client = requestIframeClient(iframe);
554
+ await client.send('api/test', {});
555
+
556
+ await waitFor(() => {
557
+ expect(handler).toHaveBeenCalledTimes(1);
558
+ }, { timeout: 3000 });
559
+
560
+ // Change dependency
561
+ userId = 2;
562
+ rerender();
563
+
564
+ // Wait for re-registration
565
+ await new Promise(resolve => setTimeout(resolve, 100));
566
+
567
+ await client.send('api/test', {});
568
+
569
+ await waitFor(() => {
570
+ expect(handler).toHaveBeenCalledTimes(2);
571
+ }, { timeout: 3000 });
572
+
573
+ client.destroy();
574
+ cleanupIframe(iframe);
575
+ });
576
+ });
577
+
578
+ describe('useServerHandlerMap', () => {
579
+ it('should register handlers using map when server is available', async () => {
580
+ const origin = 'https://example.com';
581
+ const iframe = createTestIframe(origin);
582
+ const mockContentWindow = {
583
+ postMessage: jest.fn((msg: any) => {
584
+ if (msg.type === 'request') {
585
+ window.dispatchEvent(
586
+ new MessageEvent('message', {
587
+ data: {
588
+ __requestIframe__: 1,
589
+ type: 'ack',
590
+ requestId: msg.requestId,
591
+ path: msg.path,
592
+ timestamp: Date.now()
593
+ },
594
+ origin,
595
+ source: mockContentWindow as any
596
+ })
597
+ );
598
+ setTimeout(() => {
599
+ window.dispatchEvent(
600
+ new MessageEvent('message', {
601
+ data: {
602
+ __requestIframe__: 1,
603
+ type: 'response',
604
+ requestId: msg.requestId,
605
+ data: { success: true },
606
+ status: 200,
607
+ statusText: 'OK',
608
+ timestamp: Date.now()
609
+ },
610
+ origin,
611
+ source: mockContentWindow as any
612
+ })
613
+ );
614
+ }, 10);
615
+ }
616
+ })
617
+ };
618
+ Object.defineProperty(iframe, 'contentWindow', {
619
+ value: mockContentWindow,
620
+ writable: true,
621
+ configurable: true
622
+ });
623
+
624
+ const handlers = {
625
+ 'api/user': jest.fn((req, res) => res.send({ user: 'test' })),
626
+ 'api/post': jest.fn((req, res) => res.send({ post: 'test' }))
627
+ };
628
+
629
+ renderHook(() => {
630
+ const server = useServer();
631
+ useServerHandlerMap(server, handlers);
632
+ });
633
+
634
+ // Wait for handlers to be registered
635
+ await new Promise(resolve => setTimeout(resolve, 100));
636
+
637
+ // Send test requests
638
+ const client = requestIframeClient(iframe);
639
+ await client.send('api/user', {});
640
+ await client.send('api/post', {});
641
+
642
+ await waitFor(() => {
643
+ expect(handlers['api/user']).toHaveBeenCalled();
644
+ expect(handlers['api/post']).toHaveBeenCalled();
645
+ }, { timeout: 3000 });
646
+
647
+ client.destroy();
648
+ cleanupIframe(iframe);
649
+ });
650
+
651
+ it('should not register handlers when server is null', () => {
652
+ const handlers = {
653
+ 'api/user': jest.fn()
654
+ };
655
+
656
+ renderHook(() => {
657
+ useServerHandlerMap(null, handlers);
658
+ });
659
+
660
+ // Should not throw
661
+ expect(handlers['api/user']).not.toHaveBeenCalled();
662
+ });
663
+
664
+ it('should unregister all handlers on unmount', async () => {
665
+ const origin = 'https://example.com';
666
+ const iframe = createTestIframe(origin);
667
+ const mockContentWindow = {
668
+ postMessage: jest.fn()
669
+ };
670
+ Object.defineProperty(iframe, 'contentWindow', {
671
+ value: mockContentWindow,
672
+ writable: true,
673
+ configurable: true
674
+ });
675
+
676
+ const handlers = {
677
+ 'api/user': jest.fn(),
678
+ 'api/post': jest.fn()
679
+ };
680
+
681
+ const { unmount } = renderHook(() => {
682
+ const server = useServer();
683
+ useServerHandlerMap(server, handlers);
684
+ });
685
+
686
+ unmount();
687
+
688
+ // Handlers should be unregistered
689
+ const client = requestIframeClient(iframe);
690
+
691
+ // Try to send requests - should get METHOD_NOT_FOUND
692
+ client.send('api/user', {}).catch(() => {});
693
+ client.send('api/post', {}).catch(() => {});
694
+
695
+ await waitFor(() => {
696
+ expect(handlers['api/user']).not.toHaveBeenCalled();
697
+ expect(handlers['api/post']).not.toHaveBeenCalled();
698
+ }, { timeout: 1000 });
699
+
700
+ client.destroy();
701
+ cleanupIframe(iframe);
702
+ });
703
+
704
+ it('should re-register handlers when dependencies change', async () => {
705
+ const origin = 'https://example.com';
706
+ const iframe = createTestIframe(origin);
707
+ const mockContentWindow = {
708
+ postMessage: jest.fn((msg: any) => {
709
+ if (msg.type === 'request') {
710
+ window.dispatchEvent(
711
+ new MessageEvent('message', {
712
+ data: {
713
+ __requestIframe__: 1,
714
+ type: 'ack',
715
+ requestId: msg.requestId,
716
+ path: msg.path,
717
+ timestamp: Date.now()
718
+ },
719
+ origin,
720
+ source: mockContentWindow as any
721
+ })
722
+ );
723
+ setTimeout(() => {
724
+ window.dispatchEvent(
725
+ new MessageEvent('message', {
726
+ data: {
727
+ __requestIframe__: 1,
728
+ type: 'response',
729
+ requestId: msg.requestId,
730
+ data: { success: true },
731
+ status: 200,
732
+ statusText: 'OK',
733
+ timestamp: Date.now()
734
+ },
735
+ origin,
736
+ source: mockContentWindow as any
737
+ })
738
+ );
739
+ }, 10);
740
+ }
741
+ })
742
+ };
743
+ Object.defineProperty(iframe, 'contentWindow', {
744
+ value: mockContentWindow,
745
+ writable: true,
746
+ configurable: true
747
+ });
748
+
749
+ const handlers = {
750
+ 'api/user': jest.fn((req, res) => res.send({}))
751
+ };
752
+ let userId = 1;
753
+
754
+ const { rerender } = renderHook(() => {
755
+ const server = useServer();
756
+ useServerHandlerMap(server, handlers, [userId]);
757
+ });
758
+
759
+ // Wait for initial registration
760
+ await new Promise(resolve => setTimeout(resolve, 100));
761
+
762
+ const client = requestIframeClient(iframe);
763
+ await client.send('api/user', {});
764
+
765
+ await waitFor(() => {
766
+ expect(handlers['api/user']).toHaveBeenCalledTimes(1);
767
+ }, { timeout: 3000 });
768
+
769
+ // Change dependency
770
+ userId = 2;
771
+ rerender();
772
+
773
+ // Wait for re-registration
774
+ await new Promise(resolve => setTimeout(resolve, 100));
775
+
776
+ await client.send('api/user', {});
777
+
778
+ await waitFor(() => {
779
+ expect(handlers['api/user']).toHaveBeenCalledTimes(2);
780
+ }, { timeout: 3000 });
781
+
782
+ client.destroy();
783
+ cleanupIframe(iframe);
784
+ });
785
+
786
+ it('should handle empty handlers map', () => {
787
+ const handlers = {};
788
+
789
+ const { result } = renderHook(() => {
790
+ const server = useServer();
791
+ useServerHandlerMap(server, handlers);
792
+ return server;
793
+ });
794
+
795
+ // Should not throw
796
+ expect(result.current).toBeDefined();
797
+ });
798
+ });
799
+ });