request-iframe 0.0.2 → 0.0.4

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 (84) hide show
  1. package/QUICKSTART.CN.md +35 -8
  2. package/QUICKSTART.md +35 -8
  3. package/README.CN.md +439 -36
  4. package/README.md +496 -30
  5. package/library/__tests__/channel.test.ts +420 -0
  6. package/library/__tests__/coverage-branches.test.ts +356 -0
  7. package/library/__tests__/debug.test.ts +588 -0
  8. package/library/__tests__/dispatcher.test.ts +481 -0
  9. package/library/__tests__/requestIframe.test.ts +3163 -185
  10. package/library/__tests__/server.test.ts +738 -0
  11. package/library/__tests__/stream.test.ts +46 -15
  12. package/library/api/client.d.ts.map +1 -1
  13. package/library/api/client.js +12 -6
  14. package/library/api/server.d.ts +4 -3
  15. package/library/api/server.d.ts.map +1 -1
  16. package/library/api/server.js +25 -7
  17. package/library/constants/index.d.ts +14 -4
  18. package/library/constants/index.d.ts.map +1 -1
  19. package/library/constants/index.js +15 -7
  20. package/library/constants/messages.d.ts +37 -0
  21. package/library/constants/messages.d.ts.map +1 -1
  22. package/library/constants/messages.js +38 -1
  23. package/library/core/client-server.d.ts +105 -0
  24. package/library/core/client-server.d.ts.map +1 -0
  25. package/library/core/client-server.js +289 -0
  26. package/library/core/client.d.ts +53 -10
  27. package/library/core/client.d.ts.map +1 -1
  28. package/library/core/client.js +529 -207
  29. package/library/core/request.d.ts +3 -1
  30. package/library/core/request.d.ts.map +1 -1
  31. package/library/core/request.js +2 -1
  32. package/library/core/response.d.ts +30 -4
  33. package/library/core/response.d.ts.map +1 -1
  34. package/library/core/response.js +176 -100
  35. package/library/core/server-client.d.ts +3 -1
  36. package/library/core/server-client.d.ts.map +1 -1
  37. package/library/core/server-client.js +19 -9
  38. package/library/core/server.d.ts +22 -1
  39. package/library/core/server.d.ts.map +1 -1
  40. package/library/core/server.js +304 -55
  41. package/library/index.d.ts +3 -2
  42. package/library/index.d.ts.map +1 -1
  43. package/library/index.js +34 -5
  44. package/library/interceptors/index.d.ts.map +1 -1
  45. package/library/message/channel.d.ts +3 -1
  46. package/library/message/channel.d.ts.map +1 -1
  47. package/library/message/dispatcher.d.ts +7 -2
  48. package/library/message/dispatcher.d.ts.map +1 -1
  49. package/library/message/dispatcher.js +48 -2
  50. package/library/message/index.d.ts.map +1 -1
  51. package/library/stream/file-stream.d.ts +5 -0
  52. package/library/stream/file-stream.d.ts.map +1 -1
  53. package/library/stream/file-stream.js +41 -12
  54. package/library/stream/index.d.ts +11 -1
  55. package/library/stream/index.d.ts.map +1 -1
  56. package/library/stream/index.js +21 -3
  57. package/library/stream/readable-stream.d.ts.map +1 -1
  58. package/library/stream/readable-stream.js +32 -30
  59. package/library/stream/types.d.ts +20 -2
  60. package/library/stream/types.d.ts.map +1 -1
  61. package/library/stream/writable-stream.d.ts +2 -1
  62. package/library/stream/writable-stream.d.ts.map +1 -1
  63. package/library/stream/writable-stream.js +13 -10
  64. package/library/types/index.d.ts +106 -32
  65. package/library/types/index.d.ts.map +1 -1
  66. package/library/utils/cache.d.ts +24 -0
  67. package/library/utils/cache.d.ts.map +1 -1
  68. package/library/utils/cache.js +76 -0
  69. package/library/utils/cookie.d.ts.map +1 -1
  70. package/library/utils/debug.d.ts.map +1 -1
  71. package/library/utils/debug.js +382 -20
  72. package/library/utils/index.d.ts +19 -0
  73. package/library/utils/index.d.ts.map +1 -1
  74. package/library/utils/index.js +113 -2
  75. package/library/utils/path-match.d.ts +16 -0
  76. package/library/utils/path-match.d.ts.map +1 -1
  77. package/library/utils/path-match.js +65 -0
  78. package/library/utils/protocol.d.ts.map +1 -1
  79. package/package.json +4 -1
  80. package/react/library/__tests__/index.test.tsx +274 -281
  81. package/react/library/index.d.ts +4 -3
  82. package/react/library/index.d.ts.map +1 -1
  83. package/react/library/index.js +225 -158
  84. package/react/package.json +7 -0
@@ -1,7 +1,8 @@
1
1
  import { requestIframeClient, clearRequestIframeClientCache } from '../api/client';
2
2
  import { requestIframeServer, clearRequestIframeServerCache } from '../api/server';
3
3
  import { RequestConfig, Response, ErrorResponse, PostMessageData } from '../types';
4
- import { HttpHeader, Messages } from '../constants';
4
+ import { HttpHeader, MessageRole, Messages } from '../constants';
5
+ import { IframeWritableStream } from '../stream';
5
6
 
6
7
  /**
7
8
  * Create test iframe
@@ -22,6 +23,18 @@ function cleanupIframe(iframe: HTMLIFrameElement): void {
22
23
  }
23
24
  }
24
25
 
26
+ /**
27
+ * Convert Blob to text (for assertions)
28
+ */
29
+ function blobToText(blob: Blob): Promise<string> {
30
+ return new Promise((resolve, reject) => {
31
+ const reader = new FileReader();
32
+ reader.onload = () => resolve(String(reader.result ?? ''));
33
+ reader.onerror = reject;
34
+ reader.readAsText(blob);
35
+ });
36
+ }
37
+
25
38
  describe('requestIframeClient and requestIframeServer', () => {
26
39
  beforeEach(() => {
27
40
  // Clear all caches
@@ -64,7 +77,8 @@ describe('requestIframeClient and requestIframeServer', () => {
64
77
  __requestIframe__: 1,
65
78
  type: 'ack',
66
79
  requestId: msg.requestId,
67
- path: msg.path
80
+ path: msg.path,
81
+ role: MessageRole.SERVER
68
82
  },
69
83
  origin
70
84
  })
@@ -79,7 +93,8 @@ describe('requestIframeClient and requestIframeServer', () => {
79
93
  requestId: msg.requestId,
80
94
  data: { result: 'success' },
81
95
  status: 200,
82
- statusText: 'OK'
96
+ statusText: 'OK',
97
+ role: MessageRole.SERVER
83
98
  },
84
99
  origin
85
100
  })
@@ -108,6 +123,251 @@ describe('requestIframeClient and requestIframeServer', () => {
108
123
  cleanupIframe(iframe);
109
124
  });
110
125
 
126
+ it('should return response.data when returnData is true in options', async () => {
127
+ const origin = 'https://example.com';
128
+ const iframe = createTestIframe(origin);
129
+
130
+ const mockContentWindow = {
131
+ postMessage: jest.fn((msg: PostMessageData) => {
132
+ if (msg.type === 'request') {
133
+ window.dispatchEvent(
134
+ new MessageEvent('message', {
135
+ data: {
136
+ __requestIframe__: 1,
137
+ type: 'ack',
138
+ requestId: msg.requestId,
139
+ path: msg.path,
140
+ role: MessageRole.SERVER
141
+ },
142
+ origin
143
+ })
144
+ );
145
+ setTimeout(() => {
146
+ window.dispatchEvent(
147
+ new MessageEvent('message', {
148
+ data: {
149
+ __requestIframe__: 1,
150
+ type: 'response',
151
+ requestId: msg.requestId,
152
+ data: { result: 'success' },
153
+ status: 200,
154
+ statusText: 'OK',
155
+ role: MessageRole.SERVER
156
+ },
157
+ origin
158
+ })
159
+ );
160
+ }, 10);
161
+ }
162
+ })
163
+ };
164
+ Object.defineProperty(iframe, 'contentWindow', {
165
+ value: mockContentWindow,
166
+ writable: true
167
+ });
168
+
169
+ const client = requestIframeClient(iframe);
170
+ const server = requestIframeServer();
171
+
172
+ server.on('test', (req, res) => {
173
+ res.send({ result: 'success' });
174
+ });
175
+
176
+ const data = await client.send('test', { param: 'value' }, { ackTimeout: 1000, returnData: true });
177
+ // Should return data directly, not Response object
178
+ expect(data).toEqual({ result: 'success' });
179
+ expect((data as any).status).toBeUndefined();
180
+ expect((data as any).requestId).toBeUndefined();
181
+
182
+ server.destroy();
183
+ cleanupIframe(iframe);
184
+ });
185
+
186
+ it('should return full Response when returnData is false', async () => {
187
+ const origin = 'https://example.com';
188
+ const iframe = createTestIframe(origin);
189
+
190
+ const mockContentWindow = {
191
+ postMessage: jest.fn((msg: PostMessageData) => {
192
+ if (msg.type === 'request') {
193
+ window.dispatchEvent(
194
+ new MessageEvent('message', {
195
+ data: {
196
+ __requestIframe__: 1,
197
+ type: 'ack',
198
+ requestId: msg.requestId,
199
+ path: msg.path,
200
+ role: MessageRole.SERVER
201
+ },
202
+ origin
203
+ })
204
+ );
205
+ setTimeout(() => {
206
+ window.dispatchEvent(
207
+ new MessageEvent('message', {
208
+ data: {
209
+ __requestIframe__: 1,
210
+ type: 'response',
211
+ requestId: msg.requestId,
212
+ data: { result: 'success' },
213
+ status: 200,
214
+ statusText: 'OK',
215
+ role: MessageRole.SERVER
216
+ },
217
+ origin
218
+ })
219
+ );
220
+ }, 10);
221
+ }
222
+ })
223
+ };
224
+ Object.defineProperty(iframe, 'contentWindow', {
225
+ value: mockContentWindow,
226
+ writable: true
227
+ });
228
+
229
+ const client = requestIframeClient(iframe);
230
+ const server = requestIframeServer();
231
+
232
+ server.on('test', (req, res) => {
233
+ res.send({ result: 'success' });
234
+ });
235
+
236
+ const response = await client.send('test', { param: 'value' }, { ackTimeout: 1000, returnData: false });
237
+ // Should return full Response object
238
+ expect(response).toHaveProperty('data');
239
+ expect(response).toHaveProperty('status');
240
+ expect(response).toHaveProperty('statusText');
241
+ expect(response).toHaveProperty('requestId');
242
+ expect(response.data).toEqual({ result: 'success' });
243
+ expect(response.status).toBe(200);
244
+
245
+ server.destroy();
246
+ cleanupIframe(iframe);
247
+ });
248
+
249
+ it('should use default returnData from RequestIframeClientOptions', async () => {
250
+ const origin = 'https://example.com';
251
+ const iframe = createTestIframe(origin);
252
+
253
+ const mockContentWindow = {
254
+ postMessage: jest.fn((msg: PostMessageData) => {
255
+ if (msg.type === 'request') {
256
+ window.dispatchEvent(
257
+ new MessageEvent('message', {
258
+ data: {
259
+ __requestIframe__: 1,
260
+ type: 'ack',
261
+ requestId: msg.requestId,
262
+ path: msg.path,
263
+ role: MessageRole.SERVER
264
+ },
265
+ origin
266
+ })
267
+ );
268
+ setTimeout(() => {
269
+ window.dispatchEvent(
270
+ new MessageEvent('message', {
271
+ data: {
272
+ __requestIframe__: 1,
273
+ type: 'response',
274
+ requestId: msg.requestId,
275
+ data: { result: 'success' },
276
+ status: 200,
277
+ statusText: 'OK',
278
+ role: MessageRole.SERVER
279
+ },
280
+ origin
281
+ })
282
+ );
283
+ }, 10);
284
+ }
285
+ })
286
+ };
287
+ Object.defineProperty(iframe, 'contentWindow', {
288
+ value: mockContentWindow,
289
+ writable: true
290
+ });
291
+
292
+ // Create client with returnData: true in options
293
+ const client = requestIframeClient(iframe, { returnData: true });
294
+ const server = requestIframeServer();
295
+
296
+ server.on('test', (req, res) => {
297
+ res.send({ result: 'success' });
298
+ });
299
+
300
+ // Should return data directly without specifying returnData in send options
301
+ const data = await client.send('test', { param: 'value' }, { ackTimeout: 1000 });
302
+ expect(data).toEqual({ result: 'success' });
303
+ expect((data as any).status).toBeUndefined();
304
+
305
+ server.destroy();
306
+ cleanupIframe(iframe);
307
+ });
308
+
309
+ it('should allow overriding default returnData in send options', async () => {
310
+ const origin = 'https://example.com';
311
+ const iframe = createTestIframe(origin);
312
+
313
+ const mockContentWindow = {
314
+ postMessage: jest.fn((msg: PostMessageData) => {
315
+ if (msg.type === 'request') {
316
+ window.dispatchEvent(
317
+ new MessageEvent('message', {
318
+ data: {
319
+ __requestIframe__: 1,
320
+ type: 'ack',
321
+ requestId: msg.requestId,
322
+ path: msg.path,
323
+ role: MessageRole.SERVER
324
+ },
325
+ origin
326
+ })
327
+ );
328
+ setTimeout(() => {
329
+ window.dispatchEvent(
330
+ new MessageEvent('message', {
331
+ data: {
332
+ __requestIframe__: 1,
333
+ type: 'response',
334
+ requestId: msg.requestId,
335
+ data: { result: 'success' },
336
+ status: 200,
337
+ statusText: 'OK',
338
+ role: MessageRole.SERVER
339
+ },
340
+ origin
341
+ })
342
+ );
343
+ }, 10);
344
+ }
345
+ })
346
+ };
347
+ Object.defineProperty(iframe, 'contentWindow', {
348
+ value: mockContentWindow,
349
+ writable: true
350
+ });
351
+
352
+ // Create client with returnData: true in options
353
+ const client = requestIframeClient(iframe, { returnData: true });
354
+ const server = requestIframeServer();
355
+
356
+ server.on('test', (req, res) => {
357
+ res.send({ result: 'success' });
358
+ });
359
+
360
+ // Override with returnData: false in send options
361
+ const response = await client.send('test', { param: 'value' }, { ackTimeout: 1000, returnData: false });
362
+ // Should return full Response object despite default being true
363
+ expect(response).toHaveProperty('data');
364
+ expect(response).toHaveProperty('status');
365
+ expect(response.data).toEqual({ result: 'success' });
366
+
367
+ server.destroy();
368
+ cleanupIframe(iframe);
369
+ });
370
+
111
371
  it('should throw error when iframe.contentWindow is unavailable', () => {
112
372
  const iframe = document.createElement('iframe');
113
373
  iframe.src = 'https://example.com/test.html';
@@ -157,7 +417,8 @@ describe('requestIframeClient and requestIframeServer', () => {
157
417
  __requestIframe__: 1,
158
418
  type: 'pong',
159
419
  requestId: msg.requestId,
160
- secretKey: msg.secretKey
420
+ secretKey: msg.secretKey,
421
+ role: MessageRole.SERVER
161
422
  },
162
423
  origin
163
424
  })
@@ -199,7 +460,8 @@ describe('requestIframeClient and requestIframeServer', () => {
199
460
  __requestIframe__: 1,
200
461
  type: 'ack',
201
462
  requestId: msg.requestId,
202
- path: msg.path
463
+ path: msg.path,
464
+ role: MessageRole.SERVER
203
465
  },
204
466
  origin
205
467
  })
@@ -215,7 +477,8 @@ describe('requestIframeClient and requestIframeServer', () => {
215
477
  requestId: msg.requestId,
216
478
  data: { success: true },
217
479
  status: 200,
218
- statusText: 'OK'
480
+ statusText: 'OK',
481
+ role: MessageRole.SERVER
219
482
  },
220
483
  origin
221
484
  })
@@ -262,7 +525,8 @@ describe('requestIframeClient and requestIframeServer', () => {
262
525
  __requestIframe__: 1,
263
526
  type: 'ack',
264
527
  requestId: msg.requestId,
265
- path: msg.path
528
+ path: msg.path,
529
+ role: MessageRole.SERVER
266
530
  },
267
531
  origin
268
532
  })
