request-iframe 0.0.1

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 (96) hide show
  1. package/QUICKSTART.CN.md +269 -0
  2. package/QUICKSTART.md +269 -0
  3. package/README.CN.md +1369 -0
  4. package/README.md +1016 -0
  5. package/library/__tests__/interceptors.test.ts +124 -0
  6. package/library/__tests__/requestIframe.test.ts +2216 -0
  7. package/library/__tests__/stream.test.ts +650 -0
  8. package/library/__tests__/utils.test.ts +433 -0
  9. package/library/api/client.d.ts +16 -0
  10. package/library/api/client.d.ts.map +1 -0
  11. package/library/api/client.js +72 -0
  12. package/library/api/server.d.ts +16 -0
  13. package/library/api/server.d.ts.map +1 -0
  14. package/library/api/server.js +44 -0
  15. package/library/constants/index.d.ts +209 -0
  16. package/library/constants/index.d.ts.map +1 -0
  17. package/library/constants/index.js +260 -0
  18. package/library/constants/messages.d.ts +80 -0
  19. package/library/constants/messages.d.ts.map +1 -0
  20. package/library/constants/messages.js +123 -0
  21. package/library/core/client.d.ts +99 -0
  22. package/library/core/client.d.ts.map +1 -0
  23. package/library/core/client.js +440 -0
  24. package/library/core/message-handler.d.ts +110 -0
  25. package/library/core/message-handler.d.ts.map +1 -0
  26. package/library/core/message-handler.js +320 -0
  27. package/library/core/request-response.d.ts +59 -0
  28. package/library/core/request-response.d.ts.map +1 -0
  29. package/library/core/request-response.js +337 -0
  30. package/library/core/request.d.ts +17 -0
  31. package/library/core/request.d.ts.map +1 -0
  32. package/library/core/request.js +34 -0
  33. package/library/core/response.d.ts +51 -0
  34. package/library/core/response.d.ts.map +1 -0
  35. package/library/core/response.js +323 -0
  36. package/library/core/server-base.d.ts +86 -0
  37. package/library/core/server-base.d.ts.map +1 -0
  38. package/library/core/server-base.js +257 -0
  39. package/library/core/server-client.d.ts +99 -0
  40. package/library/core/server-client.d.ts.map +1 -0
  41. package/library/core/server-client.js +256 -0
  42. package/library/core/server.d.ts +82 -0
  43. package/library/core/server.d.ts.map +1 -0
  44. package/library/core/server.js +338 -0
  45. package/library/index.d.ts +16 -0
  46. package/library/index.d.ts.map +1 -0
  47. package/library/index.js +211 -0
  48. package/library/interceptors/index.d.ts +41 -0
  49. package/library/interceptors/index.d.ts.map +1 -0
  50. package/library/interceptors/index.js +126 -0
  51. package/library/message/channel.d.ts +107 -0
  52. package/library/message/channel.d.ts.map +1 -0
  53. package/library/message/channel.js +184 -0
  54. package/library/message/dispatcher.d.ts +119 -0
  55. package/library/message/dispatcher.d.ts.map +1 -0
  56. package/library/message/dispatcher.js +249 -0
  57. package/library/message/index.d.ts +5 -0
  58. package/library/message/index.d.ts.map +1 -0
  59. package/library/message/index.js +25 -0
  60. package/library/stream/file-stream.d.ts +48 -0
  61. package/library/stream/file-stream.d.ts.map +1 -0
  62. package/library/stream/file-stream.js +240 -0
  63. package/library/stream/index.d.ts +15 -0
  64. package/library/stream/index.d.ts.map +1 -0
  65. package/library/stream/index.js +83 -0
  66. package/library/stream/readable-stream.d.ts +83 -0
  67. package/library/stream/readable-stream.d.ts.map +1 -0
  68. package/library/stream/readable-stream.js +249 -0
  69. package/library/stream/types.d.ts +165 -0
  70. package/library/stream/types.d.ts.map +1 -0
  71. package/library/stream/types.js +5 -0
  72. package/library/stream/writable-stream.d.ts +60 -0
  73. package/library/stream/writable-stream.d.ts.map +1 -0
  74. package/library/stream/writable-stream.js +348 -0
  75. package/library/types/index.d.ts +408 -0
  76. package/library/types/index.d.ts.map +1 -0
  77. package/library/types/index.js +5 -0
  78. package/library/utils/cache.d.ts +19 -0
  79. package/library/utils/cache.d.ts.map +1 -0
  80. package/library/utils/cache.js +83 -0
  81. package/library/utils/cookie.d.ts +117 -0
  82. package/library/utils/cookie.d.ts.map +1 -0
  83. package/library/utils/cookie.js +365 -0
  84. package/library/utils/debug.d.ts +11 -0
  85. package/library/utils/debug.d.ts.map +1 -0
  86. package/library/utils/debug.js +162 -0
  87. package/library/utils/index.d.ts +13 -0
  88. package/library/utils/index.d.ts.map +1 -0
  89. package/library/utils/index.js +132 -0
  90. package/library/utils/path-match.d.ts +17 -0
  91. package/library/utils/path-match.d.ts.map +1 -0
  92. package/library/utils/path-match.js +90 -0
  93. package/library/utils/protocol.d.ts +61 -0
  94. package/library/utils/protocol.d.ts.map +1 -0
  95. package/library/utils/protocol.js +169 -0
  96. package/package.json +58 -0