@@ -278,7 +542,8 @@ describe('requestIframeClient and requestIframeServer', () => {
278
542
  requestId: msg.requestId,
279
543
  data: { success: true },
280
544
  status: 200,
281
- statusText: 'OK'
545
+ statusText: 'OK',
546
+ role: MessageRole.SERVER
282
547
  },
283
548
  origin
284
549
  })
@@ -328,7 +593,8 @@ describe('requestIframeClient and requestIframeServer', () => {
328
593
  __requestIframe__: 1,
329
594
  type: 'ack',
330
595
  requestId: msg.requestId,
331
- path: msg.path
596
+ path: msg.path,
597
+ role: MessageRole.SERVER
332
598
  },
333
599
  origin
334
600
  })
@@ -347,7 +613,8 @@ describe('requestIframeClient and requestIframeServer', () => {
347
613
  code: 'METHOD_NOT_FOUND'
348
614
  },
349
615
  status: 404,
350
- statusText: 'Not Found'
616
+ statusText: 'Not Found',
617
+ role: MessageRole.SERVER
351
618
  },
352
619
  origin
353
620
  })
@@ -389,7 +656,8 @@ describe('requestIframeClient and requestIframeServer', () => {
389
656
  __requestIframe__: 1,
390
657
  type: 'ack',
391
658
  requestId: msg.requestId,
392
- path: msg.path
659
+ path: msg.path,
660
+ role: MessageRole.SERVER
393
661
  },
394
662
  origin
395
663
  })
@@ -403,7 +671,8 @@ describe('requestIframeClient and requestIframeServer', () => {
403
671
  __requestIframe__: 1,
404
672
  type: 'async',
405
673
  requestId: msg.requestId,
406
- path: msg.path
674
+ path: msg.path,
675
+ role: MessageRole.SERVER
407
676
  },
408
677
  origin
409
678
  })
@@ -420,7 +689,8 @@ describe('requestIframeClient and requestIframeServer', () => {
420
689
  requestId: msg.requestId,
421
690
  data: { result: 'async success' },
422
691
  status: 200,
423
- statusText: 'OK'
692
+ statusText: 'OK',
693
+ role: MessageRole.SERVER
424
694
  },
425
695
  origin
426
696
  })
@@ -513,30 +783,338 @@ describe('requestIframeClient and requestIframeServer', () => {
513
783
  });
514
784
  });
515
785
 
516
- describe('Middleware', () => {
517
- it('should support global middleware', async () => {
786
+ describe('secretKey message isolation', () => {
787
+ it('should successfully communicate when client and server use the same secretKey', async () => {
518
788
  const origin = 'https://example.com';
519
789
  const iframe = createTestIframe(origin);
520
790
 
521
791
  const mockContentWindow = {
522
- postMessage: jest.fn()
792
+ postMessage: jest.fn((msg: PostMessageData) => {
793
+ if (msg.type === 'request') {
794
+ // Verify secretKey is in message
795
+ expect(msg.secretKey).toBe('test-key');
796
+ // Verify path is NOT prefixed with secretKey
797
+ expect(msg.path).toBe('test');
798
+
799
+ // Send ACK first
800
+ window.dispatchEvent(
801
+ new MessageEvent('message', {
802
+ data: {
803
+ __requestIframe__: 1,
804
+ type: 'ack',
805
+ requestId: msg.requestId,
806
+ path: msg.path,
807
+ secretKey: 'test-key',
808
+ role: MessageRole.SERVER
809
+ },
810
+ origin
811
+ })
812
+ );
813
+ // Then send response
814
+ setTimeout(() => {
815
+ window.dispatchEvent(
816
+ new MessageEvent('message', {
817
+ data: {
818
+ __requestIframe__: 1,
819
+ type: 'response',
820
+ requestId: msg.requestId,
821
+ data: { result: 'success' },
822
+ status: 200,
823
+ statusText: 'OK',
824
+ secretKey: 'test-key',
825
+ role: MessageRole.SERVER
826
+ },
827
+ origin
828
+ })
829
+ );
830
+ }, 10);
831
+ }
832
+ })
523
833
  };
524
834
  Object.defineProperty(iframe, 'contentWindow', {
525
835
  value: mockContentWindow,
526
836
  writable: true
527
837
  });
528
838
 
529
- const client = requestIframeClient(iframe);
530
- const server = requestIframeServer();
839
+ const client = requestIframeClient(iframe, { secretKey: 'test-key' });
840
+ const server = requestIframeServer({ secretKey: 'test-key' });
531
841
 
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
- });
842
+ server.on('test', (req, res) => {
843
+ res.send({ result: 'success' });
844
+ });
845
+
846
+ const response = await client.send('test', { param: 'value' }, { ackTimeout: 1000 });
847
+ expect(response.data).toEqual({ result: 'success' });
848
+ expect(response.status).toBe(200);
849
+ expect(mockContentWindow.postMessage).toHaveBeenCalled();
850
+
851
+ // Verify the sent message has secretKey
852
+ const sentMessage = (mockContentWindow.postMessage as jest.Mock).mock.calls[0][0];
853
+ expect(sentMessage.secretKey).toBe('test-key');
854
+ expect(sentMessage.path).toBe('test'); // Path should NOT be prefixed
855
+
856
+ server.destroy();
857
+ cleanupIframe(iframe);
858
+ });
859
+
860
+ it('should NOT communicate when client and server use different secretKeys', async () => {
861
+ const origin = 'https://example.com';
862
+ const iframe = createTestIframe(origin);
863
+
864
+ const mockContentWindow = {
865
+ postMessage: jest.fn()
866
+ };
867
+ Object.defineProperty(iframe, 'contentWindow', {
868
+ value: mockContentWindow,
869
+ writable: true
870
+ });
871
+
872
+ const client = requestIframeClient(iframe, { secretKey: 'client-key' });
873
+ const server = requestIframeServer({ secretKey: 'server-key' });
874
+
875
+ server.on('test', (req, res) => {
876
+ res.send({ result: 'success' });
877
+ });
878
+
879
+ // Request should timeout because server won't respond (different secretKey)
880
+ await expect(
881
+ client.send('test', { param: 'value' }, { ackTimeout: 100 })
882
+ ).rejects.toMatchObject({
883
+ code: 'ACK_TIMEOUT'
884
+ });
885
+
886
+ server.destroy();
887
+ cleanupIframe(iframe);
888
+ });
889
+
890
+ it('should NOT communicate when client has secretKey but server does not', async () => {
891
+ const origin = 'https://example.com';
892
+ const iframe = createTestIframe(origin);
893
+
894
+ const mockContentWindow = {
895
+ postMessage: jest.fn()
896
+ };
897
+ Object.defineProperty(iframe, 'contentWindow', {
898
+ value: mockContentWindow,
899
+ writable: true
900
+ });
901
+
902
+ const client = requestIframeClient(iframe, { secretKey: 'client-key' });
903
+ const server = requestIframeServer(); // No secretKey
904
+
905
+ server.on('test', (req, res) => {
906
+ res.send({ result: 'success' });
907
+ });
908
+
909
+ // Request should timeout because server won't respond (different secretKey)
910
+ await expect(
911
+ client.send('test', { param: 'value' }, { ackTimeout: 100 })
912
+ ).rejects.toMatchObject({
913
+ code: 'ACK_TIMEOUT'
914
+ });
915
+
916
+ server.destroy();
917
+ cleanupIframe(iframe);
918
+ });
919
+
920
+ it('should NOT communicate when client has no secretKey but server has secretKey', async () => {
921
+ const origin = 'https://example.com';
922
+ const iframe = createTestIframe(origin);
923
+
924
+ const mockContentWindow = {
925
+ postMessage: jest.fn()
926
+ };
927
+ Object.defineProperty(iframe, 'contentWindow', {
928
+ value: mockContentWindow,
929
+ writable: true
930
+ });
931
+
932
+ const client = requestIframeClient(iframe); // No secretKey
933
+ const server = requestIframeServer({ secretKey: 'server-key' });
934
+
935
+ server.on('test', (req, res) => {
936
+ res.send({ result: 'success' });
937
+ });
938
+
939
+ // Request should timeout because server won't respond (different secretKey)
940
+ await expect(
941
+ client.send('test', { param: 'value' }, { ackTimeout: 100 })
942
+ ).rejects.toMatchObject({
943
+ code: 'ACK_TIMEOUT'
944
+ });
945
+
946
+ server.destroy();
947
+ cleanupIframe(iframe);
948
+ });
949
+
950
+ it('should successfully communicate when both client and server have no secretKey', async () => {
951
+ const origin = 'https://example.com';
952
+ const iframe = createTestIframe(origin);
953
+
954
+ const mockContentWindow = {
955
+ postMessage: jest.fn((msg: PostMessageData) => {
956
+ if (msg.type === 'request') {
957
+ // Verify secretKey is NOT in message
958
+ expect(msg.secretKey).toBeUndefined();
959
+ // Verify path is NOT prefixed
960
+ expect(msg.path).toBe('test');
961
+
962
+ // Send ACK first
963
+ window.dispatchEvent(
964
+ new MessageEvent('message', {
965
+ data: {
966
+ __requestIframe__: 1,
967
+ type: 'ack',
968
+ requestId: msg.requestId,
969
+ path: msg.path,
970
+ role: MessageRole.SERVER
971
+ },
972
+ origin
973
+ })
974
+ );
975
+ // Then send response
976
+ setTimeout(() => {
977
+ window.dispatchEvent(
978
+ new MessageEvent('message', {
979
+ data: {
980
+ __requestIframe__: 1,
981
+ type: 'response',
982
+ requestId: msg.requestId,
983
+ data: { result: 'success' },
984
+ status: 200,
985
+ statusText: 'OK',
986
+ role: MessageRole.SERVER
987
+ },
988
+ origin
989
+ })
990
+ );
991
+ }, 10);
992
+ }
993
+ })
994
+ };
995
+ Object.defineProperty(iframe, 'contentWindow', {
996
+ value: mockContentWindow,
997
+ writable: true
998
+ });
999
+
1000
+ const client = requestIframeClient(iframe); // No secretKey
1001
+ const server = requestIframeServer(); // No secretKey
1002
+
1003
+ server.on('test', (req, res) => {
1004
+ res.send({ result: 'success' });
1005
+ });
1006
+
1007
+ const response = await client.send('test', { param: 'value' }, { ackTimeout: 1000 });
1008
+ expect(response.data).toEqual({ result: 'success' });
1009
+ expect(response.status).toBe(200);
1010
+ expect(mockContentWindow.postMessage).toHaveBeenCalled();
1011
+
1012
+ // Verify the sent message has no secretKey
1013
+ const sentMessage = (mockContentWindow.postMessage as jest.Mock).mock.calls[0][0];
1014
+ expect(sentMessage.secretKey).toBeUndefined();
1015
+ expect(sentMessage.path).toBe('test'); // Path should NOT be prefixed
1016
+
1017
+ server.destroy();
1018
+ cleanupIframe(iframe);
1019
+ });
1020
+
1021
+ it('should handle path correctly with secretKey (path should not be prefixed)', async () => {
1022
+ const origin = 'https://example.com';
1023
+ const iframe = createTestIframe(origin);
1024
+
1025
+ const mockContentWindow = {
1026
+ postMessage: jest.fn((msg: PostMessageData) => {
1027
+ if (msg.type === 'request') {
1028
+ // Verify path is NOT prefixed with secretKey
1029
+ expect(msg.path).toBe('api/users');
1030
+ expect(msg.secretKey).toBe('my-app');
1031
+
1032
+ // Send ACK first
1033
+ window.dispatchEvent(
1034
+ new MessageEvent('message', {
1035
+ data: {
1036
+ __requestIframe__: 1,
1037
+ type: 'ack',
1038
+ requestId: msg.requestId,
1039
+ path: msg.path,
1040
+ secretKey: 'my-app',
1041
+ role: MessageRole.SERVER
1042
+ },
1043
+ origin
1044
+ })
1045
+ );
1046
+ // Then send response
1047
+ setTimeout(() => {
1048
+ window.dispatchEvent(
1049
+ new MessageEvent('message', {
1050
+ data: {
1051
+ __requestIframe__: 1,
1052
+ type: 'response',
1053
+ requestId: msg.requestId,
1054
+ data: { users: [] },
1055
+ status: 200,
1056
+ statusText: 'OK',
1057
+ secretKey: 'my-app',
1058
+ role: MessageRole.SERVER
1059
+ },
1060
+ origin
1061
+ })
1062
+ );
1063
+ }, 10);
1064
+ }
1065
+ })
1066
+ };
1067
+ Object.defineProperty(iframe, 'contentWindow', {
1068
+ value: mockContentWindow,
1069
+ writable: true
1070
+ });
1071
+
1072
+ const client = requestIframeClient(iframe, { secretKey: 'my-app' });
1073
+ const server = requestIframeServer({ secretKey: 'my-app' });
1074
+
1075
+ // Server registers handler with original path (not prefixed)
1076
+ server.on('api/users', (req, res) => {
1077
+ res.send({ users: [] });
1078
+ });
1079
+
1080
+ // Client sends request with original path (not prefixed)
1081
+ const response = await client.send('api/users', undefined, { ackTimeout: 1000 });
1082
+ expect(response.data).toEqual({ users: [] });
1083
+
1084
+ // Verify path in sent message is NOT prefixed
1085
+ const sentMessage = (mockContentWindow.postMessage as jest.Mock).mock.calls[0][0];
1086
+ expect(sentMessage.path).toBe('api/users');
1087
+ expect(sentMessage.secretKey).toBe('my-app');
1088
+
1089
+ server.destroy();
1090
+ cleanupIframe(iframe);
1091
+ });
1092
+ });
1093
+
1094
+ describe('Middleware', () => {
1095
+ it('should support global middleware', async () => {
1096
+ const origin = 'https://example.com';
1097
+ const iframe = createTestIframe(origin);
1098
+
1099
+ const mockContentWindow = {
1100
+ postMessage: jest.fn()
1101
+ };
1102
+ Object.defineProperty(iframe, 'contentWindow', {
1103
+ value: mockContentWindow,
1104
+ writable: true
1105
+ });
1106
+
1107
+ const client = requestIframeClient(iframe);
1108
+ const server = requestIframeServer();
1109
+
1110
+ // Add middleware (auth validation)
1111
+ const middleware = jest.fn((req, res, next) => {
1112
+ if (req.headers['authorization'] === 'Bearer token123') {
1113
+ next();
1114
+ } else {
1115
+ res.status(401).send({ error: 'Unauthorized' });
1116
+ }
1117
+ });
540
1118
 
541
1119
  server.use(middleware);
542
1120
 
@@ -689,7 +1267,7 @@ describe('requestIframeClient and requestIframeServer', () => {
689
1267
  });
690
1268
 
691
1269
  describe('sendFile', () => {
692
- it('should support sending file (base64 encoded)', async () => {
1270
+ it('should support sending file (stream)', async () => {
693
1271
  const origin = 'https://example.com';
694
1272
  const iframe = createTestIframe(origin);
695
1273
 
@@ -705,11 +1283,16 @@ describe('requestIframeClient and requestIframeServer', () => {
705
1283
  const server = requestIframeServer();
706
1284
 
707
1285
  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
- });
1286
+ try {
1287
+ const fileContent = 'Hello World';
1288
+ await res.sendFile(fileContent, {
1289
+ mimeType: 'text/plain',
1290
+ fileName: 'test.txt'
1291
+ });
1292
+ } catch (error) {
1293
+ console.error('Error in sendFile:', error);
1294
+ throw error;
1295
+ }
713
1296
  });
714
1297
 
715
1298
  // Simulate request from iframe
@@ -727,22 +1310,43 @@ describe('requestIframeClient and requestIframeServer', () => {
727
1310
  source: mockContentWindow as any
728
1311
  })
729
1312
  );
730
- await new Promise((resolve) => setTimeout(resolve, 100));
1313
+ // Wait for async handler to complete
1314
+ await new Promise((resolve) => setTimeout(resolve, 1000));
731
1315
 
732
- // Verify sendFile was called
1316
+ // Verify sendFile was called - now it uses stream
733
1317
  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
1318
 
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');
1319
+ // Debug: Check all message types sent
1320
+ const allCalls = mockContentWindow.postMessage.mock.calls;
1321
+ const messageTypes = allCalls.map(call => call[0]?.type).filter(Boolean);
1322
+ if (messageTypes.length === 0) {
1323
+ throw new Error('No messages were sent to mockContentWindow.postMessage');
1324
+ }
1325
+
1326
+ const streamStartCall = allCalls.find(
1327
+ (call: any[]) => call[0]?.type === 'stream_start'
1328
+ );
1329
+ if (!streamStartCall) {
1330
+ throw new Error(`stream_start not found. Message types sent: ${messageTypes.join(', ')}`);
745
1331
  }
1332
+ expect(streamStartCall).toBeDefined();
1333
+ const streamBody = streamStartCall![0].body;
1334
+ expect(streamBody.type).toBe('file');
1335
+ expect(streamBody.autoResolve).toBe(true);
1336
+ expect(streamBody.metadata?.mimeType).toBe('text/plain');
1337
+ expect(streamBody.metadata?.filename).toBe('test.txt');
1338
+
1339
+ // Verify stream_data was sent
1340
+ const streamDataCall = mockContentWindow.postMessage.mock.calls.find(
1341
+ (call: any[]) => call[0]?.type === 'stream_data'
1342
+ );
1343
+ expect(streamDataCall).toBeDefined();
1344
+
1345
+ // Verify stream_end was sent
1346
+ const streamEndCall = mockContentWindow.postMessage.mock.calls.find(
1347
+ (call: any[]) => call[0]?.type === 'stream_end'
1348
+ );
1349
+ expect(streamEndCall).toBeDefined();
746
1350
 
747
1351
  server.destroy();
748
1352
  cleanupIframe(iframe);
@@ -784,13 +1388,17 @@ describe('requestIframeClient and requestIframeServer', () => {
784
1388
  source: mockContentWindow as any
785
1389
  })
786
1390
  );
787
- await new Promise((resolve) => setTimeout(resolve, 100));
1391
+ await new Promise((resolve) => setTimeout(resolve, 200));
788
1392
 
789
- const fileCall = mockContentWindow.postMessage.mock.calls.find(
790
- (call: any[]) => call[0]?.type === 'response' && call[0]?.fileData
1393
+ // Verify stream_start was sent
1394
+ const streamStartCall = mockContentWindow.postMessage.mock.calls.find(
1395
+ (call: any[]) => call[0]?.type === 'stream_start'
791
1396
  );
792
- expect(fileCall).toBeDefined();
793
- expect(fileCall![0].fileData.mimeType).toBe('text/plain');
1397
+ expect(streamStartCall).toBeDefined();
1398
+ const streamBody = streamStartCall![0].body;
1399
+ expect(streamBody.type).toBe('file');
1400
+ expect(streamBody.autoResolve).toBe(true);
1401
+ expect(streamBody.metadata?.mimeType).toBe('text/plain');
794
1402
 
795
1403
  server.destroy();
796
1404
  cleanupIframe(iframe);
@@ -829,13 +1437,17 @@ describe('requestIframeClient and requestIframeServer', () => {
829
1437
  source: mockContentWindow as any
830
1438
  })
831
1439
  );
832
- await new Promise((resolve) => setTimeout(resolve, 100));
1440
+ await new Promise((resolve) => setTimeout(resolve, 200));
833
1441
 
834
- const fileCall = mockContentWindow.postMessage.mock.calls.find(
835
- (call: any[]) => call[0]?.type === 'response' && call[0]?.fileData
1442
+ // Verify stream_start was sent
1443
+ const streamStartCall = mockContentWindow.postMessage.mock.calls.find(
1444
+ (call: any[]) => call[0]?.type === 'stream_start'
836
1445
  );
837
- expect(fileCall).toBeDefined();
838
- expect(fileCall[0].fileData.fileName).toBe('test.txt');
1446
+ expect(streamStartCall).toBeDefined();
1447
+ const streamBody = streamStartCall![0].body;
1448
+ expect(streamBody.type).toBe('file');
1449
+ expect(streamBody.autoResolve).toBe(true);
1450
+ expect(streamBody.metadata?.filename).toBe('test.txt');
839
1451
 
840
1452
  server.destroy();
841
1453
  cleanupIframe(iframe);
@@ -845,52 +1457,14 @@ describe('requestIframeClient and requestIframeServer', () => {
845
1457
  const origin = 'https://example.com';
846
1458
  const iframe = createTestIframe(origin);
847
1459
 
848
- let responseMessage: any = null;
849
1460
  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
- })
1461
+ postMessage: jest.fn()
887
1462
  };
888
1463
  Object.defineProperty(iframe, 'contentWindow', {
889
1464
  value: mockContentWindow,
890
1465
  writable: true
891
1466
  });
892
1467
 
893
- const client = requestIframeClient(iframe);
894
1468
  const server = requestIframeServer();
895
1469
 
896
1470
  server.on('getFileAck', async (req, res) => {
@@ -916,16 +1490,298 @@ describe('requestIframeClient and requestIframeServer', () => {
916
1490
  );
917
1491
  await new Promise((resolve) => setTimeout(resolve, 150));
918
1492
 
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'
1493
+ // Verify stream_start was sent with requireAck
1494
+ const streamStartCall = mockContentWindow.postMessage.mock.calls.find(
1495
+ (call: any[]) => call[0]?.type === 'stream_start'
922
1496
  );
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
- }
1497
+ expect(streamStartCall).toBeDefined();
1498
+ const streamBody = streamStartCall![0].body;
1499
+ expect(streamBody.type).toBe('file');
1500
+ expect(streamBody.autoResolve).toBe(true);
1501
+
1502
+ server.destroy();
1503
+ cleanupIframe(iframe);
1504
+ });
1505
+
1506
+ it('should auto-resolve file stream to fileData on client side', async () => {
1507
+ const origin = 'https://example.com';
1508
+ const iframe = createTestIframe(origin);
1509
+
1510
+ const mockContentWindow = {
1511
+ postMessage: jest.fn((msg: PostMessageData) => {
1512
+ if (msg.type === 'request') {
1513
+ // Send ACK first
1514
+ window.dispatchEvent(
1515
+ new MessageEvent('message', {
1516
+ data: {
1517
+ __requestIframe__: 1,
1518
+ type: 'ack',
1519
+ requestId: msg.requestId,
1520
+ path: msg.path,
1521
+ role: MessageRole.SERVER
1522
+ },
1523
+ origin
1524
+ })
1525
+ );
1526
+ // Then send stream_start
1527
+ setTimeout(() => {
1528
+ const streamId = 'stream-test';
1529
+ const fileContent = btoa('Hello World');
1530
+
1531
+ // Send stream_start
1532
+ window.dispatchEvent(
1533
+ new MessageEvent('message', {
1534
+ data: {
1535
+ __requestIframe__: 1,
1536
+ timestamp: Date.now(),
1537
+ type: 'stream_start',
1538
+ requestId: msg.requestId,
1539
+ status: 200,
1540
+ statusText: 'OK',
1541
+ headers: {
1542
+ 'Content-Type': 'text/plain',
1543
+ 'Content-Disposition': 'attachment; filename="test.txt"'
1544
+ },
1545
+ body: {
1546
+ streamId,
1547
+ type: 'file',
1548
+ chunked: false,
1549
+ autoResolve: true,
1550
+ metadata: {
1551
+ filename: 'test.txt',
1552
+ mimeType: 'text/plain'
1553
+ }
1554
+ },
1555
+ role: MessageRole.SERVER
1556
+ },
1557
+ origin
1558
+ })
1559
+ );
1560
+
1561
+ // Send stream_data
1562
+ setTimeout(() => {
1563
+ window.dispatchEvent(
1564
+ new MessageEvent('message', {
1565
+ data: {
1566
+ __requestIframe__: 1,
1567
+ timestamp: Date.now(),
1568
+ type: 'stream_data',
1569
+ requestId: msg.requestId,
1570
+ body: {
1571
+ streamId,
1572
+ data: fileContent,
1573
+ done: true
1574
+ },
1575
+ role: MessageRole.SERVER
1576
+ },
1577
+ origin
1578
+ })
1579
+ );
1580
+
1581
+ // Send stream_end
1582
+ setTimeout(() => {
1583
+ window.dispatchEvent(
1584
+ new MessageEvent('message', {
1585
+ data: {
1586
+ __requestIframe__: 1,
1587
+ timestamp: Date.now(),
1588
+ type: 'stream_end',
1589
+ requestId: msg.requestId,
1590
+ body: {
1591
+ streamId
1592
+ },
1593
+ role: MessageRole.SERVER
1594
+ },
1595
+ origin
1596
+ })
1597
+ );
1598
+ }, 100);
1599
+ }, 100);
1600
+ }, 100);
1601
+ }
1602
+ })
1603
+ };
1604
+ Object.defineProperty(iframe, 'contentWindow', {
1605
+ value: mockContentWindow,
1606
+ writable: true
1607
+ });
1608
+
1609
+ const client = requestIframeClient(iframe);
1610
+
1611
+ const response = await client.send('getFile', undefined, {
1612
+ ackTimeout: 1000,
1613
+ timeout: 10000
1614
+ }) as any;
1615
+
1616
+ // Verify that data is a File object (auto-resolved from stream)
1617
+ expect(response.data).toBeInstanceOf(File);
1618
+ const file = response.data as File;
1619
+ expect(file.name).toBe('test.txt');
1620
+ expect(file.type).toBe('text/plain');
1621
+
1622
+ // Verify file content using FileReader or arrayBuffer
1623
+ const fileContent = await new Promise<string>((resolve) => {
1624
+ const reader = new FileReader();
1625
+ reader.onload = () => {
1626
+ resolve(reader.result as string);
1627
+ };
1628
+ reader.readAsText(file);
1629
+ });
1630
+ expect(fileContent).toBe('Hello World');
1631
+
1632
+ // Verify that stream is not present (because it was auto-resolved)
1633
+ expect(response.stream).toBeUndefined();
1634
+
1635
+ cleanupIframe(iframe);
1636
+ }, 20000);
1637
+ });
1638
+
1639
+ describe('Path parameters', () => {
1640
+ it('should extract path parameters from route pattern', async () => {
1641
+ const origin = 'https://example.com';
1642
+ const iframe = createTestIframe(origin);
1643
+
1644
+ const mockContentWindow: any = {
1645
+ postMessage: jest.fn((msg: PostMessageData) => {
1646
+ window.dispatchEvent(
1647
+ new MessageEvent('message', {
1648
+ data: msg,
1649
+ origin,
1650
+ source: mockContentWindow as any
1651
+ })
1652
+ );
1653
+ })
1654
+ };
1655
+ Object.defineProperty(iframe, 'contentWindow', { value: mockContentWindow, writable: true });
1656
+
1657
+ const client = requestIframeClient(iframe);
1658
+ const server = requestIframeServer();
1659
+
1660
+ server.on('/api/users/:id', (req, res) => {
1661
+ expect(req.params.id).toBe('123');
1662
+ expect(req.path).toBe('/api/users/123');
1663
+ res.send({ userId: req.params.id });
1664
+ });
1665
+
1666
+ const resp = await client.send<any>('/api/users/123');
1667
+ expect((resp as any).data.userId).toBe('123');
1668
+
1669
+ client.destroy();
1670
+ server.destroy();
1671
+ cleanupIframe(iframe);
1672
+ });
1673
+
1674
+ it('should extract multiple path parameters', async () => {
1675
+ const origin = 'https://example.com';
1676
+ const iframe = createTestIframe(origin);
1677
+
1678
+ const mockContentWindow: any = {
1679
+ postMessage: jest.fn((msg: PostMessageData) => {
1680
+ window.dispatchEvent(
1681
+ new MessageEvent('message', {
1682
+ data: msg,
1683
+ origin,
1684
+ source: mockContentWindow as any
1685
+ })
1686
+ );
1687
+ })
1688
+ };
1689
+ Object.defineProperty(iframe, 'contentWindow', { value: mockContentWindow, writable: true });
1690
+
1691
+ const client = requestIframeClient(iframe);
1692
+ const server = requestIframeServer();
1693
+
1694
+ server.on('/api/users/:userId/posts/:postId', (req, res) => {
1695
+ expect(req.params.userId).toBe('456');
1696
+ expect(req.params.postId).toBe('789');
1697
+ res.send({ userId: req.params.userId, postId: req.params.postId });
1698
+ });
1699
+
1700
+ const resp = await client.send<any>('/api/users/456/posts/789');
1701
+ expect((resp as any).data.userId).toBe('456');
1702
+ expect((resp as any).data.postId).toBe('789');
1703
+
1704
+ client.destroy();
1705
+ server.destroy();
1706
+ cleanupIframe(iframe);
1707
+ });
1708
+
1709
+ it('should return empty params for exact path match', async () => {
1710
+ const origin = 'https://example.com';
1711
+ const iframe = createTestIframe(origin);
1712
+
1713
+ const mockContentWindow: any = {
1714
+ postMessage: jest.fn((msg: PostMessageData) => {
1715
+ window.dispatchEvent(
1716
+ new MessageEvent('message', {
1717
+ data: msg,
1718
+ origin,
1719
+ source: mockContentWindow as any
1720
+ })
1721
+ );
1722
+ })
1723
+ };
1724
+ Object.defineProperty(iframe, 'contentWindow', { value: mockContentWindow, writable: true });
1725
+
1726
+ const client = requestIframeClient(iframe);
1727
+ const server = requestIframeServer();
1728
+
1729
+ server.on('/api/users', (req, res) => {
1730
+ expect(req.params).toEqual({});
1731
+ expect(req.path).toBe('/api/users');
1732
+ res.send({ success: true });
1733
+ });
1734
+
1735
+ const resp = await client.send<any>('/api/users');
1736
+ expect((resp as any).data.success).toBe(true);
1737
+
1738
+ client.destroy();
1739
+ server.destroy();
1740
+ cleanupIframe(iframe);
1741
+ });
1742
+
1743
+ it('should work with stream requests', async () => {
1744
+ const origin = 'https://example.com';
1745
+ const iframe = createTestIframe(origin);
1746
+
1747
+ const mockContentWindow: any = {
1748
+ postMessage: jest.fn((msg: PostMessageData) => {
1749
+ window.dispatchEvent(
1750
+ new MessageEvent('message', {
1751
+ data: msg,
1752
+ origin,
1753
+ source: mockContentWindow as any
1754
+ })
1755
+ );
1756
+ })
1757
+ };
1758
+ Object.defineProperty(iframe, 'contentWindow', { value: mockContentWindow, writable: true });
1759
+
1760
+ const client = requestIframeClient(iframe);
1761
+ const server = requestIframeServer();
1762
+
1763
+ server.on('/api/upload/:fileId', async (req, res) => {
1764
+ expect(req.params.fileId).toBe('file-123');
1765
+ expect(req.stream).toBeDefined();
1766
+ const chunks: any[] = [];
1767
+ for await (const chunk of req.stream as any) {
1768
+ chunks.push(chunk);
1769
+ }
1770
+ res.send({ fileId: req.params.fileId, chunks });
1771
+ });
1772
+
1773
+ const stream = new IframeWritableStream({
1774
+ iterator: async function* () {
1775
+ yield 'chunk1';
1776
+ yield 'chunk2';
1777
+ }
1778
+ });
928
1779
 
1780
+ const resp = await client.sendStream<any>('/api/upload/file-123', stream);
1781
+ expect((resp as any).data.fileId).toBe('file-123');
1782
+ expect((resp as any).data.chunks).toEqual(['chunk1', 'chunk2']);
1783
+
1784
+ client.destroy();
929
1785
  server.destroy();
930
1786
  cleanupIframe(iframe);
931
1787
  });
@@ -1219,11 +2075,19 @@ describe('requestIframeClient and requestIframeServer', () => {
1219
2075
  );
1220
2076
  await new Promise((resolve) => setTimeout(resolve, 50));
1221
2077
 
2078
+ // Wait for response to be sent
2079
+ await new Promise((resolve) => setTimeout(resolve, 100));
2080
+
1222
2081
  // Simulate client receiving response
1223
2082
  const responseCall = mockContentWindow.postMessage.mock.calls.find(
1224
2083
  (call: any[]) => call[0]?.type === 'response'
1225
2084
  );
1226
- if (responseCall) {
2085
+ expect(responseCall).toBeDefined();
2086
+ if (responseCall && responseCall[0]) {
2087
+ // Verify response contains Set-Cookie header
2088
+ expect(responseCall[0].headers).toBeDefined();
2089
+ expect(responseCall[0].headers[HttpHeader.SET_COOKIE]).toBeDefined();
2090
+
1227
2091
  window.dispatchEvent(
1228
2092
  new MessageEvent('message', {
1229
2093
  data: responseCall[0],
@@ -1232,6 +2096,9 @@ describe('requestIframeClient and requestIframeServer', () => {
1232
2096
  })
1233
2097
  );
1234
2098
  }
2099
+
2100
+ // Wait for response to be processed
2101
+ await responsePromise;
1235
2102
  await new Promise((resolve) => setTimeout(resolve, 50));
1236
2103
 
1237
2104
  // Verify client automatically saved server-set cookies
@@ -1258,7 +2125,8 @@ describe('requestIframeClient and requestIframeServer', () => {
1258
2125
  __requestIframe__: 1,
1259
2126
  type: 'ack',
1260
2127
  requestId: msg.requestId,
1261
- path: msg.path
2128
+ path: msg.path,
2129
+ role: MessageRole.SERVER
1262
2130
  },
1263
2131
  origin
1264
2132
  })
@@ -1271,7 +2139,8 @@ describe('requestIframeClient and requestIframeServer', () => {
1271
2139
  requestId: msg.requestId,
1272
2140
  data: { result: 'success' },
1273
2141
  status: 200,
1274
- requireAck: true
2142
+ requireAck: true,
2143
+ role: MessageRole.SERVER
1275
2144
  };
1276
2145
  responseMessage = response;
1277
2146
  window.dispatchEvent(
@@ -1334,7 +2203,8 @@ describe('requestIframeClient and requestIframeServer', () => {
1334
2203
  __requestIframe__: 1,
1335
2204
  type: 'ack',
1336
2205
  requestId: msg.requestId,
1337
- path: msg.path
2206
+ path: msg.path,
2207
+ role: MessageRole.SERVER
1338
2208
  },
1339
2209
  origin
1340
2210
  })
@@ -1348,7 +2218,8 @@ describe('requestIframeClient and requestIframeServer', () => {
1348
2218
  requestId: msg.requestId,
1349
2219
  data: { json: true },
1350
2220
  status: 200,
1351
- requireAck: true
2221
+ requireAck: true,
2222
+ role: MessageRole.SERVER
1352
2223
  },
1353
2224
  origin
1354
2225
  })
@@ -1409,7 +2280,8 @@ describe('requestIframeClient and requestIframeServer', () => {
1409
2280
  __requestIframe__: 1,
1410
2281
  type: 'ack',
1411
2282
  requestId: msg.requestId,
1412
- path: msg.path
2283
+ path: msg.path,
2284
+ role: MessageRole.SERVER
1413
2285
  },
1414
2286
  origin
1415
2287
  })
@@ -1423,7 +2295,8 @@ describe('requestIframeClient and requestIframeServer', () => {
1423
2295
  requestId: msg.requestId,
1424
2296
  data: { error: 'Not Found' },
1425
2297
  status: 404,
1426
- statusText: 'Not Found'
2298
+ statusText: 'Not Found',
2299
+ role: MessageRole.SERVER
1427
2300
  },
1428
2301
  origin
1429
2302
  })
@@ -1977,105 +2850,257 @@ describe('requestIframeClient and requestIframeServer', () => {
1977
2850
  });
1978
2851
  });
1979
2852
 
1980
- describe('Stream response', () => {
1981
- it('should support sendStream', async () => {
2853
+ describe('client send various body types', () => {
2854
+ it('should send plain object and server receives JSON + Content-Type', async () => {
1982
2855
  const origin = 'https://example.com';
1983
2856
  const iframe = createTestIframe(origin);
1984
2857
 
1985
- const mockContentWindow = {
2858
+ const mockContentWindow: any = {
1986
2859
  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
- }
2860
+ window.dispatchEvent(
2861
+ new MessageEvent('message', {
2862
+ data: msg,
2863
+ origin,
2864
+ source: mockContentWindow as any
2865
+ })
2866
+ );
2017
2867
  })
2018
2868
  };
2019
- Object.defineProperty(iframe, 'contentWindow', {
2020
- value: mockContentWindow,
2021
- writable: true
2022
- });
2869
+ Object.defineProperty(iframe, 'contentWindow', { value: mockContentWindow, writable: true });
2023
2870
 
2024
2871
  const client = requestIframeClient(iframe);
2025
2872
  const server = requestIframeServer();
2026
2873
 
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);
2874
+ server.on('echoObject', (req, res) => {
2875
+ expect(req.headers[HttpHeader.CONTENT_TYPE]).toBe('application/json');
2876
+ res.send({ ok: true, received: req.body });
2036
2877
  });
2037
2878
 
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
2879
+ const resp = await client.send<any>('echoObject', { a: 1 });
2880
+ expect((resp as any).data.ok).toBe(true);
2881
+ expect((resp as any).data.received).toEqual({ a: 1 });
2882
+
2883
+ client.destroy();
2884
+ server.destroy();
2885
+ cleanupIframe(iframe);
2886
+ });
2887
+
2888
+ it('should send string and server receives text/plain Content-Type', async () => {
2889
+ const origin = 'https://example.com';
2890
+ const iframe = createTestIframe(origin);
2891
+
2892
+ const mockContentWindow: any = {
2893
+ postMessage: jest.fn((msg: PostMessageData) => {
2894
+ window.dispatchEvent(
2895
+ new MessageEvent('message', {
2896
+ data: msg,
2897
+ origin,
2898
+ source: mockContentWindow as any
2899
+ })
2900
+ );
2050
2901
  })
2051
- );
2052
- await new Promise((resolve) => setTimeout(resolve, 150));
2902
+ };
2903
+ Object.defineProperty(iframe, 'contentWindow', { value: mockContentWindow, writable: true });
2053
2904
 
2054
- const streamStartCall = mockContentWindow.postMessage.mock.calls.find(
2055
- (call: any[]) => call[0]?.type === 'stream_start'
2056
- );
2057
- expect(streamStartCall).toBeDefined();
2905
+ const client = requestIframeClient(iframe);
2906
+ const server = requestIframeServer();
2058
2907
 
2908
+ server.on('echoText', (req, res) => {
2909
+ expect(req.headers[HttpHeader.CONTENT_TYPE]).toContain('text/plain');
2910
+ res.send({ received: req.body, type: typeof req.body });
2911
+ });
2912
+
2913
+ const resp = await client.send<any>('echoText', 'hello');
2914
+ expect((resp as any).data.received).toBe('hello');
2915
+ expect((resp as any).data.type).toBe('string');
2916
+
2917
+ client.destroy();
2059
2918
  server.destroy();
2060
2919
  cleanupIframe(iframe);
2061
2920
  });
2062
2921
 
2063
- it('should handle stream response from server', async () => {
2922
+ it('should send URLSearchParams and server receives correct Content-Type', async () => {
2064
2923
  const origin = 'https://example.com';
2065
2924
  const iframe = createTestIframe(origin);
2066
2925
 
2067
- const mockContentWindow = {
2926
+ const mockContentWindow: any = {
2068
2927
  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
2928
+ window.dispatchEvent(
2929
+ new MessageEvent('message', {
2930
+ data: msg,
2931
+ origin,
2932
+ source: mockContentWindow as any
2933
+ })
2934
+ );
2935
+ })
2936
+ };
2937
+ Object.defineProperty(iframe, 'contentWindow', { value: mockContentWindow, writable: true });
2938
+
2939
+ const client = requestIframeClient(iframe);
2940
+ const server = requestIframeServer();
2941
+
2942
+ server.on('echoParams', (req, res) => {
2943
+ expect(req.headers[HttpHeader.CONTENT_TYPE]).toBe('application/x-www-form-urlencoded');
2944
+ // URLSearchParams should be structured-cloneable in modern browsers
2945
+ const value = req.body?.toString?.() ?? String(req.body);
2946
+ res.send({ received: value });
2947
+ });
2948
+
2949
+ const params = new URLSearchParams({ a: '1', b: '2' });
2950
+ const resp = await client.send<any>('echoParams', params as any);
2951
+ expect((resp as any).data.received).toContain('a=1');
2952
+ expect((resp as any).data.received).toContain('b=2');
2953
+
2954
+ client.destroy();
2955
+ server.destroy();
2956
+ cleanupIframe(iframe);
2957
+ });
2958
+
2959
+ it('should auto-dispatch File/Blob body to client.sendFile and server receives file via stream (autoResolve)', async () => {
2960
+ const origin = 'https://example.com';
2961
+ const iframe = createTestIframe(origin);
2962
+
2963
+ const mockContentWindow: any = {
2964
+ postMessage: jest.fn((msg: PostMessageData) => {
2965
+ window.dispatchEvent(
2966
+ new MessageEvent('message', {
2967
+ data: msg,
2968
+ origin,
2969
+ source: mockContentWindow as any
2970
+ })
2971
+ );
2972
+ })
2973
+ };
2974
+ Object.defineProperty(iframe, 'contentWindow', { value: mockContentWindow, writable: true });
2975
+
2976
+ const client = requestIframeClient(iframe);
2977
+ const server = requestIframeServer();
2978
+
2979
+ server.on('uploadFile', async (req, res) => {
2980
+ expect(req.body).toBeDefined();
2981
+ const blob = req.body as Blob;
2982
+ const text = await blobToText(blob);
2983
+ res.send({ ok: true, text });
2984
+ });
2985
+
2986
+ const blob = new Blob(['Hello Upload'], { type: 'text/plain' });
2987
+ const resp = await client.send<any>('uploadFile', blob);
2988
+ expect((resp as any).data.ok).toBe(true);
2989
+ expect((resp as any).data.text).toBe('Hello Upload');
2990
+
2991
+ client.destroy();
2992
+ server.destroy();
2993
+ cleanupIframe(iframe);
2994
+ });
2995
+
2996
+ it('should send stream from client to server and server receives req.stream', async () => {
2997
+ const origin = 'https://example.com';
2998
+ const iframe = createTestIframe(origin);
2999
+
3000
+ const mockContentWindow: any = {
3001
+ postMessage: jest.fn((msg: PostMessageData) => {
3002
+ window.dispatchEvent(
3003
+ new MessageEvent('message', {
3004
+ data: msg,
3005
+ origin,
3006
+ source: mockContentWindow as any
3007
+ })
3008
+ );
3009
+ })
3010
+ };
3011
+ Object.defineProperty(iframe, 'contentWindow', { value: mockContentWindow, writable: true });
3012
+
3013
+ const client = requestIframeClient(iframe);
3014
+ const server = requestIframeServer();
3015
+
3016
+ server.on('uploadStream', async (req, res) => {
3017
+ expect(req.stream).toBeDefined();
3018
+ const chunks: any[] = [];
3019
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
3020
+ for await (const chunk of req.stream as any) {
3021
+ chunks.push(chunk);
3022
+ }
3023
+ res.send({ chunks });
3024
+ });
3025
+
3026
+ const stream = new IframeWritableStream({
3027
+ iterator: async function* () {
3028
+ yield 'c1';
3029
+ yield 'c2';
3030
+ yield 'c3';
3031
+ }
3032
+ });
3033
+
3034
+ const resp = await client.sendStream<any>('uploadStream', stream);
3035
+ expect((resp as any).data.chunks).toEqual(['c1', 'c2', 'c3']);
3036
+
3037
+ client.destroy();
3038
+ server.destroy();
3039
+ cleanupIframe(iframe);
3040
+ });
3041
+
3042
+ it('should support client.sendFile with autoResolve (server receives File/Blob in req.body)', async () => {
3043
+ const origin = 'https://example.com';
3044
+ const iframe = createTestIframe(origin);
3045
+
3046
+ const mockContentWindow: any = {
3047
+ postMessage: jest.fn((msg: PostMessageData) => {
3048
+ window.dispatchEvent(
3049
+ new MessageEvent('message', {
3050
+ data: msg,
3051
+ origin,
3052
+ source: mockContentWindow as any
3053
+ })
3054
+ );
3055
+ })
3056
+ };
3057
+ Object.defineProperty(iframe, 'contentWindow', { value: mockContentWindow, writable: true });
3058
+
3059
+ const client = requestIframeClient(iframe);
3060
+ const server = requestIframeServer();
3061
+
3062
+ server.on('uploadFileStream', async (req, res) => {
3063
+ // autoResolve: server should get File/Blob directly
3064
+ expect(req.body).toBeDefined();
3065
+ expect(req.stream).toBeUndefined();
3066
+ const blob = req.body as Blob;
3067
+ const text = await blobToText(blob);
3068
+ res.send({ ok: true, text });
3069
+ });
3070
+
3071
+ const blob = new Blob(['Hello Upload Stream'], { type: 'text/plain' });
3072
+ const resp = await client.sendFile<any>('uploadFileStream', blob, {
3073
+ autoResolve: true,
3074
+ mimeType: 'text/plain',
3075
+ fileName: 'upload.txt'
3076
+ });
3077
+ expect((resp as any).data.ok).toBe(true);
3078
+ expect((resp as any).data.text).toBe('Hello Upload Stream');
3079
+
3080
+ client.destroy();
3081
+ server.destroy();
3082
+ cleanupIframe(iframe);
3083
+ });
3084
+ });
3085
+
3086
+ describe('Stream response', () => {
3087
+ it('should support sendStream', async () => {
3088
+ const origin = 'https://example.com';
3089
+ const iframe = createTestIframe(origin);
3090
+
3091
+ const mockContentWindow = {
3092
+ postMessage: jest.fn((msg: PostMessageData) => {
3093
+ if (msg.type === 'request') {
3094
+ window.dispatchEvent(
3095
+ new MessageEvent('message', {
3096
+ data: {
3097
+ __requestIframe__: 1,
3098
+ type: 'ack',
3099
+ requestId: msg.requestId,
3100
+ path: msg.path,
3101
+ role: MessageRole.SERVER
3102
+ },
3103
+ origin
2079
3104
  })
2080
3105
  );
2081
3106
  setTimeout(() => {
@@ -2089,7 +3114,8 @@ describe('requestIframeClient and requestIframeServer', () => {
2089
3114
  streamId: 'stream-123',
2090
3115
  type: 'data',
2091
3116
  chunked: true
2092
- }
3117
+ },
3118
+ role: MessageRole.SERVER
2093
3119
  },
2094
3120
  origin
2095
3121
  })
@@ -2142,6 +3168,90 @@ describe('requestIframeClient and requestIframeServer', () => {
2142
3168
  cleanupIframe(iframe);
2143
3169
  });
2144
3170
 
3171
+ it('should handle stream response from server', async () => {
3172
+ const origin = 'https://example.com';
3173
+ const iframe = createTestIframe(origin);
3174
+
3175
+ const mockContentWindow = {
3176
+ postMessage: jest.fn((msg: PostMessageData) => {
3177
+ if (msg.type === 'request') {
3178
+ window.dispatchEvent(
3179
+ new MessageEvent('message', {
3180
+ data: {
3181
+ __requestIframe__: 1,
3182
+ type: 'ack',
3183
+ requestId: msg.requestId,
3184
+ path: msg.path,
3185
+ role: MessageRole.SERVER
3186
+ },
3187
+ origin
3188
+ })
3189
+ );
3190
+ setTimeout(() => {
3191
+ window.dispatchEvent(
3192
+ new MessageEvent('message', {
3193
+ data: {
3194
+ __requestIframe__: 1,
3195
+ type: 'stream_start',
3196
+ requestId: msg.requestId,
3197
+ body: {
3198
+ streamId: 'stream-123',
3199
+ type: 'data',
3200
+ chunked: true
3201
+ },
3202
+ role: MessageRole.SERVER
3203
+ },
3204
+ origin
3205
+ })
3206
+ );
3207
+ }, 10);
3208
+ }
3209
+ })
3210
+ };
3211
+ Object.defineProperty(iframe, 'contentWindow', {
3212
+ value: mockContentWindow,
3213
+ writable: true
3214
+ });
3215
+
3216
+ const client = requestIframeClient(iframe);
3217
+ const server = requestIframeServer();
3218
+
3219
+ server.on('getStream', async (req, res) => {
3220
+ const { IframeWritableStream } = await import('../stream');
3221
+ const stream = new IframeWritableStream({
3222
+ iterator: async function* () {
3223
+ yield 'chunk1';
3224
+ yield 'chunk2';
3225
+ }
3226
+ });
3227
+ await res.sendStream(stream);
3228
+ });
3229
+
3230
+ const requestId = 'req-stream';
3231
+ window.dispatchEvent(
3232
+ new MessageEvent('message', {
3233
+ data: {
3234
+ __requestIframe__: 1,
3235
+ type: 'request',
3236
+ requestId: requestId,
3237
+ path: 'getStream',
3238
+ body: {}
3239
+ },
3240
+ origin,
3241
+ source: mockContentWindow as any
3242
+ })
3243
+ );
3244
+ await new Promise((resolve) => setTimeout(resolve, 200));
3245
+
3246
+ const streamStartCall = mockContentWindow.postMessage.mock.calls.find(
3247
+ (call: any[]) => call[0]?.type === 'stream_start'
3248
+ );
3249
+ expect(streamStartCall).toBeDefined();
3250
+
3251
+ server.destroy();
3252
+ cleanupIframe(iframe);
3253
+ });
3254
+
2145
3255
  it('should handle server open/close methods', () => {
2146
3256
  const origin = 'https://example.com';
2147
3257
  const iframe = createTestIframe(origin);
@@ -2403,4 +3513,1872 @@ describe('requestIframeClient and requestIframeServer', () => {
2403
3513
  cleanupIframe(iframe);
2404
3514
  });
2405
3515
  });
3516
+
3517
+ describe('Client additional features', () => {
3518
+ it('should support postMessage method for stream handler', () => {
3519
+ const origin = 'https://example.com';
3520
+ const iframe = createTestIframe(origin);
3521
+ const mockContentWindow = {
3522
+ postMessage: jest.fn()
3523
+ };
3524
+ Object.defineProperty(iframe, 'contentWindow', {
3525
+ value: mockContentWindow,
3526
+ writable: true
3527
+ });
3528
+
3529
+ const client = requestIframeClient(iframe);
3530
+ const message = { type: 'test', data: 'value' };
3531
+
3532
+ // Access postMessage through stream handler interface
3533
+ (client as any).postMessage(message);
3534
+
3535
+ // Verify message was sent via dispatcher
3536
+ expect(mockContentWindow.postMessage).toHaveBeenCalled();
3537
+
3538
+ cleanupIframe(iframe);
3539
+ });
3540
+
3541
+ it('should handle function-type headers', async () => {
3542
+ const origin = 'https://example.com';
3543
+ const iframe = createTestIframe(origin);
3544
+ const mockContentWindow = {
3545
+ postMessage: jest.fn((msg: PostMessageData) => {
3546
+ if (msg.type === 'request') {
3547
+ window.dispatchEvent(
3548
+ new MessageEvent('message', {
3549
+ data: {
3550
+ __requestIframe__: 1,
3551
+ type: 'ack',
3552
+ requestId: msg.requestId,
3553
+ path: msg.path,
3554
+ role: MessageRole.SERVER
3555
+ },
3556
+ origin
3557
+ })
3558
+ );
3559
+ setTimeout(() => {
3560
+ window.dispatchEvent(
3561
+ new MessageEvent('message', {
3562
+ data: {
3563
+ __requestIframe__: 1,
3564
+ type: 'response',
3565
+ requestId: msg.requestId,
3566
+ data: { result: 'success' },
3567
+ status: 200,
3568
+ statusText: 'OK',
3569
+ role: MessageRole.SERVER
3570
+ },
3571
+ origin
3572
+ })
3573
+ );
3574
+ }, 10);
3575
+ }
3576
+ })
3577
+ };
3578
+ Object.defineProperty(iframe, 'contentWindow', {
3579
+ value: mockContentWindow,
3580
+ writable: true
3581
+ });
3582
+
3583
+ const client = requestIframeClient(iframe, {
3584
+ headers: {
3585
+ 'X-Dynamic': (config: RequestConfig) => `value-${config.path}`
3586
+ }
3587
+ });
3588
+
3589
+ await client.send('test', {});
3590
+
3591
+ const requestCall = mockContentWindow.postMessage.mock.calls.find(
3592
+ (call: any[]) => call[0]?.type === 'request'
3593
+ );
3594
+ expect(requestCall).toBeDefined();
3595
+ if (requestCall && requestCall[0]) {
3596
+ expect(requestCall[0].headers?.['X-Dynamic']).toBe('value-test');
3597
+ }
3598
+
3599
+ cleanupIframe(iframe);
3600
+ });
3601
+
3602
+ it('should handle isConnect timeout', async () => {
3603
+ const origin = 'https://example.com';
3604
+ const iframe = createTestIframe(origin);
3605
+ const mockContentWindow = {
3606
+ postMessage: jest.fn()
3607
+ };
3608
+ Object.defineProperty(iframe, 'contentWindow', {
3609
+ value: mockContentWindow,
3610
+ writable: true
3611
+ });
3612
+
3613
+ const client = requestIframeClient(iframe, { ackTimeout: 50 });
3614
+
3615
+ // Server doesn't respond, should timeout
3616
+ const connected = await client.isConnect();
3617
+ expect(connected).toBe(false);
3618
+
3619
+ cleanupIframe(iframe);
3620
+ });
3621
+
3622
+ it('should handle isConnect rejection', async () => {
3623
+ const origin = 'https://example.com';
3624
+ const iframe = createTestIframe(origin);
3625
+ const mockContentWindow = {
3626
+ postMessage: jest.fn((msg: PostMessageData) => {
3627
+ if (msg.type === 'ping') {
3628
+ // Simulate error by not sending pong
3629
+ setTimeout(() => {
3630
+ window.dispatchEvent(
3631
+ new MessageEvent('message', {
3632
+ data: {
3633
+ __requestIframe__: 1,
3634
+ type: 'error',
3635
+ requestId: msg.requestId,
3636
+ error: { message: 'Connection failed' },
3637
+ role: MessageRole.SERVER
3638
+ },
3639
+ origin
3640
+ })
3641
+ );
3642
+ }, 10);
3643
+ }
3644
+ })
3645
+ };
3646
+ Object.defineProperty(iframe, 'contentWindow', {
3647
+ value: mockContentWindow,
3648
+ writable: true
3649
+ });
3650
+
3651
+ const client = requestIframeClient(iframe);
3652
+
3653
+ const connected = await client.isConnect();
3654
+ expect(connected).toBe(false);
3655
+
3656
+ cleanupIframe(iframe);
3657
+ });
3658
+
3659
+ it('should remember targetServerId from ACK and use it in subsequent requests', async () => {
3660
+ const origin = 'https://example.com';
3661
+ const iframe = createTestIframe(origin);
3662
+ const serverId = 'server-123';
3663
+
3664
+ const mockContentWindow = {
3665
+ postMessage: jest.fn((msg: PostMessageData) => {
3666
+ if (msg.type === 'request') {
3667
+ window.dispatchEvent(
3668
+ new MessageEvent('message', {
3669
+ data: {
3670
+ __requestIframe__: 1,
3671
+ type: 'ack',
3672
+ requestId: msg.requestId,
3673
+ path: msg.path,
3674
+ role: MessageRole.SERVER,
3675
+ creatorId: serverId
3676
+ },
3677
+ origin
3678
+ })
3679
+ );
3680
+ setTimeout(() => {
3681
+ window.dispatchEvent(
3682
+ new MessageEvent('message', {
3683
+ data: {
3684
+ __requestIframe__: 1,
3685
+ type: 'response',
3686
+ requestId: msg.requestId,
3687
+ data: { result: 'success' },
3688
+ status: 200,
3689
+ statusText: 'OK',
3690
+ role: MessageRole.SERVER,
3691
+ creatorId: serverId
3692
+ },
3693
+ origin
3694
+ })
3695
+ );
3696
+ }, 10);
3697
+ }
3698
+ })
3699
+ };
3700
+ Object.defineProperty(iframe, 'contentWindow', {
3701
+ value: mockContentWindow,
3702
+ writable: true
3703
+ });
3704
+
3705
+ const client = requestIframeClient(iframe);
3706
+
3707
+ // First request - should remember serverId
3708
+ await client.send('test1', {});
3709
+
3710
+ // Second request - should use remembered serverId
3711
+ await client.send('test2', {});
3712
+
3713
+ const requestCalls = mockContentWindow.postMessage.mock.calls.filter(
3714
+ (call: any[]) => call[0]?.type === 'request'
3715
+ );
3716
+
3717
+ // First request may not have targetId (if serverId not remembered yet)
3718
+ // Second request should have targetId
3719
+ expect(requestCalls.length).toBeGreaterThanOrEqual(2);
3720
+ const secondRequest = requestCalls[requestCalls.length - 1];
3721
+ if (secondRequest) {
3722
+ expect(secondRequest[0].targetId).toBe(serverId);
3723
+ }
3724
+
3725
+ cleanupIframe(iframe);
3726
+ });
3727
+
3728
+ it('should not override existing targetServerId', async () => {
3729
+ const origin = 'https://example.com';
3730
+ const iframe = createTestIframe(origin);
3731
+ const existingServerId = 'existing-server';
3732
+ const newServerId = 'new-server';
3733
+
3734
+ const mockContentWindow = {
3735
+ postMessage: jest.fn((msg: PostMessageData) => {
3736
+ if (msg.type === 'request') {
3737
+ window.dispatchEvent(
3738
+ new MessageEvent('message', {
3739
+ data: {
3740
+ __requestIframe__: 1,
3741
+ type: 'ack',
3742
+ requestId: msg.requestId,
3743
+ path: msg.path,
3744
+ role: MessageRole.SERVER,
3745
+ creatorId: newServerId
3746
+ },
3747
+ origin
3748
+ })
3749
+ );
3750
+ setTimeout(() => {
3751
+ window.dispatchEvent(
3752
+ new MessageEvent('message', {
3753
+ data: {
3754
+ __requestIframe__: 1,
3755
+ type: 'response',
3756
+ requestId: msg.requestId,
3757
+ data: { result: 'success' },
3758
+ status: 200,
3759
+ statusText: 'OK',
3760
+ role: MessageRole.SERVER,
3761
+ creatorId: newServerId
3762
+ },
3763
+ origin
3764
+ })
3765
+ );
3766
+ }, 10);
3767
+ }
3768
+ })
3769
+ };
3770
+ Object.defineProperty(iframe, 'contentWindow', {
3771
+ value: mockContentWindow,
3772
+ writable: true
3773
+ });
3774
+
3775
+ const client = requestIframeClient(iframe);
3776
+
3777
+ // Set existing targetServerId
3778
+ (client as any)._targetServerId = existingServerId;
3779
+
3780
+ // Send request with explicit targetId
3781
+ await client.send('test', {}, { targetId: existingServerId });
3782
+
3783
+ const requestCall = mockContentWindow.postMessage.mock.calls.find(
3784
+ (call: any[]) => call[0]?.type === 'request'
3785
+ );
3786
+ expect(requestCall).toBeDefined();
3787
+ if (requestCall) {
3788
+ expect(requestCall[0].targetId).toBe(existingServerId);
3789
+ }
3790
+
3791
+ // targetServerId should not be overridden
3792
+ expect((client as any)._targetServerId).toBe(existingServerId);
3793
+
3794
+ cleanupIframe(iframe);
3795
+ });
3796
+
3797
+ it('should handle setCookie with expires option', () => {
3798
+ const origin = 'https://example.com';
3799
+ const iframe = createTestIframe(origin);
3800
+ const mockContentWindow = {
3801
+ postMessage: jest.fn()
3802
+ };
3803
+ Object.defineProperty(iframe, 'contentWindow', {
3804
+ value: mockContentWindow,
3805
+ writable: true
3806
+ });
3807
+
3808
+ const client = requestIframeClient(iframe);
3809
+ const expires = new Date(Date.now() + 3600000); // 1 hour from now
3810
+
3811
+ client.setCookie('token', 'value', { expires });
3812
+
3813
+ expect(client.getCookie('token')).toBe('value');
3814
+
3815
+ cleanupIframe(iframe);
3816
+ });
3817
+
3818
+ it('should handle setCookie with maxAge option', () => {
3819
+ const origin = 'https://example.com';
3820
+ const iframe = createTestIframe(origin);
3821
+ const mockContentWindow = {
3822
+ postMessage: jest.fn()
3823
+ };
3824
+ Object.defineProperty(iframe, 'contentWindow', {
3825
+ value: mockContentWindow,
3826
+ writable: true
3827
+ });
3828
+
3829
+ const client = requestIframeClient(iframe);
3830
+
3831
+ client.setCookie('token', 'value', { maxAge: 3600 });
3832
+
3833
+ expect(client.getCookie('token')).toBe('value');
3834
+
3835
+ cleanupIframe(iframe);
3836
+ });
3837
+
3838
+ it('should handle getServer method', () => {
3839
+ const origin = 'https://example.com';
3840
+ const iframe = createTestIframe(origin);
3841
+ const mockContentWindow = {
3842
+ postMessage: jest.fn()
3843
+ };
3844
+ Object.defineProperty(iframe, 'contentWindow', {
3845
+ value: mockContentWindow,
3846
+ writable: true
3847
+ });
3848
+
3849
+ const client = requestIframeClient(iframe);
3850
+ const server = (client as any).getServer();
3851
+
3852
+ expect(server).toBeDefined();
3853
+ expect(server.isOpen).toBe(true);
3854
+
3855
+ cleanupIframe(iframe);
3856
+ });
3857
+
3858
+ it('should handle non-autoResolve file stream', async () => {
3859
+ const origin = 'https://example.com';
3860
+ const iframe = createTestIframe(origin);
3861
+ const mockContentWindow = {
3862
+ postMessage: jest.fn((msg: PostMessageData) => {
3863
+ if (msg.type === 'request') {
3864
+ window.dispatchEvent(
3865
+ new MessageEvent('message', {
3866
+ data: {
3867
+ __requestIframe__: 1,
3868
+ type: 'ack',
3869
+ requestId: msg.requestId,
3870
+ path: msg.path,
3871
+ role: MessageRole.SERVER
3872
+ },
3873
+ origin
3874
+ })
3875
+ );
3876
+ setTimeout(() => {
3877
+ const streamId = 'stream-test';
3878
+ window.dispatchEvent(
3879
+ new MessageEvent('message', {
3880
+ data: {
3881
+ __requestIframe__: 1,
3882
+ timestamp: Date.now(),
3883
+ type: 'stream_start',
3884
+ requestId: msg.requestId,
3885
+ status: 200,
3886
+ statusText: 'OK',
3887
+ body: {
3888
+ streamId,
3889
+ type: 'file',
3890
+ chunked: false,
3891
+ autoResolve: false, // Not auto-resolve
3892
+ metadata: {
3893
+ filename: 'test.txt',
3894
+ mimeType: 'text/plain'
3895
+ }
3896
+ },
3897
+ role: MessageRole.SERVER
3898
+ },
3899
+ origin
3900
+ })
3901
+ );
3902
+ setTimeout(() => {
3903
+ window.dispatchEvent(
3904
+ new MessageEvent('message', {
3905
+ data: {
3906
+ __requestIframe__: 1,
3907
+ timestamp: Date.now(),
3908
+ type: 'stream_data',
3909
+ requestId: msg.requestId,
3910
+ body: {
3911
+ streamId,
3912
+ data: btoa('Hello World'),
3913
+ done: true
3914
+ },
3915
+ role: MessageRole.SERVER
3916
+ },
3917
+ origin
3918
+ })
3919
+ );
3920
+ setTimeout(() => {
3921
+ window.dispatchEvent(
3922
+ new MessageEvent('message', {
3923
+ data: {
3924
+ __requestIframe__: 1,
3925
+ timestamp: Date.now(),
3926
+ type: 'stream_end',
3927
+ requestId: msg.requestId,
3928
+ body: { streamId },
3929
+ role: MessageRole.SERVER
3930
+ },
3931
+ origin
3932
+ })
3933
+ );
3934
+ }, 10);
3935
+ }, 10);
3936
+ }, 10);
3937
+ }
3938
+ })
3939
+ };
3940
+ Object.defineProperty(iframe, 'contentWindow', {
3941
+ value: mockContentWindow,
3942
+ writable: true
3943
+ });
3944
+
3945
+ const client = requestIframeClient(iframe);
3946
+ const response = await client.send('getFile', {}, {
3947
+ ackTimeout: 1000,
3948
+ timeout: 10000
3949
+ }) as any;
3950
+
3951
+ expect(response.stream).toBeDefined();
3952
+ expect(response.data).not.toBeInstanceOf(File); // Not auto-resolved, data is not a File
3953
+
3954
+ cleanupIframe(iframe);
3955
+ }, 20000);
3956
+
3957
+ it('should handle regular data stream (non-file)', async () => {
3958
+ const origin = 'https://example.com';
3959
+ const iframe = createTestIframe(origin);
3960
+ const mockContentWindow = {
3961
+ postMessage: jest.fn((msg: PostMessageData) => {
3962
+ if (msg.type === 'request') {
3963
+ window.dispatchEvent(
3964
+ new MessageEvent('message', {
3965
+ data: {
3966
+ __requestIframe__: 1,
3967
+ type: 'ack',
3968
+ requestId: msg.requestId,
3969
+ path: msg.path,
3970
+ role: MessageRole.SERVER
3971
+ },
3972
+ origin
3973
+ })
3974
+ );
3975
+ setTimeout(() => {
3976
+ const streamId = 'stream-test';
3977
+ window.dispatchEvent(
3978
+ new MessageEvent('message', {
3979
+ data: {
3980
+ __requestIframe__: 1,
3981
+ timestamp: Date.now(),
3982
+ type: 'stream_start',
3983
+ requestId: msg.requestId,
3984
+ status: 200,
3985
+ statusText: 'OK',
3986
+ body: {
3987
+ streamId,
3988
+ type: 'data',
3989
+ chunked: true
3990
+ },
3991
+ role: MessageRole.SERVER
3992
+ },
3993
+ origin
3994
+ })
3995
+ );
3996
+ setTimeout(() => {
3997
+ window.dispatchEvent(
3998
+ new MessageEvent('message', {
3999
+ data: {
4000
+ __requestIframe__: 1,
4001
+ timestamp: Date.now(),
4002
+ type: 'stream_data',
4003
+ requestId: msg.requestId,
4004
+ body: {
4005
+ streamId,
4006
+ data: btoa('chunk1'),
4007
+ done: false
4008
+ },
4009
+ role: MessageRole.SERVER
4010
+ },
4011
+ origin
4012
+ })
4013
+ );
4014
+ setTimeout(() => {
4015
+ window.dispatchEvent(
4016
+ new MessageEvent('message', {
4017
+ data: {
4018
+ __requestIframe__: 1,
4019
+ timestamp: Date.now(),
4020
+ type: 'stream_data',
4021
+ requestId: msg.requestId,
4022
+ body: {
4023
+ streamId,
4024
+ data: btoa('chunk2'),
4025
+ done: true
4026
+ },
4027
+ role: MessageRole.SERVER
4028
+ },
4029
+ origin
4030
+ })
4031
+ );
4032
+ setTimeout(() => {
4033
+ window.dispatchEvent(
4034
+ new MessageEvent('message', {
4035
+ data: {
4036
+ __requestIframe__: 1,
4037
+ timestamp: Date.now(),
4038
+ type: 'stream_end',
4039
+ requestId: msg.requestId,
4040
+ body: { streamId },
4041
+ role: MessageRole.SERVER
4042
+ },
4043
+ origin
4044
+ })
4045
+ );
4046
+ }, 10);
4047
+ }, 10);
4048
+ }, 10);
4049
+ }, 10);
4050
+ }
4051
+ })
4052
+ };
4053
+ Object.defineProperty(iframe, 'contentWindow', {
4054
+ value: mockContentWindow,
4055
+ writable: true
4056
+ });
4057
+
4058
+ const client = requestIframeClient(iframe);
4059
+ const response = await client.send('getStream', {}, {
4060
+ ackTimeout: 1000,
4061
+ timeout: 10000
4062
+ }) as any;
4063
+
4064
+ expect(response.stream).toBeDefined();
4065
+ expect(response.stream.type).toBe('data');
4066
+
4067
+ cleanupIframe(iframe);
4068
+ }, 20000);
4069
+
4070
+ it('should handle dispatchStreamMessage for stream messages', async () => {
4071
+ const origin = 'https://example.com';
4072
+ const iframe = createTestIframe(origin);
4073
+ const mockContentWindow = {
4074
+ postMessage: jest.fn()
4075
+ };
4076
+ Object.defineProperty(iframe, 'contentWindow', {
4077
+ value: mockContentWindow,
4078
+ writable: true
4079
+ });
4080
+
4081
+ const client = requestIframeClient(iframe);
4082
+ const streamId = 'test-stream';
4083
+ const handler = jest.fn();
4084
+
4085
+ // Register stream handler
4086
+ (client as any).registerStreamHandler(streamId, handler);
4087
+
4088
+ // Dispatch stream message
4089
+ window.dispatchEvent(
4090
+ new MessageEvent('message', {
4091
+ data: {
4092
+ __requestIframe__: 1,
4093
+ type: 'stream_data',
4094
+ requestId: 'req123',
4095
+ body: {
4096
+ streamId,
4097
+ data: 'test',
4098
+ type: 'data'
4099
+ },
4100
+ role: MessageRole.SERVER
4101
+ },
4102
+ origin
4103
+ })
4104
+ );
4105
+
4106
+ await new Promise(resolve => setTimeout(resolve, 50));
4107
+
4108
+ expect(handler).toHaveBeenCalled();
4109
+
4110
+ cleanupIframe(iframe);
4111
+ });
4112
+
4113
+ it('should handle error in response interceptor rejected callback', async () => {
4114
+ const origin = 'https://example.com';
4115
+ const iframe = createTestIframe(origin);
4116
+ const mockContentWindow = {
4117
+ postMessage: jest.fn((msg: PostMessageData) => {
4118
+ if (msg.type === 'request') {
4119
+ window.dispatchEvent(
4120
+ new MessageEvent('message', {
4121
+ data: {
4122
+ __requestIframe__: 1,
4123
+ type: 'ack',
4124
+ requestId: msg.requestId,
4125
+ path: msg.path,
4126
+ role: MessageRole.SERVER
4127
+ },
4128
+ origin
4129
+ })
4130
+ );
4131
+ setTimeout(() => {
4132
+ window.dispatchEvent(
4133
+ new MessageEvent('message', {
4134
+ data: {
4135
+ __requestIframe__: 1,
4136
+ type: 'error',
4137
+ requestId: msg.requestId,
4138
+ error: { message: 'Test error', code: 'TEST_ERROR' },
4139
+ status: 500,
4140
+ statusText: 'Internal Server Error',
4141
+ role: MessageRole.SERVER
4142
+ },
4143
+ origin
4144
+ })
4145
+ );
4146
+ }, 10);
4147
+ }
4148
+ })
4149
+ };
4150
+ Object.defineProperty(iframe, 'contentWindow', {
4151
+ value: mockContentWindow,
4152
+ writable: true
4153
+ });
4154
+
4155
+ const client = requestIframeClient(iframe);
4156
+
4157
+ // Add error interceptor that rejects
4158
+ client.interceptors.response.use(
4159
+ (response) => response,
4160
+ (error) => {
4161
+ // Reject to test the catch path
4162
+ return Promise.reject(error);
4163
+ }
4164
+ );
4165
+
4166
+ try {
4167
+ await client.send('test', {});
4168
+ fail('Should have thrown error');
4169
+ } catch (error: any) {
4170
+ expect(error.message).toBe('Test error');
4171
+ }
4172
+
4173
+ cleanupIframe(iframe);
4174
+ });
4175
+ });
4176
+
4177
+ describe('Server additional features', () => {
4178
+ it('should handle protocol version error', async () => {
4179
+ const origin = 'https://example.com';
4180
+ const iframe = createTestIframe(origin);
4181
+ const mockContentWindow = {
4182
+ postMessage: jest.fn()
4183
+ };
4184
+ Object.defineProperty(iframe, 'contentWindow', {
4185
+ value: mockContentWindow,
4186
+ writable: true
4187
+ });
4188
+
4189
+ const server = requestIframeServer();
4190
+
4191
+ // Send message with incompatible version
4192
+ window.dispatchEvent(
4193
+ new MessageEvent('message', {
4194
+ data: {
4195
+ __requestIframe__: 0, // Incompatible version
4196
+ timestamp: Date.now(),
4197
+ type: 'request',
4198
+ requestId: 'req123',
4199
+ path: 'test',
4200
+ role: MessageRole.CLIENT
4201
+ },
4202
+ origin,
4203
+ source: mockContentWindow as any
4204
+ })
4205
+ );
4206
+
4207
+ await new Promise(resolve => setTimeout(resolve, 50));
4208
+
4209
+ expect(mockContentWindow.postMessage).toHaveBeenCalledWith(
4210
+ expect.objectContaining({
4211
+ type: 'error',
4212
+ requestId: 'req123'
4213
+ }),
4214
+ origin
4215
+ );
4216
+
4217
+ server.destroy();
4218
+ cleanupIframe(iframe);
4219
+ });
4220
+
4221
+ it('should handle handler returning undefined result', async () => {
4222
+ const origin = 'https://example.com';
4223
+ const iframe = createTestIframe(origin);
4224
+ const mockContentWindow = {
4225
+ postMessage: jest.fn()
4226
+ };
4227
+ Object.defineProperty(iframe, 'contentWindow', {
4228
+ value: mockContentWindow,
4229
+ writable: true
4230
+ });
4231
+
4232
+ const server = requestIframeServer();
4233
+
4234
+ server.on('test', (req, res) => {
4235
+ // Handler doesn't return anything (undefined)
4236
+ // This should trigger NO_RESPONSE_SENT error
4237
+ });
4238
+
4239
+ window.dispatchEvent(
4240
+ new MessageEvent('message', {
4241
+ data: {
4242
+ __requestIframe__: 1,
4243
+ timestamp: Date.now(),
4244
+ type: 'request',
4245
+ requestId: 'req123',
4246
+ path: 'test',
4247
+ role: MessageRole.CLIENT,
4248
+ targetId: server.id
4249
+ },
4250
+ origin,
4251
+ source: mockContentWindow as any
4252
+ })
4253
+ );
4254
+
4255
+ await new Promise(resolve => setTimeout(resolve, 100));
4256
+
4257
+ const errorCall = mockContentWindow.postMessage.mock.calls.find(
4258
+ (call: any[]) => call[0]?.type === 'error' && call[0]?.requestId === 'req123'
4259
+ );
4260
+ expect(errorCall).toBeDefined();
4261
+ if (errorCall && errorCall[0]) {
4262
+ expect(errorCall[0]).toMatchObject({
4263
+ type: 'error',
4264
+ requestId: 'req123',
4265
+ error: expect.objectContaining({
4266
+ code: 'NO_RESPONSE'
4267
+ })
4268
+ });
4269
+ }
4270
+
4271
+ server.destroy();
4272
+ cleanupIframe(iframe);
4273
+ });
4274
+
4275
+ it('should skip processing when message already handled by another server', async () => {
4276
+ const origin = 'https://example.com';
4277
+ const iframe = createTestIframe(origin);
4278
+ const mockContentWindow = {
4279
+ postMessage: jest.fn()
4280
+ };
4281
+ Object.defineProperty(iframe, 'contentWindow', {
4282
+ value: mockContentWindow,
4283
+ writable: true
4284
+ });
4285
+
4286
+ const server1 = requestIframeServer();
4287
+ const server2 = requestIframeServer();
4288
+
4289
+ const handler1 = jest.fn((req, res) => res.send({ server: 1 }));
4290
+ const handler2 = jest.fn((req, res) => res.send({ server: 2 }));
4291
+
4292
+ server1.on('test', handler1);
4293
+ server2.on('test', handler2);
4294
+
4295
+ // Create a context that indicates message was already handled
4296
+ const messageData = {
4297
+ __requestIframe__: 1,
4298
+ timestamp: Date.now(),
4299
+ type: 'request' as const,
4300
+ requestId: 'req123',
4301
+ path: 'test',
4302
+ role: MessageRole.CLIENT,
4303
+ targetId: server1.id
4304
+ };
4305
+
4306
+ window.dispatchEvent(
4307
+ new MessageEvent('message', {
4308
+ data: messageData,
4309
+ origin,
4310
+ source: mockContentWindow as any
4311
+ })
4312
+ );
4313
+
4314
+ await new Promise(resolve => setTimeout(resolve, 100));
4315
+
4316
+ // Only server1 should handle it (because of targetId)
4317
+ expect(handler1).toHaveBeenCalled();
4318
+ expect(handler2).not.toHaveBeenCalled();
4319
+
4320
+ server1.destroy();
4321
+ server2.destroy();
4322
+ cleanupIframe(iframe);
4323
+ });
4324
+
4325
+ it('should handle ack timeout in registerPendingAck', async () => {
4326
+ const origin = 'https://example.com';
4327
+ const iframe = createTestIframe(origin);
4328
+ const mockContentWindow = {
4329
+ postMessage: jest.fn()
4330
+ };
4331
+ Object.defineProperty(iframe, 'contentWindow', {
4332
+ value: mockContentWindow,
4333
+ writable: true
4334
+ });
4335
+
4336
+ const server = requestIframeServer({ ackTimeout: 50 });
4337
+
4338
+ server.on('test', (req, res) => {
4339
+ // Send response with requireAck, but client never sends 'received'
4340
+ res.send({ result: 'success' }, { requireAck: true });
4341
+ });
4342
+
4343
+ window.dispatchEvent(
4344
+ new MessageEvent('message', {
4345
+ data: {
4346
+ __requestIframe__: 1,
4347
+ timestamp: Date.now(),
4348
+ type: 'request',
4349
+ requestId: 'req123',
4350
+ path: 'test',
4351
+ role: MessageRole.CLIENT,
4352
+ targetId: server.id
4353
+ },
4354
+ origin,
4355
+ source: mockContentWindow as any
4356
+ })
4357
+ );
4358
+
4359
+ // Wait for ack timeout
4360
+ await new Promise(resolve => setTimeout(resolve, 150));
4361
+
4362
+ // Server should have sent response
4363
+ expect(mockContentWindow.postMessage).toHaveBeenCalledWith(
4364
+ expect.objectContaining({
4365
+ type: 'response',
4366
+ requestId: 'req123'
4367
+ }),
4368
+ origin
4369
+ );
4370
+
4371
+ server.destroy();
4372
+ cleanupIframe(iframe);
4373
+ });
4374
+
4375
+ it('should handle middleware that sends response early', async () => {
4376
+ const origin = 'https://example.com';
4377
+ const iframe = createTestIframe(origin);
4378
+ const mockContentWindow = {
4379
+ postMessage: jest.fn()
4380
+ };
4381
+ Object.defineProperty(iframe, 'contentWindow', {
4382
+ value: mockContentWindow,
4383
+ writable: true
4384
+ });
4385
+
4386
+ const server = requestIframeServer();
4387
+
4388
+ const middleware = jest.fn((req, res, next) => {
4389
+ res.send({ middleware: true });
4390
+ // Don't call next() - response already sent
4391
+ });
4392
+
4393
+ const handler = jest.fn((req, res) => {
4394
+ res.send({ handler: true });
4395
+ });
4396
+
4397
+ server.use(middleware);
4398
+ server.on('test', handler);
4399
+
4400
+ window.dispatchEvent(
4401
+ new MessageEvent('message', {
4402
+ data: {
4403
+ __requestIframe__: 1,
4404
+ timestamp: Date.now(),
4405
+ type: 'request',
4406
+ requestId: 'req123',
4407
+ path: 'test',
4408
+ role: MessageRole.CLIENT,
4409
+ targetId: server.id
4410
+ },
4411
+ origin,
4412
+ source: mockContentWindow as any
4413
+ })
4414
+ );
4415
+
4416
+ await new Promise(resolve => setTimeout(resolve, 100));
4417
+
4418
+ // Middleware should be called
4419
+ expect(middleware).toHaveBeenCalled();
4420
+ // Handler should NOT be called because response was already sent
4421
+ expect(handler).not.toHaveBeenCalled();
4422
+
4423
+ // Response should be from middleware
4424
+ expect(mockContentWindow.postMessage).toHaveBeenCalledWith(
4425
+ expect.objectContaining({
4426
+ type: 'response',
4427
+ requestId: 'req123',
4428
+ data: { middleware: true }
4429
+ }),
4430
+ origin
4431
+ );
4432
+
4433
+ server.destroy();
4434
+ cleanupIframe(iframe);
4435
+ });
4436
+
4437
+ it('should handle map return cleanup function', () => {
4438
+ const server = requestIframeServer();
4439
+
4440
+ const handler1 = jest.fn((req, res) => res.send({}));
4441
+ const handler2 = jest.fn((req, res) => res.send({}));
4442
+
4443
+ const cleanup = server.map({
4444
+ 'path1': handler1,
4445
+ 'path2': handler2
4446
+ });
4447
+
4448
+ // Cleanup should unregister all handlers
4449
+ cleanup();
4450
+
4451
+ // Verify handlers are unregistered
4452
+ expect(server).toBeDefined();
4453
+
4454
+ server.destroy();
4455
+ });
4456
+ });
4457
+
4458
+ describe('Cache utilities', () => {
4459
+ it('should test server cache functions', () => {
4460
+ const { getCachedServer, cacheServer, removeCachedServer, clearServerCache } = require('../utils/cache');
4461
+ const { requestIframeServer } = require('../api/server');
4462
+
4463
+ // Test getCachedServer with no id
4464
+ expect(getCachedServer('key1')).toBeNull();
4465
+ expect(getCachedServer(undefined, undefined)).toBeNull();
4466
+
4467
+ // Test cacheServer with no id
4468
+ const server1 = requestIframeServer({ id: 'server1', secretKey: 'key1' });
4469
+ cacheServer(server1, 'key1', 'server1');
4470
+
4471
+ // Test getCachedServer with id
4472
+ const cached = getCachedServer('key1', 'server1');
4473
+ expect(cached).toBe(server1);
4474
+
4475
+ // Test removeCachedServer with no id
4476
+ removeCachedServer('key1'); // Should not throw
4477
+ removeCachedServer(undefined, undefined); // Should not throw
4478
+
4479
+ // Test removeCachedServer with id
4480
+ removeCachedServer('key1', 'server1');
4481
+ expect(getCachedServer('key1', 'server1')).toBeNull();
4482
+
4483
+ // Test clearServerCache
4484
+ const server2 = requestIframeServer({ id: 'server2', secretKey: 'key2' });
4485
+ cacheServer(server2, 'key2', 'server2');
4486
+ clearServerCache();
4487
+ expect(getCachedServer('key2', 'server2')).toBeNull();
4488
+
4489
+ server1.destroy();
4490
+ server2.destroy();
4491
+ });
4492
+
4493
+ it('should test clearMessageChannelCache', () => {
4494
+ const { clearMessageChannelCache, getOrCreateMessageChannel } = require('../utils/cache');
4495
+
4496
+ // Create a channel
4497
+ const channel1 = getOrCreateMessageChannel('test-key');
4498
+ expect(channel1).toBeDefined();
4499
+
4500
+ // Clear cache
4501
+ clearMessageChannelCache();
4502
+
4503
+ // Create another channel - should be new instance
4504
+ const channel2 = getOrCreateMessageChannel('test-key');
4505
+ expect(channel2).toBeDefined();
4506
+
4507
+ channel1.release();
4508
+ channel2.release();
4509
+ });
4510
+ });
4511
+
4512
+ describe('Additional edge cases', () => {
4513
+ it('should handle headers in request options', async () => {
4514
+ const origin = 'https://example.com';
4515
+ const iframe = createTestIframe(origin);
4516
+ const mockContentWindow = {
4517
+ postMessage: jest.fn((msg: PostMessageData) => {
4518
+ if (msg.type === 'request') {
4519
+ window.dispatchEvent(
4520
+ new MessageEvent('message', {
4521
+ data: {
4522
+ __requestIframe__: 1,
4523
+ type: 'ack',
4524
+ requestId: msg.requestId,
4525
+ path: msg.path,
4526
+ role: MessageRole.SERVER
4527
+ },
4528
+ origin
4529
+ })
4530
+ );
4531
+ setTimeout(() => {
4532
+ window.dispatchEvent(
4533
+ new MessageEvent('message', {
4534
+ data: {
4535
+ __requestIframe__: 1,
4536
+ type: 'response',
4537
+ requestId: msg.requestId,
4538
+ data: { result: 'success' },
4539
+ status: 200,
4540
+ statusText: 'OK',
4541
+ role: MessageRole.SERVER
4542
+ },
4543
+ origin
4544
+ })
4545
+ );
4546
+ }, 10);
4547
+ }
4548
+ })
4549
+ };
4550
+ Object.defineProperty(iframe, 'contentWindow', {
4551
+ value: mockContentWindow,
4552
+ writable: true
4553
+ });
4554
+
4555
+ const client = requestIframeClient(iframe, {
4556
+ headers: {
4557
+ 'X-Initial': 'initial-value'
4558
+ }
4559
+ });
4560
+
4561
+ // Send request with additional headers
4562
+ await client.send('test', {}, {
4563
+ headers: {
4564
+ 'X-Request': 'request-value',
4565
+ 'X-Dynamic': (config: RequestConfig) => `dynamic-${config.path}`
4566
+ }
4567
+ });
4568
+
4569
+ const requestCall = mockContentWindow.postMessage.mock.calls.find(
4570
+ (call: any[]) => call[0]?.type === 'request'
4571
+ );
4572
+ expect(requestCall).toBeDefined();
4573
+ if (requestCall && requestCall[0]) {
4574
+ expect(requestCall[0].headers?.['X-Initial']).toBe('initial-value');
4575
+ expect(requestCall[0].headers?.['X-Request']).toBe('request-value');
4576
+ expect(requestCall[0].headers?.['X-Dynamic']).toBe('dynamic-test');
4577
+ }
4578
+
4579
+ cleanupIframe(iframe);
4580
+ });
4581
+
4582
+ it('should handle isConnect with error response', async () => {
4583
+ const origin = 'https://example.com';
4584
+ const iframe = createTestIframe(origin);
4585
+ const mockContentWindow = {
4586
+ postMessage: jest.fn((msg: PostMessageData) => {
4587
+ if (msg.type === 'ping') {
4588
+ // Send error instead of pong
4589
+ setTimeout(() => {
4590
+ window.dispatchEvent(
4591
+ new MessageEvent('message', {
4592
+ data: {
4593
+ __requestIframe__: 1,
4594
+ type: 'error',
4595
+ requestId: msg.requestId,
4596
+ error: { message: 'Connection error' },
4597
+ role: MessageRole.SERVER
4598
+ },
4599
+ origin
4600
+ })
4601
+ );
4602
+ }, 10);
4603
+ }
4604
+ })
4605
+ };
4606
+ Object.defineProperty(iframe, 'contentWindow', {
4607
+ value: mockContentWindow,
4608
+ writable: true
4609
+ });
4610
+
4611
+ const client = requestIframeClient(iframe, { ackTimeout: 1000 });
4612
+
4613
+ const connected = await client.isConnect();
4614
+ expect(connected).toBe(false);
4615
+
4616
+ cleanupIframe(iframe);
4617
+ });
4618
+
4619
+ it('should handle response interceptor without rejected callback', async () => {
4620
+ const origin = 'https://example.com';
4621
+ const iframe = createTestIframe(origin);
4622
+ const mockContentWindow = {
4623
+ postMessage: jest.fn((msg: PostMessageData) => {
4624
+ if (msg.type === 'request') {
4625
+ window.dispatchEvent(
4626
+ new MessageEvent('message', {
4627
+ data: {
4628
+ __requestIframe__: 1,
4629
+ type: 'ack',
4630
+ requestId: msg.requestId,
4631
+ path: msg.path,
4632
+ role: MessageRole.SERVER
4633
+ },
4634
+ origin
4635
+ })
4636
+ );
4637
+ setTimeout(() => {
4638
+ window.dispatchEvent(
4639
+ new MessageEvent('message', {
4640
+ data: {
4641
+ __requestIframe__: 1,
4642
+ type: 'error',
4643
+ requestId: msg.requestId,
4644
+ error: { message: 'Test error', code: 'TEST_ERROR' },
4645
+ status: 500,
4646
+ statusText: 'Internal Server Error',
4647
+ role: MessageRole.SERVER
4648
+ },
4649
+ origin
4650
+ })
4651
+ );
4652
+ }, 10);
4653
+ }
4654
+ })
4655
+ };
4656
+ Object.defineProperty(iframe, 'contentWindow', {
4657
+ value: mockContentWindow,
4658
+ writable: true
4659
+ });
4660
+
4661
+ const client = requestIframeClient(iframe);
4662
+
4663
+ // Add response interceptor without rejected callback
4664
+ client.interceptors.response.use(
4665
+ (response) => response
4666
+ // No rejected callback - should test the Promise.reject path
4667
+ );
4668
+
4669
+ try {
4670
+ await client.send('test', {});
4671
+ fail('Should have thrown error');
4672
+ } catch (error: any) {
4673
+ expect(error.message).toBe('Test error');
4674
+ }
4675
+
4676
+ cleanupIframe(iframe);
4677
+ });
4678
+
4679
+ it('should handle request timeout', async () => {
4680
+ const origin = 'https://example.com';
4681
+ const iframe = createTestIframe(origin);
4682
+ const mockContentWindow = {
4683
+ postMessage: jest.fn((msg: PostMessageData) => {
4684
+ if (msg.type === 'request') {
4685
+ // Send ACK but never send response
4686
+ window.dispatchEvent(
4687
+ new MessageEvent('message', {
4688
+ data: {
4689
+ __requestIframe__: 1,
4690
+ type: 'ack',
4691
+ requestId: msg.requestId,
4692
+ path: msg.path,
4693
+ role: MessageRole.SERVER
4694
+ },
4695
+ origin
4696
+ })
4697
+ );
4698
+ // Don't send response - should timeout
4699
+ }
4700
+ })
4701
+ };
4702
+ Object.defineProperty(iframe, 'contentWindow', {
4703
+ value: mockContentWindow,
4704
+ writable: true
4705
+ });
4706
+
4707
+ const client = requestIframeClient(iframe, { timeout: 50 });
4708
+
4709
+ try {
4710
+ await client.send('test', {});
4711
+ fail('Should have timed out');
4712
+ } catch (error: any) {
4713
+ expect(error.message).toContain('timeout');
4714
+ }
4715
+
4716
+ cleanupIframe(iframe);
4717
+ });
4718
+
4719
+ it('should handle async timeout', async () => {
4720
+ const origin = 'https://example.com';
4721
+ const iframe = createTestIframe(origin);
4722
+ const mockContentWindow = {
4723
+ postMessage: jest.fn((msg: PostMessageData) => {
4724
+ if (msg.type === 'request') {
4725
+ window.dispatchEvent(
4726
+ new MessageEvent('message', {
4727
+ data: {
4728
+ __requestIframe__: 1,
4729
+ type: 'ack',
4730
+ requestId: msg.requestId,
4731
+ path: msg.path,
4732
+ role: MessageRole.SERVER
4733
+ },
4734
+ origin
4735
+ })
4736
+ );
4737
+ setTimeout(() => {
4738
+ // Send ASYNC but never send response
4739
+ window.dispatchEvent(
4740
+ new MessageEvent('message', {
4741
+ data: {
4742
+ __requestIframe__: 1,
4743
+ type: 'async',
4744
+ requestId: msg.requestId,
4745
+ path: msg.path,
4746
+ role: MessageRole.SERVER
4747
+ },
4748
+ origin
4749
+ })
4750
+ );
4751
+ }, 10);
4752
+ // Don't send response - should timeout
4753
+ }
4754
+ })
4755
+ };
4756
+ Object.defineProperty(iframe, 'contentWindow', {
4757
+ value: mockContentWindow,
4758
+ writable: true
4759
+ });
4760
+
4761
+ const client = requestIframeClient(iframe, { asyncTimeout: 50 });
4762
+
4763
+ try {
4764
+ await client.send('test', {});
4765
+ fail('Should have timed out');
4766
+ } catch (error: any) {
4767
+ expect(error.message).toContain('timeout');
4768
+ }
4769
+
4770
+ cleanupIframe(iframe);
4771
+ });
4772
+
4773
+ it('should not override existing targetServerId when receiving ACK', async () => {
4774
+ const origin = 'https://example.com';
4775
+ const iframe = createTestIframe(origin);
4776
+ const existingServerId = 'existing-server';
4777
+ const newServerId = 'new-server';
4778
+
4779
+ const mockContentWindow = {
4780
+ postMessage: jest.fn((msg: PostMessageData) => {
4781
+ if (msg.type === 'request') {
4782
+ window.dispatchEvent(
4783
+ new MessageEvent('message', {
4784
+ data: {
4785
+ __requestIframe__: 1,
4786
+ type: 'ack',
4787
+ requestId: msg.requestId,
4788
+ path: msg.path,
4789
+ role: MessageRole.SERVER,
4790
+ creatorId: newServerId
4791
+ },
4792
+ origin
4793
+ })
4794
+ );
4795
+ setTimeout(() => {
4796
+ window.dispatchEvent(
4797
+ new MessageEvent('message', {
4798
+ data: {
4799
+ __requestIframe__: 1,
4800
+ type: 'response',
4801
+ requestId: msg.requestId,
4802
+ data: { result: 'success' },
4803
+ status: 200,
4804
+ statusText: 'OK',
4805
+ role: MessageRole.SERVER,
4806
+ creatorId: newServerId
4807
+ },
4808
+ origin
4809
+ })
4810
+ );
4811
+ }, 10);
4812
+ }
4813
+ })
4814
+ };
4815
+ Object.defineProperty(iframe, 'contentWindow', {
4816
+ value: mockContentWindow,
4817
+ writable: true
4818
+ });
4819
+
4820
+ const client = requestIframeClient(iframe);
4821
+
4822
+ // Set existing targetServerId
4823
+ (client as any)._targetServerId = existingServerId;
4824
+
4825
+ await client.send('test', {});
4826
+
4827
+ // targetServerId should not be overridden
4828
+ expect((client as any)._targetServerId).toBe(existingServerId);
4829
+
4830
+ cleanupIframe(iframe);
4831
+ });
4832
+
4833
+ it('should handle response with requireAck', async () => {
4834
+ const origin = 'https://example.com';
4835
+ const iframe = createTestIframe(origin);
4836
+ const mockContentWindow = {
4837
+ postMessage: jest.fn((msg: PostMessageData) => {
4838
+ if (msg.type === 'request') {
4839
+ window.dispatchEvent(
4840
+ new MessageEvent('message', {
4841
+ data: {
4842
+ __requestIframe__: 1,
4843
+ type: 'ack',
4844
+ requestId: msg.requestId,
4845
+ path: msg.path,
4846
+ role: MessageRole.SERVER
4847
+ },
4848
+ origin
4849
+ })
4850
+ );
4851
+ setTimeout(() => {
4852
+ window.dispatchEvent(
4853
+ new MessageEvent('message', {
4854
+ data: {
4855
+ __requestIframe__: 1,
4856
+ type: 'response',
4857
+ requestId: msg.requestId,
4858
+ data: { result: 'success' },
4859
+ status: 200,
4860
+ statusText: 'OK',
4861
+ role: MessageRole.SERVER,
4862
+ requireAck: true
4863
+ },
4864
+ origin
4865
+ })
4866
+ );
4867
+ }, 10);
4868
+ } else if (msg.type === 'received') {
4869
+ // Acknowledge receipt
4870
+ }
4871
+ })
4872
+ };
4873
+ Object.defineProperty(iframe, 'contentWindow', {
4874
+ value: mockContentWindow,
4875
+ writable: true
4876
+ });
4877
+
4878
+ const client = requestIframeClient(iframe);
4879
+ const response = await client.send('test', {});
4880
+
4881
+ expect(response.data).toEqual({ result: 'success' });
4882
+
4883
+ // Verify RECEIVED message was sent
4884
+ const receivedCall = mockContentWindow.postMessage.mock.calls.find(
4885
+ (call: any[]) => call[0]?.type === 'received'
4886
+ );
4887
+ expect(receivedCall).toBeDefined();
4888
+
4889
+ cleanupIframe(iframe);
4890
+ });
4891
+
4892
+ it('should handle handler returning a value', async () => {
4893
+ const origin = 'https://example.com';
4894
+ const iframe = createTestIframe(origin);
4895
+ const mockContentWindow = {
4896
+ postMessage: jest.fn()
4897
+ };
4898
+ Object.defineProperty(iframe, 'contentWindow', {
4899
+ value: mockContentWindow,
4900
+ writable: true
4901
+ });
4902
+
4903
+ const server = requestIframeServer();
4904
+
4905
+ // Handler returns a value (not undefined)
4906
+ server.on('test', (req, res) => {
4907
+ return { result: 'from-return' };
4908
+ });
4909
+
4910
+ window.dispatchEvent(
4911
+ new MessageEvent('message', {
4912
+ data: {
4913
+ __requestIframe__: 1,
4914
+ timestamp: Date.now(),
4915
+ type: 'request',
4916
+ requestId: 'req123',
4917
+ path: 'test',
4918
+ role: MessageRole.CLIENT,
4919
+ targetId: server.id
4920
+ },
4921
+ origin,
4922
+ source: mockContentWindow as any
4923
+ })
4924
+ );
4925
+
4926
+ await new Promise(resolve => setTimeout(resolve, 100));
4927
+
4928
+ // Should send response with returned value
4929
+ expect(mockContentWindow.postMessage).toHaveBeenCalledWith(
4930
+ expect.objectContaining({
4931
+ type: 'response',
4932
+ requestId: 'req123',
4933
+ data: { result: 'from-return' }
4934
+ }),
4935
+ origin
4936
+ );
4937
+
4938
+ server.destroy();
4939
+ cleanupIframe(iframe);
4940
+ });
4941
+
4942
+ it('should handle ack timeout in registerPendingAck reject callback', async () => {
4943
+ const origin = 'https://example.com';
4944
+ const iframe = createTestIframe(origin);
4945
+ const mockContentWindow = {
4946
+ postMessage: jest.fn()
4947
+ };
4948
+ Object.defineProperty(iframe, 'contentWindow', {
4949
+ value: mockContentWindow,
4950
+ writable: true
4951
+ });
4952
+
4953
+ const server = requestIframeServer({ ackTimeout: 50 });
4954
+
4955
+ server.on('test', (req, res) => {
4956
+ // Send response with requireAck, but client never sends 'received'
4957
+ res.send({ result: 'success' }, { requireAck: true });
4958
+ });
4959
+
4960
+ window.dispatchEvent(
4961
+ new MessageEvent('message', {
4962
+ data: {
4963
+ __requestIframe__: 1,
4964
+ timestamp: Date.now(),
4965
+ type: 'request',
4966
+ requestId: 'req123',
4967
+ path: 'test',
4968
+ role: MessageRole.CLIENT,
4969
+ targetId: server.id
4970
+ },
4971
+ origin,
4972
+ source: mockContentWindow as any
4973
+ })
4974
+ );
4975
+
4976
+ // Wait for ack timeout (reject callback should be called)
4977
+ await new Promise(resolve => setTimeout(resolve, 150));
4978
+
4979
+ // Server should have sent response
4980
+ expect(mockContentWindow.postMessage).toHaveBeenCalledWith(
4981
+ expect.objectContaining({
4982
+ type: 'response',
4983
+ requestId: 'req123'
4984
+ }),
4985
+ origin
4986
+ );
4987
+
4988
+ server.destroy();
4989
+ cleanupIframe(iframe);
4990
+ });
4991
+
4992
+ it('should skip middleware when response already sent', async () => {
4993
+ const origin = 'https://example.com';
4994
+ const iframe = createTestIframe(origin);
4995
+ const mockContentWindow = {
4996
+ postMessage: jest.fn()
4997
+ };
4998
+ Object.defineProperty(iframe, 'contentWindow', {
4999
+ value: mockContentWindow,
5000
+ writable: true
5001
+ });
5002
+
5003
+ const server = requestIframeServer();
5004
+
5005
+ const middleware1 = jest.fn((req, res, next) => {
5006
+ res.send({ middleware1: true });
5007
+ // Response sent, don't call next
5008
+ });
5009
+
5010
+ const middleware2 = jest.fn((req, res, next) => {
5011
+ next();
5012
+ });
5013
+
5014
+ const handler = jest.fn((req, res) => {
5015
+ res.send({ handler: true });
5016
+ });
5017
+
5018
+ server.use(middleware1);
5019
+ server.use(middleware2);
5020
+ server.on('test', handler);
5021
+
5022
+ window.dispatchEvent(
5023
+ new MessageEvent('message', {
5024
+ data: {
5025
+ __requestIframe__: 1,
5026
+ timestamp: Date.now(),
5027
+ type: 'request',
5028
+ requestId: 'req123',
5029
+ path: 'test',
5030
+ role: MessageRole.CLIENT,
5031
+ targetId: server.id
5032
+ },
5033
+ origin,
5034
+ source: mockContentWindow as any
5035
+ })
5036
+ );
5037
+
5038
+ await new Promise(resolve => setTimeout(resolve, 100));
5039
+
5040
+ // Middleware1 should be called
5041
+ expect(middleware1).toHaveBeenCalled();
5042
+ // Middleware2 should NOT be called because response was already sent in middleware1
5043
+ expect(middleware2).not.toHaveBeenCalled();
5044
+ // Handler should NOT be called because response was already sent
5045
+ expect(handler).not.toHaveBeenCalled();
5046
+
5047
+ server.destroy();
5048
+ cleanupIframe(iframe);
5049
+ });
5050
+
5051
+ it('should handle isConnect reject callback', async () => {
5052
+ const origin = 'https://example.com';
5053
+ const iframe = createTestIframe(origin);
5054
+ const mockContentWindow = {
5055
+ postMessage: jest.fn()
5056
+ };
5057
+ Object.defineProperty(iframe, 'contentWindow', {
5058
+ value: mockContentWindow,
5059
+ writable: true
5060
+ });
5061
+
5062
+ const client = requestIframeClient(iframe, { ackTimeout: 50 });
5063
+
5064
+ // Simulate error in pending request registration
5065
+ // This will trigger the reject callback
5066
+ const connected = await client.isConnect();
5067
+ expect(connected).toBe(false);
5068
+
5069
+ cleanupIframe(iframe);
5070
+ });
5071
+
5072
+ it('should handle stream messages via dispatchStreamMessage', async () => {
5073
+ const origin = 'https://example.com';
5074
+ const iframe = createTestIframe(origin);
5075
+ const mockContentWindow = {
5076
+ postMessage: jest.fn()
5077
+ };
5078
+ Object.defineProperty(iframe, 'contentWindow', {
5079
+ value: mockContentWindow,
5080
+ writable: true
5081
+ });
5082
+
5083
+ const client = requestIframeClient(iframe);
5084
+ const streamId = 'test-stream';
5085
+ const handler = jest.fn();
5086
+
5087
+ // Register stream handler
5088
+ (client as any).registerStreamHandler(streamId, handler);
5089
+
5090
+ // Dispatch stream_data message
5091
+ window.dispatchEvent(
5092
+ new MessageEvent('message', {
5093
+ data: {
5094
+ __requestIframe__: 1,
5095
+ type: 'stream_data',
5096
+ requestId: 'req123',
5097
+ body: {
5098
+ streamId,
5099
+ data: 'test',
5100
+ type: 'data'
5101
+ },
5102
+ role: MessageRole.SERVER
5103
+ },
5104
+ origin
5105
+ })
5106
+ );
5107
+
5108
+ await new Promise(resolve => setTimeout(resolve, 50));
5109
+
5110
+ expect(handler).toHaveBeenCalled();
5111
+
5112
+ // Dispatch stream_end message
5113
+ window.dispatchEvent(
5114
+ new MessageEvent('message', {
5115
+ data: {
5116
+ __requestIframe__: 1,
5117
+ type: 'stream_end',
5118
+ requestId: 'req123',
5119
+ body: {
5120
+ streamId
5121
+ },
5122
+ role: MessageRole.SERVER
5123
+ },
5124
+ origin
5125
+ })
5126
+ );
5127
+
5128
+ await new Promise(resolve => setTimeout(resolve, 50));
5129
+
5130
+ cleanupIframe(iframe);
5131
+ });
5132
+
5133
+ it('should handle error with requireAck', async () => {
5134
+ const origin = 'https://example.com';
5135
+ const iframe = createTestIframe(origin);
5136
+ const mockContentWindow = {
5137
+ postMessage: jest.fn((msg: PostMessageData) => {
5138
+ if (msg.type === 'request') {
5139
+ window.dispatchEvent(
5140
+ new MessageEvent('message', {
5141
+ data: {
5142
+ __requestIframe__: 1,
5143
+ type: 'ack',
5144
+ requestId: msg.requestId,
5145
+ path: msg.path,
5146
+ role: MessageRole.SERVER
5147
+ },
5148
+ origin
5149
+ })
5150
+ );
5151
+ setTimeout(() => {
5152
+ window.dispatchEvent(
5153
+ new MessageEvent('message', {
5154
+ data: {
5155
+ __requestIframe__: 1,
5156
+ type: 'error',
5157
+ requestId: msg.requestId,
5158
+ error: { message: 'Test error', code: 'TEST_ERROR' },
5159
+ status: 500,
5160
+ statusText: 'Internal Server Error',
5161
+ role: MessageRole.SERVER,
5162
+ requireAck: true
5163
+ },
5164
+ origin
5165
+ })
5166
+ );
5167
+ }, 10);
5168
+ }
5169
+ })
5170
+ };
5171
+ Object.defineProperty(iframe, 'contentWindow', {
5172
+ value: mockContentWindow,
5173
+ writable: true
5174
+ });
5175
+
5176
+ const client = requestIframeClient(iframe);
5177
+
5178
+ try {
5179
+ await client.send('test', {});
5180
+ fail('Should have thrown error');
5181
+ } catch (error: any) {
5182
+ expect(error.message).toBe('Test error');
5183
+ }
5184
+
5185
+ // Verify RECEIVED message was sent
5186
+ const receivedCall = mockContentWindow.postMessage.mock.calls.find(
5187
+ (call: any[]) => call[0]?.type === 'received'
5188
+ );
5189
+ expect(receivedCall).toBeDefined();
5190
+
5191
+ cleanupIframe(iframe);
5192
+ });
5193
+
5194
+ it('should handle error in pending request registration', async () => {
5195
+ const origin = 'https://example.com';
5196
+ const iframe = createTestIframe(origin);
5197
+ const mockContentWindow = {
5198
+ postMessage: jest.fn()
5199
+ };
5200
+ Object.defineProperty(iframe, 'contentWindow', {
5201
+ value: mockContentWindow,
5202
+ writable: true
5203
+ });
5204
+
5205
+ const client = requestIframeClient(iframe);
5206
+
5207
+ // Simulate error during request registration
5208
+ // This will trigger the error callback in _registerPendingRequest
5209
+ try {
5210
+ // Force an error by making the server unavailable
5211
+ await client.send('test', {}, { timeout: 50 });
5212
+ fail('Should have thrown error');
5213
+ } catch (error: any) {
5214
+ expect(error).toBeDefined();
5215
+ }
5216
+
5217
+ cleanupIframe(iframe);
5218
+ });
5219
+
5220
+ it('should handle message already handled by another server', async () => {
5221
+ const origin = 'https://example.com';
5222
+ const iframe = createTestIframe(origin);
5223
+ const mockContentWindow = {
5224
+ postMessage: jest.fn()
5225
+ };
5226
+ Object.defineProperty(iframe, 'contentWindow', {
5227
+ value: mockContentWindow,
5228
+ writable: true
5229
+ });
5230
+
5231
+ const server1 = requestIframeServer();
5232
+ const server2 = requestIframeServer();
5233
+
5234
+ const handler1 = jest.fn((req, res) => res.send({ server: 1 }));
5235
+ const handler2 = jest.fn((req, res) => res.send({ server: 2 }));
5236
+
5237
+ server1.on('test', handler1);
5238
+ server2.on('test', handler2);
5239
+
5240
+ // Create message context that indicates it was already handled
5241
+ // This simulates the case where context.handledBy is set
5242
+ const messageData = {
5243
+ __requestIframe__: 1,
5244
+ timestamp: Date.now(),
5245
+ type: 'request' as const,
5246
+ requestId: 'req123',
5247
+ path: 'test',
5248
+ role: MessageRole.CLIENT,
5249
+ targetId: server1.id
5250
+ };
5251
+
5252
+ // First server processes it
5253
+ window.dispatchEvent(
5254
+ new MessageEvent('message', {
5255
+ data: messageData,
5256
+ origin,
5257
+ source: mockContentWindow as any
5258
+ })
5259
+ );
5260
+
5261
+ await new Promise(resolve => setTimeout(resolve, 50));
5262
+
5263
+ // Only server1 should handle it (because of targetId)
5264
+ expect(handler1).toHaveBeenCalled();
5265
+ expect(handler2).not.toHaveBeenCalled();
5266
+
5267
+ server1.destroy();
5268
+ server2.destroy();
5269
+ cleanupIframe(iframe);
5270
+ });
5271
+
5272
+ it('should handle ack timeout reject callback', async () => {
5273
+ const origin = 'https://example.com';
5274
+ const iframe = createTestIframe(origin);
5275
+ const mockContentWindow = {
5276
+ postMessage: jest.fn()
5277
+ };
5278
+ Object.defineProperty(iframe, 'contentWindow', {
5279
+ value: mockContentWindow,
5280
+ writable: true
5281
+ });
5282
+
5283
+ const server = requestIframeServer({ ackTimeout: 50 });
5284
+
5285
+ server.on('test', (req, res) => {
5286
+ // Send response with requireAck, but client never sends 'received'
5287
+ // This will trigger ack timeout and the reject callback
5288
+ res.send({ result: 'success' }, { requireAck: true });
5289
+ });
5290
+
5291
+ window.dispatchEvent(
5292
+ new MessageEvent('message', {
5293
+ data: {
5294
+ __requestIframe__: 1,
5295
+ timestamp: Date.now(),
5296
+ type: 'request',
5297
+ requestId: 'req123',
5298
+ path: 'test',
5299
+ role: MessageRole.CLIENT,
5300
+ targetId: server.id
5301
+ },
5302
+ origin,
5303
+ source: mockContentWindow as any
5304
+ })
5305
+ );
5306
+
5307
+ // Wait for ack timeout (reject callback should be called)
5308
+ await new Promise(resolve => setTimeout(resolve, 150));
5309
+
5310
+ // Server should have sent response
5311
+ expect(mockContentWindow.postMessage).toHaveBeenCalledWith(
5312
+ expect.objectContaining({
5313
+ type: 'response',
5314
+ requestId: 'req123'
5315
+ }),
5316
+ origin
5317
+ );
5318
+
5319
+ server.destroy();
5320
+ cleanupIframe(iframe);
5321
+ });
5322
+
5323
+ it('should skip next middleware when response already sent', async () => {
5324
+ const origin = 'https://example.com';
5325
+ const iframe = createTestIframe(origin);
5326
+ const mockContentWindow = {
5327
+ postMessage: jest.fn()
5328
+ };
5329
+ Object.defineProperty(iframe, 'contentWindow', {
5330
+ value: mockContentWindow,
5331
+ writable: true
5332
+ });
5333
+
5334
+ const server = requestIframeServer();
5335
+
5336
+ const middleware1 = jest.fn((req, res, next) => {
5337
+ res.send({ middleware1: true });
5338
+ // Response sent, but still call next to test the res._sent check
5339
+ next();
5340
+ });
5341
+
5342
+ const middleware2 = jest.fn((req, res, next) => {
5343
+ // This should not execute because res._sent is true
5344
+ next();
5345
+ });
5346
+
5347
+ const handler = jest.fn((req, res) => {
5348
+ res.send({ handler: true });
5349
+ });
5350
+
5351
+ server.use(middleware1);
5352
+ server.use(middleware2);
5353
+ server.on('test', handler);
5354
+
5355
+ window.dispatchEvent(
5356
+ new MessageEvent('message', {
5357
+ data: {
5358
+ __requestIframe__: 1,
5359
+ timestamp: Date.now(),
5360
+ type: 'request',
5361
+ requestId: 'req123',
5362
+ path: 'test',
5363
+ role: MessageRole.CLIENT,
5364
+ targetId: server.id
5365
+ },
5366
+ origin,
5367
+ source: mockContentWindow as any
5368
+ })
5369
+ );
5370
+
5371
+ await new Promise(resolve => setTimeout(resolve, 100));
5372
+
5373
+ // Middleware1 should be called
5374
+ expect(middleware1).toHaveBeenCalled();
5375
+ // Middleware2's next() should check res._sent and return early, so handler should not be called
5376
+ // Note: middleware2 itself may or may not be called depending on implementation
5377
+ // Handler should NOT be called because response was already sent
5378
+ expect(handler).not.toHaveBeenCalled();
5379
+
5380
+ server.destroy();
5381
+ cleanupIframe(iframe);
5382
+ });
5383
+ });
2406
5384
  });