@@ -0,0 +1,2216 @@
1
+ import { requestIframeClient, clearRequestIframeClientCache } from '../api/client';
2
+ import { requestIframeServer, clearRequestIframeServerCache } from '../api/server';
3
+ import { RequestConfig, Response, ErrorResponse, PostMessageData } from '../types';
4
+ import { HttpHeader, Messages } from '../constants';
5
+
6
+ /**
7
+ * Create test iframe
8
+ */
9
+ function createTestIframe(origin: string): HTMLIFrameElement {
10
+ const iframe = document.createElement('iframe');
11
+ iframe.src = `${origin}/test.html`;
12
+ document.body.appendChild(iframe);
13
+ return iframe;
14
+ }
15
+
16
+ /**
17
+ * Cleanup test iframe
18
+ */
19
+ function cleanupIframe(iframe: HTMLIFrameElement): void {
20
+ if (iframe.parentNode) {
21
+ iframe.parentNode.removeChild(iframe);
22
+ }
23
+ }
24
+
25
+ describe('requestIframeClient and requestIframeServer', () => {
26
+ beforeEach(() => {
27
+ // Clear all caches
28
+ clearRequestIframeClientCache();
29
+ clearRequestIframeServerCache();
30
+ // Clear all iframes
31
+ document.querySelectorAll('iframe').forEach((iframe) => {
32
+ if (iframe.parentNode) {
33
+ iframe.parentNode.removeChild(iframe);
34
+ }
35
+ });
36
+ });
37
+
38
+ afterEach(() => {
39
+ // Clear all caches
40
+ clearRequestIframeClientCache();
41
+ clearRequestIframeServerCache();
42
+ // Clear all iframes
43
+ document.querySelectorAll('iframe').forEach((iframe) => {
44
+ if (iframe.parentNode) {
45
+ iframe.parentNode.removeChild(iframe);
46
+ }
47
+ });
48
+ });
49
+
50
+ describe('Basic functionality', () => {
51
+ it('should send request and receive response', async () => {
52
+ const origin = 'https://example.com';
53
+ const iframe = createTestIframe(origin);
54
+
55
+ // Mock iframe response
56
+ const mockContentWindow = {
57
+ postMessage: jest.fn((msg: PostMessageData) => {
58
+ // Simulate server handling request
59
+ if (msg.type === 'request') {
60
+ // Send ACK first
61
+ window.dispatchEvent(
62
+ new MessageEvent('message', {
63
+ data: {
64
+ __requestIframe__: 1,
65
+ type: 'ack',
66
+ requestId: msg.requestId,
67
+ path: msg.path
68
+ },
69
+ origin
70
+ })
71
+ );
72
+ // Then send response
73
+ setTimeout(() => {
74
+ window.dispatchEvent(
75
+ new MessageEvent('message', {
76
+ data: {
77
+ __requestIframe__: 1,
78
+ type: 'response',
79
+ requestId: msg.requestId,
80
+ data: { result: 'success' },
81
+ status: 200,
82
+ statusText: 'OK'
83
+ },
84
+ origin
85
+ })
86
+ );
87
+ }, 10);
88
+ }
89
+ })
90
+ };
91
+ Object.defineProperty(iframe, 'contentWindow', {
92
+ value: mockContentWindow,
93
+ writable: true
94
+ });
95
+
96
+ const client = requestIframeClient(iframe);
97
+ const server = requestIframeServer();
98
+
99
+ server.on('test', (req, res) => {
100
+ res.send({ result: 'success' });
101
+ });
102
+
103
+ const response = await client.send('test', { param: 'value' }, { ackTimeout: 1000 });
104
+ expect(response.data).toEqual({ result: 'success' });
105
+ expect(response.status).toBe(200);
106
+ expect(mockContentWindow.postMessage).toHaveBeenCalled();
107
+ server.destroy();
108
+ cleanupIframe(iframe);
109
+ });
110
+
111
+ it('should throw error when iframe.contentWindow is unavailable', () => {
112
+ const iframe = document.createElement('iframe');
113
+ iframe.src = 'https://example.com/test.html';
114
+ document.body.appendChild(iframe);
115
+ Object.defineProperty(iframe, 'contentWindow', {
116
+ value: null,
117
+ writable: true
118
+ });
119
+
120
+ expect(() => requestIframeClient(iframe)).toThrow();
121
+ });
122
+
123
+ it('should throw error on connection timeout', async () => {
124
+ const origin = 'https://example.com';
125
+ const iframe = createTestIframe(origin);
126
+
127
+ const mockContentWindow = {
128
+ postMessage: jest.fn()
129
+ };
130
+ Object.defineProperty(iframe, 'contentWindow', {
131
+ value: mockContentWindow,
132
+ writable: true
133
+ });
134
+
135
+ const client = requestIframeClient(iframe);
136
+ const server = requestIframeServer();
137
+
138
+ await expect(
139
+ client.send('test', undefined, { ackTimeout: 100 })
140
+ ).rejects.toMatchObject({
141
+ code: 'ACK_TIMEOUT'
142
+ });
143
+ cleanupIframe(iframe);
144
+ server.destroy();
145
+ });
146
+
147
+ it('should support isConnect method to check server availability', async () => {
148
+ const origin = 'https://example.com';
149
+ const iframe = createTestIframe(origin);
150
+
151
+ const mockContentWindow = {
152
+ postMessage: jest.fn((msg: PostMessageData) => {
153
+ if (msg.type === 'ping') {
154
+ window.dispatchEvent(
155
+ new MessageEvent('message', {
156
+ data: {
157
+ __requestIframe__: 1,
158
+ type: 'pong',
159
+ requestId: msg.requestId,
160
+ secretKey: msg.secretKey
161
+ },
162
+ origin
163
+ })
164
+ );
165
+ }
166
+ })
167
+ };
168
+ Object.defineProperty(iframe, 'contentWindow', {
169
+ value: mockContentWindow,
170
+ writable: true
171
+ });
172
+
173
+ const client = requestIframeClient(iframe);
174
+ const server = requestIframeServer();
175
+
176
+ const connected = await client.isConnect();
177
+ expect(connected).toBe(true);
178
+
179
+ server.destroy();
180
+ cleanupIframe(iframe);
181
+ });
182
+ });
183
+
184
+ describe('Interceptors', () => {
185
+ it('should support request interceptors', async () => {
186
+ const origin = 'https://example.com';
187
+ const iframe = createTestIframe(origin);
188
+
189
+ const mockContentWindow = {
190
+ postMessage: jest.fn((msg: PostMessageData) => {
191
+ if (msg.type === 'request') {
192
+ // Verify interceptor is effective
193
+ expect(msg.body).toHaveProperty('intercepted', true);
194
+
195
+ // Send ACK first
196
+ window.dispatchEvent(
197
+ new MessageEvent('message', {
198
+ data: {
199
+ __requestIframe__: 1,
200
+ type: 'ack',
201
+ requestId: msg.requestId,
202
+ path: msg.path
203
+ },
204
+ origin
205
+ })
206
+ );
207
+
208
+ // Then send response
209
+ setTimeout(() => {
210
+ window.dispatchEvent(
211
+ new MessageEvent('message', {
212
+ data: {
213
+ __requestIframe__: 1,
214
+ type: 'response',
215
+ requestId: msg.requestId,
216
+ data: { success: true },
217
+ status: 200,
218
+ statusText: 'OK'
219
+ },
220
+ origin
221
+ })
222
+ );
223
+ }, 10);
224
+ }
225
+ })
226
+ };
227
+ Object.defineProperty(iframe, 'contentWindow', {
228
+ value: mockContentWindow,
229
+ writable: true
230
+ });
231
+
232
+ const client = requestIframeClient(iframe);
233
+ const server = requestIframeServer();
234
+
235
+ server.on('test', (req, res) => {
236
+ res.send({ success: true });
237
+ });
238
+
239
+ const requestInterceptor = jest.fn((config: RequestConfig) => {
240
+ config.body = { ...config.body, intercepted: true };
241
+ return config;
242
+ });
243
+ client.interceptors.request.use(requestInterceptor);
244
+
245
+ await client.send('test', { param: 'value' }, { ackTimeout: 1000 });
246
+ expect(requestInterceptor).toHaveBeenCalled();
247
+ server.destroy();
248
+ cleanupIframe(iframe);
249
+ });
250
+
251
+ it('should support response interceptors', async () => {
252
+ const origin = 'https://example.com';
253
+ const iframe = createTestIframe(origin);
254
+
255
+ const mockContentWindow = {
256
+ postMessage: jest.fn((msg: PostMessageData) => {
257
+ if (msg.type === 'request') {
258
+ // Send ACK first
259
+ window.dispatchEvent(
260
+ new MessageEvent('message', {
261
+ data: {
262
+ __requestIframe__: 1,
263
+ type: 'ack',
264
+ requestId: msg.requestId,
265
+ path: msg.path
266
+ },
267
+ origin
268
+ })
269
+ );
270
+
271
+ // Then send response
272
+ setTimeout(() => {
273
+ window.dispatchEvent(
274
+ new MessageEvent('message', {
275
+ data: {
276
+ __requestIframe__: 1,
277
+ type: 'response',
278
+ requestId: msg.requestId,
279
+ data: { success: true },
280
+ status: 200,
281
+ statusText: 'OK'
282
+ },
283
+ origin
284
+ })
285
+ );
286
+ }, 10);
287
+ }
288
+ })
289
+ };
290
+ Object.defineProperty(iframe, 'contentWindow', {
291
+ value: mockContentWindow,
292
+ writable: true
293
+ });
294
+
295
+ const client = requestIframeClient(iframe);
296
+ const server = requestIframeServer();
297
+
298
+ server.on('test', (req, res) => {
299
+ res.send({ success: true });
300
+ });
301
+
302
+ const responseInterceptor = jest.fn((response: Response) => {
303
+ response.data = { ...response.data, intercepted: true };
304
+ return response;
305
+ });
306
+ client.interceptors.response.use(responseInterceptor as any);
307
+
308
+ const response = await client.send('test', undefined, { ackTimeout: 1000 });
309
+ expect(response.data).toHaveProperty('intercepted', true);
310
+ expect(responseInterceptor).toHaveBeenCalled();
311
+ server.destroy();
312
+ cleanupIframe(iframe);
313
+ });
314
+ });
315
+
316
+ describe('Error handling', () => {
317
+ it('should handle error response correctly', async () => {
318
+ const origin = 'https://example.com';
319
+ const iframe = createTestIframe(origin);
320
+
321
+ const mockContentWindow = {
322
+ postMessage: jest.fn((msg: PostMessageData) => {
323
+ if (msg.type === 'request') {
324
+ // Send ACK first
325
+ window.dispatchEvent(
326
+ new MessageEvent('message', {
327
+ data: {
328
+ __requestIframe__: 1,
329
+ type: 'ack',
330
+ requestId: msg.requestId,
331
+ path: msg.path
332
+ },
333
+ origin
334
+ })
335
+ );
336
+
337
+ // Then send error response
338
+ setTimeout(() => {
339
+ window.dispatchEvent(
340
+ new MessageEvent('message', {
341
+ data: {
342
+ __requestIframe__: 1,
343
+ type: 'error',
344
+ requestId: msg.requestId,
345
+ error: {
346
+ message: 'Method not found',
347
+ code: 'METHOD_NOT_FOUND'
348
+ },
349
+ status: 404,
350
+ statusText: 'Not Found'
351
+ },
352
+ origin
353
+ })
354
+ );
355
+ }, 10);
356
+ }
357
+ })
358
+ };
359
+ Object.defineProperty(iframe, 'contentWindow', {
360
+ value: mockContentWindow,
361
+ writable: true
362
+ });
363
+
364
+ const client = requestIframeClient(iframe);
365
+ const server = requestIframeServer();
366
+
367
+ // No handler registered, should return METHOD_NOT_FOUND
368
+ await expect(client.send('test', undefined, { ackTimeout: 1000 })).rejects.toMatchObject({
369
+ code: 'METHOD_NOT_FOUND',
370
+ response: { status: 404 }
371
+ });
372
+ cleanupIframe(iframe);
373
+ server.destroy();
374
+ });
375
+ });
376
+
377
+ describe('Async tasks', () => {
378
+ it('should support async task handling', async () => {
379
+ const origin = 'https://example.com';
380
+ const iframe = createTestIframe(origin);
381
+
382
+ const mockContentWindow = {
383
+ postMessage: jest.fn((msg: PostMessageData) => {
384
+ if (msg.type === 'request') {
385
+ // Send ACK first
386
+ window.dispatchEvent(
387
+ new MessageEvent('message', {
388
+ data: {
389
+ __requestIframe__: 1,
390
+ type: 'ack',
391
+ requestId: msg.requestId,
392
+ path: msg.path
393
+ },
394
+ origin
395
+ })
396
+ );
397
+
398
+ // Send async notification
399
+ setTimeout(() => {
400
+ window.dispatchEvent(
401
+ new MessageEvent('message', {
402
+ data: {
403
+ __requestIframe__: 1,
404
+ type: 'async',
405
+ requestId: msg.requestId,
406
+ path: msg.path
407
+ },
408
+ origin
409
+ })
410
+ );
411
+ }, 10);
412
+
413
+ // Delay response (simulate async processing)
414
+ setTimeout(() => {
415
+ window.dispatchEvent(
416
+ new MessageEvent('message', {
417
+ data: {
418
+ __requestIframe__: 1,
419
+ type: 'response',
420
+ requestId: msg.requestId,
421
+ data: { result: 'async success' },
422
+ status: 200,
423
+ statusText: 'OK'
424
+ },
425
+ origin
426
+ })
427
+ );
428
+ }, 100);
429
+ }
430
+ })
431
+ };
432
+ Object.defineProperty(iframe, 'contentWindow', {
433
+ value: mockContentWindow,
434
+ writable: true
435
+ });
436
+
437
+ const client = requestIframeClient(iframe);
438
+ const server = requestIframeServer();
439
+
440
+ server.on('asyncTest', async (req, res) => {
441
+ await new Promise(resolve => setTimeout(resolve, 50));
442
+ res.send({ result: 'async success' });
443
+ });
444
+
445
+ const response = await client.send('asyncTest', undefined, {
446
+ ackTimeout: 1000,
447
+ timeout: 200,
448
+ asyncTimeout: 5000
449
+ });
450
+ expect(response.data).toEqual({ result: 'async success' });
451
+ server.destroy();
452
+ cleanupIframe(iframe);
453
+ });
454
+ });
455
+
456
+ describe('MessageChannel sharing', () => {
457
+ it('should share the same message channel for the same secretKey', () => {
458
+ const iframe1 = createTestIframe('https://example.com');
459
+ const iframe2 = createTestIframe('https://example2.com');
460
+
461
+ const server1 = requestIframeServer({ secretKey: 'demo' });
462
+ const server2 = requestIframeServer({ secretKey: 'demo' });
463
+
464
+ // Server instances are different
465
+ expect(server1).not.toBe(server2);
466
+
467
+ // But they should share the same underlying message channel (verified by secretKey)
468
+ expect(server1.secretKey).toBe(server2.secretKey);
469
+ expect(server1.secretKey).toBe('demo');
470
+
471
+ server1.destroy();
472
+ server2.destroy();
473
+ cleanupIframe(iframe1);
474
+ cleanupIframe(iframe2);
475
+ });
476
+
477
+ it('should have independent message channels for different secretKeys', () => {
478
+ const iframe = createTestIframe('https://example.com');
479
+
480
+ const server1 = requestIframeServer({ secretKey: 'demo1' });
481
+ const server2 = requestIframeServer({ secretKey: 'demo2' });
482
+
483
+ // Verify different server instances
484
+ expect(server1).not.toBe(server2);
485
+
486
+ // secretKeys are different
487
+ expect(server1.secretKey).toBe('demo1');
488
+ expect(server2.secretKey).toBe('demo2');
489
+
490
+ server1.destroy();
491
+ server2.destroy();
492
+ cleanupIframe(iframe);
493
+ });
494
+
495
+ it('should share the same message channel when no secretKey', () => {
496
+ const iframe1 = createTestIframe('https://example.com');
497
+ const iframe2 = createTestIframe('https://example2.com');
498
+
499
+ const server1 = requestIframeServer();
500
+ const server2 = requestIframeServer();
501
+
502
+ // Server instances are different
503
+ expect(server1).not.toBe(server2);
504
+
505
+ // But they should share the same underlying message channel (both have no secretKey)
506
+ expect(server1.secretKey).toBe(server2.secretKey);
507
+ expect(server1.secretKey).toBeUndefined();
508
+
509
+ server1.destroy();
510
+ server2.destroy();
511
+ cleanupIframe(iframe1);
512
+ cleanupIframe(iframe2);
513
+ });
514
+ });
515
+
516
+ describe('Middleware', () => {
517
+ it('should support global middleware', async () => {
518
+ const origin = 'https://example.com';
519
+ const iframe = createTestIframe(origin);
520
+
521
+ const mockContentWindow = {
522
+ postMessage: jest.fn()
523
+ };
524
+ Object.defineProperty(iframe, 'contentWindow', {
525
+ value: mockContentWindow,
526
+ writable: true
527
+ });
528
+
529
+ const client = requestIframeClient(iframe);
530
+ const server = requestIframeServer();
531
+
532
+ // Add middleware (auth validation)
533
+ const middleware = jest.fn((req, res, next) => {
534
+ if (req.headers['authorization'] === 'Bearer token123') {
535
+ next();
536
+ } else {
537
+ res.status(401).send({ error: 'Unauthorized' });
538
+ }
539
+ });
540
+
541
+ server.use(middleware);
542
+
543
+ server.on('test', (req, res) => {
544
+ res.send({ result: 'success' });
545
+ });
546
+
547
+ // Simulate request from iframe to current window (unauthorized)
548
+ const requestId1 = 'req-unauthorized';
549
+ window.dispatchEvent(
550
+ new MessageEvent('message', {
551
+ data: {
552
+ __requestIframe__: 1,
553
+ type: 'request',
554
+ requestId: requestId1,
555
+ path: 'test',
556
+ body: {},
557
+ headers: {}
558
+ },
559
+ origin,
560
+ source: mockContentWindow as any
561
+ })
562
+ );
563
+ await new Promise((resolve) => setTimeout(resolve, 50));
564
+
565
+ // Verify middleware was called
566
+ expect(middleware).toHaveBeenCalled();
567
+
568
+ // Find response (should be 401 or contain Unauthorized)
569
+ const ackCall = mockContentWindow.postMessage.mock.calls.find(
570
+ (call: any[]) => call[0]?.type === 'ack'
571
+ );
572
+ expect(ackCall).toBeDefined();
573
+
574
+ const errorCall = mockContentWindow.postMessage.mock.calls.find(
575
+ (call: any[]) => {
576
+ const msg = call[0];
577
+ return (msg?.type === 'error' || msg?.type === 'response') &&
578
+ (msg?.status === 401 || msg?.data?.error === 'Unauthorized');
579
+ }
580
+ );
581
+ expect(errorCall).toBeDefined();
582
+
583
+ // Test authorized request
584
+ const requestId2 = 'req-authorized';
585
+ window.dispatchEvent(
586
+ new MessageEvent('message', {
587
+ data: {
588
+ __requestIframe__: 1,
589
+ type: 'request',
590
+ requestId: requestId2,
591
+ path: 'test',
592
+ body: {},
593
+ headers: { authorization: 'Bearer token123' }
594
+ },
595
+ origin,
596
+ source: mockContentWindow as any
597
+ })
598
+ );
599
+ await new Promise((resolve) => setTimeout(resolve, 50));
600
+
601
+ // Verify authorized request succeeded
602
+ const successCall = mockContentWindow.postMessage.mock.calls.find(
603
+ (call: any[]) => call[0]?.type === 'response' && call[0]?.status === 200
604
+ );
605
+ expect(successCall).toBeDefined();
606
+ expect(successCall[0].data).toEqual({ result: 'success' });
607
+
608
+ server.destroy();
609
+ cleanupIframe(iframe);
610
+ });
611
+
612
+ it('should support path-matching middleware', async () => {
613
+ const origin = 'https://example.com';
614
+ const iframe = createTestIframe(origin);
615
+
616
+ const mockContentWindow = {
617
+ postMessage: jest.fn()
618
+ };
619
+ Object.defineProperty(iframe, 'contentWindow', {
620
+ value: mockContentWindow,
621
+ writable: true
622
+ });
623
+
624
+ const client = requestIframeClient(iframe);
625
+ const server = requestIframeServer();
626
+
627
+ const apiMiddleware = jest.fn((req, res, next) => {
628
+ next();
629
+ });
630
+ const otherMiddleware = jest.fn((req, res, next) => {
631
+ next();
632
+ });
633
+
634
+ // Apply middleware only to /api path
635
+ server.use('/api', apiMiddleware);
636
+ server.use('/other', otherMiddleware);
637
+
638
+ server.on('api/test', (req, res) => {
639
+ res.send({ result: 'api success' });
640
+ });
641
+ server.on('other/test', (req, res) => {
642
+ res.send({ result: 'other success' });
643
+ });
644
+
645
+ // Test /api/test path
646
+ const requestId1 = 'req-api';
647
+ window.dispatchEvent(
648
+ new MessageEvent('message', {
649
+ data: {
650
+ __requestIframe__: 1,
651
+ type: 'request',
652
+ requestId: requestId1,
653
+ path: 'api/test',
654
+ body: {}
655
+ },
656
+ origin,
657
+ source: mockContentWindow as any
658
+ })
659
+ );
660
+ await new Promise((resolve) => setTimeout(resolve, 50));
661
+
662
+ // Verify apiMiddleware was called, otherMiddleware was not
663
+ expect(apiMiddleware).toHaveBeenCalled();
664
+ expect(otherMiddleware).not.toHaveBeenCalled();
665
+
666
+ // Test /other/test path
667
+ const requestId2 = 'req-other';
668
+ window.dispatchEvent(
669
+ new MessageEvent('message', {
670
+ data: {
671
+ __requestIframe__: 1,
672
+ type: 'request',
673
+ requestId: requestId2,
674
+ path: 'other/test',
675
+ body: {}
676
+ },
677
+ origin,
678
+ source: mockContentWindow as any
679
+ })
680
+ );
681
+ await new Promise((resolve) => setTimeout(resolve, 50));
682
+
683
+ // Verify otherMiddleware was called
684
+ expect(otherMiddleware).toHaveBeenCalled();
685
+
686
+ server.destroy();
687
+ cleanupIframe(iframe);
688
+ });
689
+ });
690
+
691
+ describe('sendFile', () => {
692
+ it('should support sending file (base64 encoded)', async () => {
693
+ const origin = 'https://example.com';
694
+ const iframe = createTestIframe(origin);
695
+
696
+ const mockContentWindow = {
697
+ postMessage: jest.fn()
698
+ };
699
+ Object.defineProperty(iframe, 'contentWindow', {
700
+ value: mockContentWindow,
701
+ writable: true
702
+ });
703
+
704
+ const client = requestIframeClient(iframe);
705
+ const server = requestIframeServer();
706
+
707
+ server.on('getFile', async (req, res) => {
708
+ const fileContent = 'Hello World';
709
+ await res.sendFile(fileContent, {
710
+ mimeType: 'text/plain',
711
+ fileName: 'test.txt'
712
+ });
713
+ });
714
+
715
+ // Simulate request from iframe
716
+ const requestId = 'req-file';
717
+ window.dispatchEvent(
718
+ new MessageEvent('message', {
719
+ data: {
720
+ __requestIframe__: 1,
721
+ type: 'request',
722
+ requestId: requestId,
723
+ path: 'getFile',
724
+ body: {}
725
+ },
726
+ origin,
727
+ source: mockContentWindow as any
728
+ })
729
+ );
730
+ await new Promise((resolve) => setTimeout(resolve, 100));
731
+
732
+ // Verify sendFile was called
733
+ expect(mockContentWindow.postMessage).toHaveBeenCalled();
734
+ const fileCall = mockContentWindow.postMessage.mock.calls.find(
735
+ (call: any[]) => call[0]?.type === 'response' && call[0]?.fileData
736
+ );
737
+ expect(fileCall).toBeDefined();
738
+ expect(fileCall[0].fileData.mimeType).toBe('text/plain');
739
+ expect(fileCall[0].fileData.fileName).toBe('test.txt');
740
+
741
+ // Decode base64 to verify content
742
+ if (fileCall[0].fileData.content) {
743
+ const decoded = atob(fileCall[0].fileData.content);
744
+ expect(decoded).toBe('Hello World');
745
+ }
746
+
747
+ server.destroy();
748
+ cleanupIframe(iframe);
749
+ });
750
+
751
+ it('should support sending Blob file', async () => {
752
+ const origin = 'https://example.com';
753
+ const iframe = createTestIframe(origin);
754
+
755
+ const mockContentWindow = {
756
+ postMessage: jest.fn()
757
+ };
758
+ Object.defineProperty(iframe, 'contentWindow', {
759
+ value: mockContentWindow,
760
+ writable: true
761
+ });
762
+
763
+ const server = requestIframeServer();
764
+
765
+ server.on('getBlob', async (req, res) => {
766
+ const blob = new Blob(['test content'], { type: 'text/plain' });
767
+ await res.sendFile(blob, {
768
+ fileName: 'blob.txt',
769
+ mimeType: 'text/plain'
770
+ });
771
+ });
772
+
773
+ const requestId = 'req-blob';
774
+ window.dispatchEvent(
775
+ new MessageEvent('message', {
776
+ data: {
777
+ __requestIframe__: 1,
778
+ type: 'request',
779
+ requestId: requestId,
780
+ path: 'getBlob',
781
+ body: {}
782
+ },
783
+ origin,
784
+ source: mockContentWindow as any
785
+ })
786
+ );
787
+ await new Promise((resolve) => setTimeout(resolve, 100));
788
+
789
+ const fileCall = mockContentWindow.postMessage.mock.calls.find(
790
+ (call: any[]) => call[0]?.type === 'response' && call[0]?.fileData
791
+ );
792
+ expect(fileCall).toBeDefined();
793
+ expect(fileCall![0].fileData.mimeType).toBe('text/plain');
794
+
795
+ server.destroy();
796
+ cleanupIframe(iframe);
797
+ });
798
+
799
+ it('should support sending File object', async () => {
800
+ const origin = 'https://example.com';
801
+ const iframe = createTestIframe(origin);
802
+
803
+ const mockContentWindow = {
804
+ postMessage: jest.fn()
805
+ };
806
+ Object.defineProperty(iframe, 'contentWindow', {
807
+ value: mockContentWindow,
808
+ writable: true
809
+ });
810
+
811
+ const server = requestIframeServer();
812
+
813
+ server.on('getFileObj', async (req, res) => {
814
+ const file = new File(['file content'], 'test.txt', { type: 'text/plain' });
815
+ await res.sendFile(file);
816
+ });
817
+
818
+ const requestId = 'req-fileobj';
819
+ window.dispatchEvent(
820
+ new MessageEvent('message', {
821
+ data: {
822
+ __requestIframe__: 1,
823
+ type: 'request',
824
+ requestId: requestId,
825
+ path: 'getFileObj',
826
+ body: {}
827
+ },
828
+ origin,
829
+ source: mockContentWindow as any
830
+ })
831
+ );
832
+ await new Promise((resolve) => setTimeout(resolve, 100));
833
+
834
+ const fileCall = mockContentWindow.postMessage.mock.calls.find(
835
+ (call: any[]) => call[0]?.type === 'response' && call[0]?.fileData
836
+ );
837
+ expect(fileCall).toBeDefined();
838
+ expect(fileCall[0].fileData.fileName).toBe('test.txt');
839
+
840
+ server.destroy();
841
+ cleanupIframe(iframe);
842
+ });
843
+
844
+ it('should support sendFile with requireAck', async () => {
845
+ const origin = 'https://example.com';
846
+ const iframe = createTestIframe(origin);
847
+
848
+ let responseMessage: any = null;
849
+ const mockContentWindow = {
850
+ postMessage: jest.fn((msg: PostMessageData) => {
851
+ if (msg.type === 'request') {
852
+ window.dispatchEvent(
853
+ new MessageEvent('message', {
854
+ data: {
855
+ __requestIframe__: 1,
856
+ type: 'ack',
857
+ requestId: msg.requestId,
858
+ path: msg.path
859
+ },
860
+ origin
861
+ })
862
+ );
863
+ setTimeout(() => {
864
+ const response: PostMessageData = {
865
+ __requestIframe__: 1,
866
+ timestamp: Date.now(),
867
+ type: 'response',
868
+ requestId: msg.requestId,
869
+ fileData: {
870
+ content: btoa('test'),
871
+ mimeType: 'text/plain',
872
+ fileName: 'test.txt'
873
+ },
874
+ status: 200,
875
+ requireAck: true
876
+ };
877
+ responseMessage = response;
878
+ window.dispatchEvent(
879
+ new MessageEvent('message', {
880
+ data: response,
881
+ origin
882
+ })
883
+ );
884
+ }, 10);
885
+ }
886
+ })
887
+ };
888
+ Object.defineProperty(iframe, 'contentWindow', {
889
+ value: mockContentWindow,
890
+ writable: true
891
+ });
892
+
893
+ const client = requestIframeClient(iframe);
894
+ const server = requestIframeServer();
895
+
896
+ server.on('getFileAck', async (req, res) => {
897
+ await res.sendFile('test', {
898
+ fileName: 'test.txt',
899
+ requireAck: true
900
+ });
901
+ });
902
+
903
+ const requestId = 'req-fileack';
904
+ window.dispatchEvent(
905
+ new MessageEvent('message', {
906
+ data: {
907
+ __requestIframe__: 1,
908
+ type: 'request',
909
+ requestId: requestId,
910
+ path: 'getFileAck',
911
+ body: {}
912
+ },
913
+ origin,
914
+ source: mockContentWindow as any
915
+ })
916
+ );
917
+ await new Promise((resolve) => setTimeout(resolve, 150));
918
+
919
+ // Client should send received message when requireAck is true
920
+ const receivedCall = mockContentWindow.postMessage.mock.calls.find(
921
+ (call: any[]) => call[0]?.type === 'received'
922
+ );
923
+ // Note: received message is sent by client, not server
924
+ // So we check that the response was sent with requireAck
925
+ if (responseMessage && 'requireAck' in responseMessage) {
926
+ expect(responseMessage.requireAck).toBe(true);
927
+ }
928
+
929
+ server.destroy();
930
+ cleanupIframe(iframe);
931
+ });
932
+ });
933
+
934
+ describe('server.map', () => {
935
+ it('should register multiple event handlers at once', async () => {
936
+ const origin = 'https://example.com';
937
+ const iframe = createTestIframe(origin);
938
+
939
+ const handlers: Record<string, jest.Mock> = {
940
+ 'api/getUser': jest.fn(async (req, res) => {
941
+ res.send({ id: req.body.id, name: 'Tom' });
942
+ }),
943
+ 'api/saveUser': jest.fn(async (req, res) => {
944
+ res.send({ success: true, saved: req.body });
945
+ })
946
+ };
947
+
948
+ const mockContentWindow = {
949
+ postMessage: jest.fn()
950
+ };
951
+ Object.defineProperty(iframe, 'contentWindow', {
952
+ value: mockContentWindow,
953
+ writable: true
954
+ });
955
+
956
+ const server = requestIframeServer();
957
+
958
+ // Use map method to register multiple handlers at once
959
+ server.map(handlers);
960
+
961
+ // Send request
962
+ const requestId1 = 'req-1';
963
+ window.dispatchEvent(
964
+ new MessageEvent('message', {
965
+ data: {
966
+ __requestIframe__: 1,
967
+ type: 'request',
968
+ requestId: requestId1,
969
+ path: 'api/getUser',
970
+ body: { id: 1 }
971
+ },
972
+ origin,
973
+ source: mockContentWindow as any
974
+ })
975
+ );
976
+
977
+ await new Promise((resolve) => setTimeout(resolve, 50));
978
+
979
+ // Verify first handler was called
980
+ expect(handlers['api/getUser']).toHaveBeenCalled();
981
+ const callArgs = handlers['api/getUser'].mock.calls[0];
982
+ expect(callArgs[0].body).toEqual({ id: 1 });
983
+ expect(callArgs[0].path).toBe('api/getUser');
984
+ expect(callArgs[0].requestId).toBe(requestId1);
985
+
986
+ // Test second handler
987
+ const requestId2 = 'req-2';
988
+ window.dispatchEvent(
989
+ new MessageEvent('message', {
990
+ data: {
991
+ __requestIframe__: 1,
992
+ type: 'request',
993
+ requestId: requestId2,
994
+ path: 'api/saveUser',
995
+ body: { name: 'Alice' }
996
+ },
997
+ origin,
998
+ source: mockContentWindow as any
999
+ })
1000
+ );
1001
+ await new Promise((resolve) => setTimeout(resolve, 50));
1002
+
1003
+ expect(handlers['api/saveUser']).toHaveBeenCalled();
1004
+ const callArgs2 = handlers['api/saveUser'].mock.calls[0];
1005
+ expect(callArgs2[0].body).toEqual({ name: 'Alice' });
1006
+ expect(callArgs2[0].path).toBe('api/saveUser');
1007
+ expect(callArgs2[0].requestId).toBe(requestId2);
1008
+
1009
+ server.destroy();
1010
+ cleanupIframe(iframe);
1011
+ });
1012
+ });
1013
+
1014
+ describe('Automatic cookie management', () => {
1015
+ it('should manually set and get cookies', async () => {
1016
+ const origin = 'https://example.com';
1017
+ const iframe = createTestIframe(origin);
1018
+
1019
+ const mockContentWindow = {
1020
+ postMessage: jest.fn()
1021
+ };
1022
+ Object.defineProperty(iframe, 'contentWindow', {
1023
+ value: mockContentWindow,
1024
+ writable: true
1025
+ });
1026
+
1027
+ const client = requestIframeClient(iframe);
1028
+
1029
+ // Initial state should be empty
1030
+ expect(client.getCookies()).toEqual({});
1031
+ expect(client.getCookie('token')).toBeUndefined();
1032
+
1033
+ // Set cookie (default path '/')
1034
+ client.setCookie('token', 'abc123');
1035
+ client.setCookie('userId', '42');
1036
+
1037
+ // Get cookie
1038
+ expect(client.getCookie('token')).toBe('abc123');
1039
+ expect(client.getCookie('userId')).toBe('42');
1040
+ expect(client.getCookies()).toEqual({ token: 'abc123', userId: '42' });
1041
+
1042
+ // Remove single cookie
1043
+ client.removeCookie('token');
1044
+ expect(client.getCookie('token')).toBeUndefined();
1045
+ expect(client.getCookie('userId')).toBe('42');
1046
+
1047
+ // Clear all cookies
1048
+ client.clearCookies();
1049
+ expect(client.getCookies()).toEqual({});
1050
+
1051
+ cleanupIframe(iframe);
1052
+ });
1053
+
1054
+ it('should support path-based cookie isolation', () => {
1055
+ const origin = 'https://example.com';
1056
+ const iframe = createTestIframe(origin);
1057
+
1058
+ const mockContentWindow = {
1059
+ postMessage: jest.fn()
1060
+ };
1061
+ Object.defineProperty(iframe, 'contentWindow', {
1062
+ value: mockContentWindow,
1063
+ writable: true
1064
+ });
1065
+
1066
+ const client = requestIframeClient(iframe);
1067
+
1068
+ // Set cookies with different paths
1069
+ client.setCookie('globalToken', 'global_123', { path: '/' });
1070
+ client.setCookie('apiToken', 'api_456', { path: '/api' });
1071
+ client.setCookie('adminToken', 'admin_789', { path: '/admin' });
1072
+
1073
+ // Get root path cookies - should only include path='/' ones
1074
+ expect(client.getCookies('/')).toEqual({ globalToken: 'global_123' });
1075
+
1076
+ // Get /api path cookies - should include '/' and '/api' ones
1077
+ expect(client.getCookies('/api')).toEqual({
1078
+ globalToken: 'global_123',
1079
+ apiToken: 'api_456'
1080
+ });
1081
+
1082
+ // Get /api/users path cookies - should include '/' and '/api' ones
1083
+ expect(client.getCookies('/api/users')).toEqual({
1084
+ globalToken: 'global_123',
1085
+ apiToken: 'api_456'
1086
+ });
1087
+
1088
+ // Get /admin path cookies - should include '/' and '/admin' ones
1089
+ expect(client.getCookies('/admin')).toEqual({
1090
+ globalToken: 'global_123',
1091
+ adminToken: 'admin_789'
1092
+ });
1093
+
1094
+ // Get all cookies
1095
+ expect(client.getCookies()).toEqual({
1096
+ globalToken: 'global_123',
1097
+ apiToken: 'api_456',
1098
+ adminToken: 'admin_789'
1099
+ });
1100
+
1101
+ cleanupIframe(iframe);
1102
+ });
1103
+
1104
+ it('should automatically include set cookies in requests', async () => {
1105
+ const origin = 'https://example.com';
1106
+ const iframe = createTestIframe(origin);
1107
+
1108
+ const mockContentWindow = {
1109
+ postMessage: jest.fn()
1110
+ };
1111
+ Object.defineProperty(iframe, 'contentWindow', {
1112
+ value: mockContentWindow,
1113
+ writable: true
1114
+ });
1115
+
1116
+ const client = requestIframeClient(iframe);
1117
+
1118
+ // Pre-set cookies
1119
+ client.setCookie('sessionId', 'sess_123');
1120
+ client.setCookie('theme', 'dark');
1121
+
1122
+ // Send request (don't wait for response, just check request data)
1123
+ client.send('/api/test', { data: 'test' }).catch(() => {});
1124
+ await new Promise((resolve) => setTimeout(resolve, 10));
1125
+
1126
+ // Verify request includes pre-set cookies
1127
+ expect(mockContentWindow.postMessage).toHaveBeenCalled();
1128
+ const requestCall = mockContentWindow.postMessage.mock.calls.find(
1129
+ (call: any[]) => call[0]?.type === 'request'
1130
+ );
1131
+ expect(requestCall).toBeDefined();
1132
+ expect(requestCall[0].cookies).toEqual({
1133
+ sessionId: 'sess_123',
1134
+ theme: 'dark'
1135
+ });
1136
+
1137
+ cleanupIframe(iframe);
1138
+ });
1139
+
1140
+ it('should override internal cookies with user-provided cookies', async () => {
1141
+ const origin = 'https://example.com';
1142
+ const iframe = createTestIframe(origin);
1143
+
1144
+ const mockContentWindow = {
1145
+ postMessage: jest.fn()
1146
+ };
1147
+ Object.defineProperty(iframe, 'contentWindow', {
1148
+ value: mockContentWindow,
1149
+ writable: true
1150
+ });
1151
+
1152
+ const client = requestIframeClient(iframe);
1153
+
1154
+ // Pre-set cookies
1155
+ client.setCookie('token', 'old_token');
1156
+ client.setCookie('lang', 'en');
1157
+
1158
+ // Pass new token in request
1159
+ client.send('/api/test', {}, {
1160
+ cookies: { token: 'new_token', extra: 'value' }
1161
+ }).catch(() => {});
1162
+ await new Promise((resolve) => setTimeout(resolve, 10));
1163
+
1164
+ // Verify user-provided cookies override internal ones
1165
+ const requestCall = mockContentWindow.postMessage.mock.calls.find(
1166
+ (call: any[]) => call[0]?.type === 'request'
1167
+ );
1168
+ expect(requestCall[0].cookies).toEqual({
1169
+ token: 'new_token', // User-provided overrides internal
1170
+ lang: 'en', // Internal preserved
1171
+ extra: 'value' // User-provided extra
1172
+ });
1173
+
1174
+ cleanupIframe(iframe);
1175
+ });
1176
+
1177
+ it('should automatically save server-set cookies after response', async () => {
1178
+ const origin = 'https://example.com';
1179
+ const iframe = createTestIframe(origin);
1180
+
1181
+ // Mock iframe contentWindow
1182
+ const mockContentWindow = {
1183
+ postMessage: jest.fn()
1184
+ };
1185
+ Object.defineProperty(iframe, 'contentWindow', {
1186
+ value: mockContentWindow,
1187
+ writable: true
1188
+ });
1189
+
1190
+ const client = requestIframeClient(iframe);
1191
+ const server = requestIframeServer();
1192
+
1193
+ // Server sets cookie
1194
+ server.on('/api/login', (req, res) => {
1195
+ res.cookie('authToken', 'jwt_xxx');
1196
+ res.cookie('refreshToken', 'refresh_yyy');
1197
+ res.send({ success: true });
1198
+ });
1199
+
1200
+ const requestId = 'test-cookie-req-1';
1201
+
1202
+ // Make request
1203
+ const responsePromise = client.send('/api/login', { username: 'test' }, { requestId });
1204
+ await new Promise((resolve) => setTimeout(resolve, 10));
1205
+
1206
+ // Simulate server receiving request and responding
1207
+ window.dispatchEvent(
1208
+ new MessageEvent('message', {
1209
+ data: {
1210
+ __requestIframe__: 1,
1211
+ type: 'request',
1212
+ requestId,
1213
+ path: '/api/login',
1214
+ body: { username: 'test' }
1215
+ },
1216
+ origin,
1217
+ source: mockContentWindow as any
1218
+ })
1219
+ );
1220
+ await new Promise((resolve) => setTimeout(resolve, 50));
1221
+
1222
+ // Simulate client receiving response
1223
+ const responseCall = mockContentWindow.postMessage.mock.calls.find(
1224
+ (call: any[]) => call[0]?.type === 'response'
1225
+ );
1226
+ if (responseCall) {
1227
+ window.dispatchEvent(
1228
+ new MessageEvent('message', {
1229
+ data: responseCall[0],
1230
+ origin,
1231
+ source: mockContentWindow as any
1232
+ })
1233
+ );
1234
+ }
1235
+ await new Promise((resolve) => setTimeout(resolve, 50));
1236
+
1237
+ // Verify client automatically saved server-set cookies
1238
+ expect(client.getCookie('authToken')).toBe('jwt_xxx');
1239
+ expect(client.getCookie('refreshToken')).toBe('refresh_yyy');
1240
+
1241
+ server.destroy();
1242
+ cleanupIframe(iframe);
1243
+ });
1244
+ });
1245
+
1246
+ describe('Response methods', () => {
1247
+ it('should support res.send with requireAck', async () => {
1248
+ const origin = 'https://example.com';
1249
+ const iframe = createTestIframe(origin);
1250
+
1251
+ let responseMessage: any = null;
1252
+ const mockContentWindow = {
1253
+ postMessage: jest.fn((msg: PostMessageData) => {
1254
+ if (msg.type === 'request') {
1255
+ window.dispatchEvent(
1256
+ new MessageEvent('message', {
1257
+ data: {
1258
+ __requestIframe__: 1,
1259
+ type: 'ack',
1260
+ requestId: msg.requestId,
1261
+ path: msg.path
1262
+ },
1263
+ origin
1264
+ })
1265
+ );
1266
+ setTimeout(() => {
1267
+ const response: PostMessageData = {
1268
+ __requestIframe__: 1,
1269
+ timestamp: Date.now(),
1270
+ type: 'response',
1271
+ requestId: msg.requestId,
1272
+ data: { result: 'success' },
1273
+ status: 200,
1274
+ requireAck: true
1275
+ };
1276
+ responseMessage = response;
1277
+ window.dispatchEvent(
1278
+ new MessageEvent('message', {
1279
+ data: response,
1280
+ origin
1281
+ })
1282
+ );
1283
+ }, 10);
1284
+ }
1285
+ })
1286
+ };
1287
+ Object.defineProperty(iframe, 'contentWindow', {
1288
+ value: mockContentWindow,
1289
+ writable: true
1290
+ });
1291
+
1292
+ const client = requestIframeClient(iframe);
1293
+ const server = requestIframeServer();
1294
+
1295
+ server.on('testAck', async (req, res) => {
1296
+ await res.send({ result: 'success' }, { requireAck: true });
1297
+ });
1298
+
1299
+ const requestId = 'req-ack';
1300
+ window.dispatchEvent(
1301
+ new MessageEvent('message', {
1302
+ data: {
1303
+ __requestIframe__: 1,
1304
+ type: 'request',
1305
+ requestId: requestId,
1306
+ path: 'testAck',
1307
+ body: {}
1308
+ },
1309
+ origin,
1310
+ source: mockContentWindow as any
1311
+ })
1312
+ );
1313
+ await new Promise((resolve) => setTimeout(resolve, 150));
1314
+
1315
+ // Verify response was sent with requireAck
1316
+ if (responseMessage && 'requireAck' in responseMessage) {
1317
+ expect(responseMessage.requireAck).toBe(true);
1318
+ }
1319
+
1320
+ server.destroy();
1321
+ cleanupIframe(iframe);
1322
+ });
1323
+
1324
+ it('should support res.json with requireAck', async () => {
1325
+ const origin = 'https://example.com';
1326
+ const iframe = createTestIframe(origin);
1327
+
1328
+ const mockContentWindow = {
1329
+ postMessage: jest.fn((msg: PostMessageData) => {
1330
+ if (msg.type === 'request') {
1331
+ window.dispatchEvent(
1332
+ new MessageEvent('message', {
1333
+ data: {
1334
+ __requestIframe__: 1,
1335
+ type: 'ack',
1336
+ requestId: msg.requestId,
1337
+ path: msg.path
1338
+ },
1339
+ origin
1340
+ })
1341
+ );
1342
+ setTimeout(() => {
1343
+ window.dispatchEvent(
1344
+ new MessageEvent('message', {
1345
+ data: {
1346
+ __requestIframe__: 1,
1347
+ type: 'response',
1348
+ requestId: msg.requestId,
1349
+ data: { json: true },
1350
+ status: 200,
1351
+ requireAck: true
1352
+ },
1353
+ origin
1354
+ })
1355
+ );
1356
+ }, 10);
1357
+ }
1358
+ })
1359
+ };
1360
+ Object.defineProperty(iframe, 'contentWindow', {
1361
+ value: mockContentWindow,
1362
+ writable: true
1363
+ });
1364
+
1365
+ const server = requestIframeServer();
1366
+
1367
+ server.on('testJson', async (req, res) => {
1368
+ await res.json({ json: true }, { requireAck: true });
1369
+ });
1370
+
1371
+ const requestId = 'req-json';
1372
+ window.dispatchEvent(
1373
+ new MessageEvent('message', {
1374
+ data: {
1375
+ __requestIframe__: 1,
1376
+ type: 'request',
1377
+ requestId: requestId,
1378
+ path: 'testJson',
1379
+ body: {}
1380
+ },
1381
+ origin,
1382
+ source: mockContentWindow as any
1383
+ })
1384
+ );
1385
+ await new Promise((resolve) => setTimeout(resolve, 150));
1386
+
1387
+ const responseCall = mockContentWindow.postMessage.mock.calls.find(
1388
+ (call: any[]) => call[0]?.type === 'response'
1389
+ );
1390
+ expect(responseCall).toBeDefined();
1391
+ if (responseCall && responseCall[0] && responseCall[0].headers) {
1392
+ expect(responseCall[0].headers[HttpHeader.CONTENT_TYPE]).toBe('application/json');
1393
+ }
1394
+
1395
+ server.destroy();
1396
+ cleanupIframe(iframe);
1397
+ });
1398
+
1399
+ it('should support res.status()', async () => {
1400
+ const origin = 'https://example.com';
1401
+ const iframe = createTestIframe(origin);
1402
+
1403
+ const mockContentWindow = {
1404
+ postMessage: jest.fn((msg: PostMessageData) => {
1405
+ if (msg.type === 'request') {
1406
+ window.dispatchEvent(
1407
+ new MessageEvent('message', {
1408
+ data: {
1409
+ __requestIframe__: 1,
1410
+ type: 'ack',
1411
+ requestId: msg.requestId,
1412
+ path: msg.path
1413
+ },
1414
+ origin
1415
+ })
1416
+ );
1417
+ setTimeout(() => {
1418
+ window.dispatchEvent(
1419
+ new MessageEvent('message', {
1420
+ data: {
1421
+ __requestIframe__: 1,
1422
+ type: 'response',
1423
+ requestId: msg.requestId,
1424
+ data: { error: 'Not Found' },
1425
+ status: 404,
1426
+ statusText: 'Not Found'
1427
+ },
1428
+ origin
1429
+ })
1430
+ );
1431
+ }, 10);
1432
+ }
1433
+ })
1434
+ };
1435
+ Object.defineProperty(iframe, 'contentWindow', {
1436
+ value: mockContentWindow,
1437
+ writable: true
1438
+ });
1439
+
1440
+ const server = requestIframeServer();
1441
+
1442
+ server.on('testStatus', (req, res) => {
1443
+ res.status(404).send({ error: 'Not Found' });
1444
+ });
1445
+
1446
+ const requestId = 'req-status';
1447
+ window.dispatchEvent(
1448
+ new MessageEvent('message', {
1449
+ data: {
1450
+ __requestIframe__: 1,
1451
+ type: 'request',
1452
+ requestId: requestId,
1453
+ path: 'testStatus',
1454
+ body: {}
1455
+ },
1456
+ origin,
1457
+ source: mockContentWindow as any
1458
+ })
1459
+ );
1460
+ await new Promise((resolve) => setTimeout(resolve, 100));
1461
+
1462
+ const responseCall = mockContentWindow.postMessage.mock.calls.find(
1463
+ (call: any[]) => call[0]?.type === 'response' && call[0]?.status === 404
1464
+ );
1465
+ expect(responseCall).toBeDefined();
1466
+
1467
+ server.destroy();
1468
+ cleanupIframe(iframe);
1469
+ });
1470
+
1471
+ it('should support res.setHeader() with array values', async () => {
1472
+ const origin = 'https://example.com';
1473
+ const iframe = createTestIframe(origin);
1474
+
1475
+ const mockContentWindow = {
1476
+ postMessage: jest.fn()
1477
+ };
1478
+ Object.defineProperty(iframe, 'contentWindow', {
1479
+ value: mockContentWindow,
1480
+ writable: true
1481
+ });
1482
+
1483
+ const server = requestIframeServer();
1484
+
1485
+ server.on('testHeader', (req, res) => {
1486
+ res.setHeader('X-Custom', ['value1', 'value2']);
1487
+ res.send({});
1488
+ });
1489
+
1490
+ const requestId = 'req-header';
1491
+ window.dispatchEvent(
1492
+ new MessageEvent('message', {
1493
+ data: {
1494
+ __requestIframe__: 1,
1495
+ type: 'request',
1496
+ requestId: requestId,
1497
+ path: 'testHeader',
1498
+ body: {}
1499
+ },
1500
+ origin,
1501
+ source: mockContentWindow as any
1502
+ })
1503
+ );
1504
+ await new Promise((resolve) => setTimeout(resolve, 100));
1505
+
1506
+ const responseCall = mockContentWindow.postMessage.mock.calls.find(
1507
+ (call: any[]) => call[0]?.type === 'response'
1508
+ );
1509
+ expect(responseCall).toBeDefined();
1510
+ expect(responseCall[0].headers['X-Custom']).toBe('value1, value2');
1511
+
1512
+ server.destroy();
1513
+ cleanupIframe(iframe);
1514
+ });
1515
+
1516
+ it('should support res.set() method', async () => {
1517
+ const origin = 'https://example.com';
1518
+ const iframe = createTestIframe(origin);
1519
+
1520
+ const mockContentWindow = {
1521
+ postMessage: jest.fn()
1522
+ };
1523
+ Object.defineProperty(iframe, 'contentWindow', {
1524
+ value: mockContentWindow,
1525
+ writable: true
1526
+ });
1527
+
1528
+ const server = requestIframeServer();
1529
+
1530
+ server.on('testSet', (req, res) => {
1531
+ res.set('X-Custom', 'value').send({});
1532
+ });
1533
+
1534
+ const requestId = 'req-set';
1535
+ window.dispatchEvent(
1536
+ new MessageEvent('message', {
1537
+ data: {
1538
+ __requestIframe__: 1,
1539
+ type: 'request',
1540
+ requestId: requestId,
1541
+ path: 'testSet',
1542
+ body: {}
1543
+ },
1544
+ origin,
1545
+ source: mockContentWindow as any
1546
+ })
1547
+ );
1548
+ await new Promise((resolve) => setTimeout(resolve, 100));
1549
+
1550
+ const responseCall = mockContentWindow.postMessage.mock.calls.find(
1551
+ (call: any[]) => call[0]?.type === 'response'
1552
+ );
1553
+ expect(responseCall).toBeDefined();
1554
+ expect(responseCall[0].headers['X-Custom']).toBe('value');
1555
+
1556
+ server.destroy();
1557
+ cleanupIframe(iframe);
1558
+ });
1559
+
1560
+ it('should support res.cookie()', async () => {
1561
+ const origin = 'https://example.com';
1562
+ const iframe = createTestIframe(origin);
1563
+
1564
+ const mockContentWindow = {
1565
+ postMessage: jest.fn()
1566
+ };
1567
+ Object.defineProperty(iframe, 'contentWindow', {
1568
+ value: mockContentWindow,
1569
+ writable: true
1570
+ });
1571
+
1572
+ const server = requestIframeServer();
1573
+
1574
+ server.on('testCookie', (req, res) => {
1575
+ res.cookie('token', 'abc123', {
1576
+ path: '/api',
1577
+ httpOnly: true,
1578
+ secure: true,
1579
+ sameSite: 'strict'
1580
+ });
1581
+ res.send({});
1582
+ });
1583
+
1584
+ const requestId = 'req-cookie';
1585
+ window.dispatchEvent(
1586
+ new MessageEvent('message', {
1587
+ data: {
1588
+ __requestIframe__: 1,
1589
+ type: 'request',
1590
+ requestId: requestId,
1591
+ path: 'testCookie',
1592
+ body: {}
1593
+ },
1594
+ origin,
1595
+ source: mockContentWindow as any
1596
+ })
1597
+ );
1598
+ await new Promise((resolve) => setTimeout(resolve, 100));
1599
+
1600
+ const responseCall = mockContentWindow.postMessage.mock.calls.find(
1601
+ (call: any[]) => call[0]?.type === 'response'
1602
+ );
1603
+ expect(responseCall).toBeDefined();
1604
+ const setCookies = responseCall[0].headers[HttpHeader.SET_COOKIE];
1605
+ expect(Array.isArray(setCookies)).toBe(true);
1606
+ expect(setCookies[0]).toContain('token=abc123');
1607
+ expect(setCookies[0]).toContain('Path=/api');
1608
+
1609
+ server.destroy();
1610
+ cleanupIframe(iframe);
1611
+ });
1612
+
1613
+ it('should support res.clearCookie()', async () => {
1614
+ const origin = 'https://example.com';
1615
+ const iframe = createTestIframe(origin);
1616
+
1617
+ const mockContentWindow = {
1618
+ postMessage: jest.fn()
1619
+ };
1620
+ Object.defineProperty(iframe, 'contentWindow', {
1621
+ value: mockContentWindow,
1622
+ writable: true
1623
+ });
1624
+
1625
+ const server = requestIframeServer();
1626
+
1627
+ server.on('testClearCookie', (req, res) => {
1628
+ res.clearCookie('token', { path: '/api' });
1629
+ res.send({});
1630
+ });
1631
+
1632
+ const requestId = 'req-clearcookie';
1633
+ window.dispatchEvent(
1634
+ new MessageEvent('message', {
1635
+ data: {
1636
+ __requestIframe__: 1,
1637
+ type: 'request',
1638
+ requestId: requestId,
1639
+ path: 'testClearCookie',
1640
+ body: {}
1641
+ },
1642
+ origin,
1643
+ source: mockContentWindow as any
1644
+ })
1645
+ );
1646
+ await new Promise((resolve) => setTimeout(resolve, 100));
1647
+
1648
+ const responseCall = mockContentWindow.postMessage.mock.calls.find(
1649
+ (call: any[]) => call[0]?.type === 'response'
1650
+ );
1651
+ expect(responseCall).toBeDefined();
1652
+ const setCookies = responseCall[0].headers[HttpHeader.SET_COOKIE];
1653
+ expect(setCookies[0]).toContain('Max-Age=0');
1654
+
1655
+ server.destroy();
1656
+ cleanupIframe(iframe);
1657
+ });
1658
+
1659
+ it('should handle async handler without sending response', async () => {
1660
+ const origin = 'https://example.com';
1661
+ const iframe = createTestIframe(origin);
1662
+
1663
+ const mockContentWindow = {
1664
+ postMessage: jest.fn((msg: PostMessageData) => {
1665
+ if (msg.type === 'request') {
1666
+ window.dispatchEvent(
1667
+ new MessageEvent('message', {
1668
+ data: {
1669
+ __requestIframe__: 1,
1670
+ type: 'ack',
1671
+ requestId: msg.requestId,
1672
+ path: msg.path
1673
+ },
1674
+ origin
1675
+ })
1676
+ );
1677
+ setTimeout(() => {
1678
+ window.dispatchEvent(
1679
+ new MessageEvent('message', {
1680
+ data: {
1681
+ __requestIframe__: 1,
1682
+ type: 'async',
1683
+ requestId: msg.requestId,
1684
+ path: msg.path
1685
+ },
1686
+ origin
1687
+ })
1688
+ );
1689
+ }, 10);
1690
+ setTimeout(() => {
1691
+ window.dispatchEvent(
1692
+ new MessageEvent('message', {
1693
+ data: {
1694
+ __requestIframe__: 1,
1695
+ type: 'error',
1696
+ requestId: msg.requestId,
1697
+ error: {
1698
+ message: Messages.NO_RESPONSE_SENT,
1699
+ code: 'NO_RESPONSE'
1700
+ },
1701
+ status: 500
1702
+ },
1703
+ origin
1704
+ })
1705
+ );
1706
+ }, 50);
1707
+ }
1708
+ })
1709
+ };
1710
+ Object.defineProperty(iframe, 'contentWindow', {
1711
+ value: mockContentWindow,
1712
+ writable: true
1713
+ });
1714
+
1715
+ const client = requestIframeClient(iframe);
1716
+ const server = requestIframeServer();
1717
+
1718
+ server.on('asyncNoResponse', async (req, res) => {
1719
+ await new Promise(resolve => setTimeout(resolve, 30));
1720
+ // Intentionally not sending response
1721
+ });
1722
+
1723
+ await expect(
1724
+ client.send('asyncNoResponse', undefined, { ackTimeout: 1000, asyncTimeout: 5000 })
1725
+ ).rejects.toMatchObject({
1726
+ code: 'NO_RESPONSE'
1727
+ });
1728
+
1729
+ server.destroy();
1730
+ cleanupIframe(iframe);
1731
+ });
1732
+
1733
+ it('should handle async handler with promise rejection', async () => {
1734
+ const origin = 'https://example.com';
1735
+ const iframe = createTestIframe(origin);
1736
+
1737
+ const mockContentWindow = {
1738
+ postMessage: jest.fn((msg: PostMessageData) => {
1739
+ if (msg.type === 'request') {
1740
+ window.dispatchEvent(
1741
+ new MessageEvent('message', {
1742
+ data: {
1743
+ __requestIframe__: 1,
1744
+ type: 'ack',
1745
+ requestId: msg.requestId,
1746
+ path: msg.path
1747
+ },
1748
+ origin
1749
+ })
1750
+ );
1751
+ setTimeout(() => {
1752
+ window.dispatchEvent(
1753
+ new MessageEvent('message', {
1754
+ data: {
1755
+ __requestIframe__: 1,
1756
+ type: 'async',
1757
+ requestId: msg.requestId,
1758
+ path: msg.path
1759
+ },
1760
+ origin
1761
+ })
1762
+ );
1763
+ }, 10);
1764
+ setTimeout(() => {
1765
+ window.dispatchEvent(
1766
+ new MessageEvent('message', {
1767
+ data: {
1768
+ __requestIframe__: 1,
1769
+ type: 'error',
1770
+ requestId: msg.requestId,
1771
+ error: {
1772
+ message: 'Test error',
1773
+ code: 'REQUEST_ERROR'
1774
+ },
1775
+ status: 500
1776
+ },
1777
+ origin
1778
+ })
1779
+ );
1780
+ }, 50);
1781
+ }
1782
+ })
1783
+ };
1784
+ Object.defineProperty(iframe, 'contentWindow', {
1785
+ value: mockContentWindow,
1786
+ writable: true
1787
+ });
1788
+
1789
+ const client = requestIframeClient(iframe);
1790
+ const server = requestIframeServer();
1791
+
1792
+ server.on('asyncError', async (req, res) => {
1793
+ await new Promise(resolve => setTimeout(resolve, 30));
1794
+ throw new Error('Test error');
1795
+ });
1796
+
1797
+ await expect(
1798
+ client.send('asyncError', undefined, { ackTimeout: 1000, asyncTimeout: 5000 })
1799
+ ).rejects.toMatchObject({
1800
+ code: 'REQUEST_ERROR'
1801
+ });
1802
+
1803
+ server.destroy();
1804
+ cleanupIframe(iframe);
1805
+ });
1806
+
1807
+ it('should handle middleware error', async () => {
1808
+ const origin = 'https://example.com';
1809
+ const iframe = createTestIframe(origin);
1810
+
1811
+ const mockContentWindow = {
1812
+ postMessage: jest.fn()
1813
+ };
1814
+ Object.defineProperty(iframe, 'contentWindow', {
1815
+ value: mockContentWindow,
1816
+ writable: true
1817
+ });
1818
+
1819
+ const server = requestIframeServer();
1820
+
1821
+ server.use((req, res, next) => {
1822
+ throw new Error('Middleware error');
1823
+ });
1824
+
1825
+ server.on('test', (req, res) => {
1826
+ res.send({});
1827
+ });
1828
+
1829
+ const requestId = 'req-middleware-error';
1830
+ window.dispatchEvent(
1831
+ new MessageEvent('message', {
1832
+ data: {
1833
+ __requestIframe__: 1,
1834
+ type: 'request',
1835
+ requestId: requestId,
1836
+ path: 'test',
1837
+ body: {}
1838
+ },
1839
+ origin,
1840
+ source: mockContentWindow as any
1841
+ })
1842
+ );
1843
+ await new Promise((resolve) => setTimeout(resolve, 100));
1844
+
1845
+ const errorCall = mockContentWindow.postMessage.mock.calls.find(
1846
+ (call: any[]) => call[0]?.type === 'error' ||
1847
+ (call[0]?.type === 'response' && call[0]?.status === 500)
1848
+ );
1849
+ expect(errorCall).toBeDefined();
1850
+
1851
+ server.destroy();
1852
+ cleanupIframe(iframe);
1853
+ });
1854
+
1855
+ it('should handle middleware promise rejection', async () => {
1856
+ const origin = 'https://example.com';
1857
+ const iframe = createTestIframe(origin);
1858
+
1859
+ const mockContentWindow = {
1860
+ postMessage: jest.fn()
1861
+ };
1862
+ Object.defineProperty(iframe, 'contentWindow', {
1863
+ value: mockContentWindow,
1864
+ writable: true
1865
+ });
1866
+
1867
+ const server = requestIframeServer();
1868
+
1869
+ server.use(async (req, res, next) => {
1870
+ await new Promise(resolve => setTimeout(resolve, 10));
1871
+ throw new Error('Async middleware error');
1872
+ });
1873
+
1874
+ server.on('test', (req, res) => {
1875
+ res.send({});
1876
+ });
1877
+
1878
+ const requestId = 'req-middleware-async-error';
1879
+ window.dispatchEvent(
1880
+ new MessageEvent('message', {
1881
+ data: {
1882
+ __requestIframe__: 1,
1883
+ type: 'request',
1884
+ requestId: requestId,
1885
+ path: 'test',
1886
+ body: {}
1887
+ },
1888
+ origin,
1889
+ source: mockContentWindow as any
1890
+ })
1891
+ );
1892
+ await new Promise((resolve) => setTimeout(resolve, 150));
1893
+
1894
+ const errorCall = mockContentWindow.postMessage.mock.calls.find(
1895
+ (call: any[]) => call[0]?.type === 'error' ||
1896
+ (call[0]?.type === 'response' && call[0]?.status === 500)
1897
+ );
1898
+ expect(errorCall).toBeDefined();
1899
+
1900
+ server.destroy();
1901
+ cleanupIframe(iframe);
1902
+ });
1903
+
1904
+ it('should handle request without path', async () => {
1905
+ const origin = 'https://example.com';
1906
+ const iframe = createTestIframe(origin);
1907
+
1908
+ const mockContentWindow = {
1909
+ postMessage: jest.fn()
1910
+ };
1911
+ Object.defineProperty(iframe, 'contentWindow', {
1912
+ value: mockContentWindow,
1913
+ writable: true
1914
+ });
1915
+
1916
+ const server = requestIframeServer();
1917
+
1918
+ const requestId = 'req-no-path';
1919
+ window.dispatchEvent(
1920
+ new MessageEvent('message', {
1921
+ data: {
1922
+ __requestIframe__: 1,
1923
+ type: 'request',
1924
+ requestId: requestId,
1925
+ body: {}
1926
+ },
1927
+ origin,
1928
+ source: mockContentWindow as any
1929
+ })
1930
+ );
1931
+ await new Promise((resolve) => setTimeout(resolve, 50));
1932
+
1933
+ // Should not crash, but also shouldn't process the request (no path means early return)
1934
+ // Server should still send ACK, but won't process further
1935
+ const ackCall = mockContentWindow.postMessage.mock.calls.find(
1936
+ (call: any[]) => call[0]?.type === 'ack'
1937
+ );
1938
+ // Server may or may not send ACK if path is missing, but should not crash
1939
+ expect(() => server.destroy()).not.toThrow();
1940
+
1941
+ cleanupIframe(iframe);
1942
+ });
1943
+
1944
+ it('should handle request without source', async () => {
1945
+ const origin = 'https://example.com';
1946
+ const iframe = createTestIframe(origin);
1947
+
1948
+ const mockContentWindow = {
1949
+ postMessage: jest.fn()
1950
+ };
1951
+ Object.defineProperty(iframe, 'contentWindow', {
1952
+ value: mockContentWindow,
1953
+ writable: true
1954
+ });
1955
+
1956
+ const server = requestIframeServer();
1957
+
1958
+ const requestId = 'req-no-source';
1959
+ window.dispatchEvent(
1960
+ new MessageEvent('message', {
1961
+ data: {
1962
+ __requestIframe__: 1,
1963
+ type: 'request',
1964
+ requestId: requestId,
1965
+ path: 'test',
1966
+ body: {}
1967
+ },
1968
+ origin
1969
+ // Intentionally no source
1970
+ })
1971
+ );
1972
+ await new Promise((resolve) => setTimeout(resolve, 50));
1973
+
1974
+ // Should not crash
1975
+ server.destroy();
1976
+ cleanupIframe(iframe);
1977
+ });
1978
+ });
1979
+
1980
+ describe('Stream response', () => {
1981
+ it('should support sendStream', async () => {
1982
+ const origin = 'https://example.com';
1983
+ const iframe = createTestIframe(origin);
1984
+
1985
+ const mockContentWindow = {
1986
+ postMessage: jest.fn((msg: PostMessageData) => {
1987
+ if (msg.type === 'request') {
1988
+ window.dispatchEvent(
1989
+ new MessageEvent('message', {
1990
+ data: {
1991
+ __requestIframe__: 1,
1992
+ type: 'ack',
1993
+ requestId: msg.requestId,
1994
+ path: msg.path
1995
+ },
1996
+ origin
1997
+ })
1998
+ );
1999
+ setTimeout(() => {
2000
+ window.dispatchEvent(
2001
+ new MessageEvent('message', {
2002
+ data: {
2003
+ __requestIframe__: 1,
2004
+ type: 'stream_start',
2005
+ requestId: msg.requestId,
2006
+ body: {
2007
+ streamId: 'stream-123',
2008
+ type: 'data',
2009
+ chunked: true
2010
+ }
2011
+ },
2012
+ origin
2013
+ })
2014
+ );
2015
+ }, 10);
2016
+ }
2017
+ })
2018
+ };
2019
+ Object.defineProperty(iframe, 'contentWindow', {
2020
+ value: mockContentWindow,
2021
+ writable: true
2022
+ });
2023
+
2024
+ const client = requestIframeClient(iframe);
2025
+ const server = requestIframeServer();
2026
+
2027
+ server.on('getStream', async (req, res) => {
2028
+ const { IframeWritableStream } = await import('../stream');
2029
+ const stream = new IframeWritableStream({
2030
+ iterator: async function* () {
2031
+ yield 'chunk1';
2032
+ yield 'chunk2';
2033
+ }
2034
+ });
2035
+ await res.sendStream(stream);
2036
+ });
2037
+
2038
+ const requestId = 'req-stream';
2039
+ window.dispatchEvent(
2040
+ new MessageEvent('message', {
2041
+ data: {
2042
+ __requestIframe__: 1,
2043
+ type: 'request',
2044
+ requestId: requestId,
2045
+ path: 'getStream',
2046
+ body: {}
2047
+ },
2048
+ origin,
2049
+ source: mockContentWindow as any
2050
+ })
2051
+ );
2052
+ await new Promise((resolve) => setTimeout(resolve, 150));
2053
+
2054
+ const streamStartCall = mockContentWindow.postMessage.mock.calls.find(
2055
+ (call: any[]) => call[0]?.type === 'stream_start'
2056
+ );
2057
+ expect(streamStartCall).toBeDefined();
2058
+
2059
+ server.destroy();
2060
+ cleanupIframe(iframe);
2061
+ });
2062
+
2063
+ it('should handle stream response from server', async () => {
2064
+ const origin = 'https://example.com';
2065
+ const iframe = createTestIframe(origin);
2066
+
2067
+ const mockContentWindow = {
2068
+ postMessage: jest.fn((msg: PostMessageData) => {
2069
+ if (msg.type === 'request') {
2070
+ window.dispatchEvent(
2071
+ new MessageEvent('message', {
2072
+ data: {
2073
+ __requestIframe__: 1,
2074
+ type: 'ack',
2075
+ requestId: msg.requestId,
2076
+ path: msg.path
2077
+ },
2078
+ origin
2079
+ })
2080
+ );
2081
+ setTimeout(() => {
2082
+ window.dispatchEvent(
2083
+ new MessageEvent('message', {
2084
+ data: {
2085
+ __requestIframe__: 1,
2086
+ type: 'stream_start',
2087
+ requestId: msg.requestId,
2088
+ body: {
2089
+ streamId: 'stream-123',
2090
+ type: 'data',
2091
+ chunked: true
2092
+ }
2093
+ },
2094
+ origin
2095
+ })
2096
+ );
2097
+ }, 10);
2098
+ }
2099
+ })
2100
+ };
2101
+ Object.defineProperty(iframe, 'contentWindow', {
2102
+ value: mockContentWindow,
2103
+ writable: true
2104
+ });
2105
+
2106
+ const client = requestIframeClient(iframe);
2107
+ const server = requestIframeServer();
2108
+
2109
+ server.on('getStream', async (req, res) => {
2110
+ const { IframeWritableStream } = await import('../stream');
2111
+ const stream = new IframeWritableStream({
2112
+ iterator: async function* () {
2113
+ yield 'chunk1';
2114
+ yield 'chunk2';
2115
+ }
2116
+ });
2117
+ await res.sendStream(stream);
2118
+ });
2119
+
2120
+ const requestId = 'req-stream';
2121
+ window.dispatchEvent(
2122
+ new MessageEvent('message', {
2123
+ data: {
2124
+ __requestIframe__: 1,
2125
+ type: 'request',
2126
+ requestId: requestId,
2127
+ path: 'getStream',
2128
+ body: {}
2129
+ },
2130
+ origin,
2131
+ source: mockContentWindow as any
2132
+ })
2133
+ );
2134
+ await new Promise((resolve) => setTimeout(resolve, 150));
2135
+
2136
+ const streamStartCall = mockContentWindow.postMessage.mock.calls.find(
2137
+ (call: any[]) => call[0]?.type === 'stream_start'
2138
+ );
2139
+ expect(streamStartCall).toBeDefined();
2140
+
2141
+ server.destroy();
2142
+ cleanupIframe(iframe);
2143
+ });
2144
+
2145
+ it('should handle server open/close methods', () => {
2146
+ const origin = 'https://example.com';
2147
+ const iframe = createTestIframe(origin);
2148
+
2149
+ const mockContentWindow = {
2150
+ postMessage: jest.fn()
2151
+ };
2152
+ Object.defineProperty(iframe, 'contentWindow', {
2153
+ value: mockContentWindow,
2154
+ writable: true
2155
+ });
2156
+
2157
+ const server = requestIframeServer();
2158
+
2159
+ expect(server.isOpen).toBe(true);
2160
+
2161
+ server.close();
2162
+ expect(server.isOpen).toBe(false);
2163
+
2164
+ server.open();
2165
+ expect(server.isOpen).toBe(true);
2166
+
2167
+ server.destroy();
2168
+ cleanupIframe(iframe);
2169
+ });
2170
+
2171
+ it('should handle server off method', async () => {
2172
+ const origin = 'https://example.com';
2173
+ const iframe = createTestIframe(origin);
2174
+
2175
+ const mockContentWindow = {
2176
+ postMessage: jest.fn()
2177
+ };
2178
+ Object.defineProperty(iframe, 'contentWindow', {
2179
+ value: mockContentWindow,
2180
+ writable: true
2181
+ });
2182
+
2183
+ const server = requestIframeServer();
2184
+
2185
+ server.on('test', (req, res) => {
2186
+ res.send({});
2187
+ });
2188
+
2189
+ server.off('test');
2190
+
2191
+ const requestId = 'req-off';
2192
+ window.dispatchEvent(
2193
+ new MessageEvent('message', {
2194
+ data: {
2195
+ __requestIframe__: 1,
2196
+ type: 'request',
2197
+ requestId: requestId,
2198
+ path: 'test',
2199
+ body: {}
2200
+ },
2201
+ origin,
2202
+ source: mockContentWindow as any
2203
+ })
2204
+ );
2205
+ await new Promise((resolve) => setTimeout(resolve, 100));
2206
+
2207
+ const errorCall = mockContentWindow.postMessage.mock.calls.find(
2208
+ (call: any[]) => call[0]?.type === 'error' && call[0]?.error?.code === 'METHOD_NOT_FOUND'
2209
+ );
2210
+ expect(errorCall).toBeDefined();
2211
+
2212
+ server.destroy();
2213
+ cleanupIframe(iframe);
2214
+ });
2215
+ });
2216
+ });