request-iframe 0.0.1 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/README.CN.md +271 -12
  2. package/README.md +268 -11
  3. package/library/__tests__/channel.test.ts +420 -0
  4. package/library/__tests__/debug.test.ts +588 -0
  5. package/library/__tests__/dispatcher.test.ts +481 -0
  6. package/library/__tests__/interceptors.test.ts +22 -0
  7. package/library/__tests__/requestIframe.test.ts +2317 -99
  8. package/library/__tests__/server.test.ts +738 -0
  9. package/library/api/client.d.js +5 -0
  10. package/library/api/client.d.ts.map +1 -1
  11. package/library/api/client.js +11 -6
  12. package/library/api/server.d.js +5 -0
  13. package/library/api/server.d.ts +4 -3
  14. package/library/api/server.d.ts.map +1 -1
  15. package/library/api/server.js +25 -7
  16. package/library/constants/index.d.js +36 -0
  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.js +5 -0
  21. package/library/constants/messages.d.ts +35 -0
  22. package/library/constants/messages.d.ts.map +1 -1
  23. package/library/constants/messages.js +36 -1
  24. package/library/core/client-server.d.ts +101 -0
  25. package/library/core/client-server.d.ts.map +1 -0
  26. package/library/core/client-server.js +266 -0
  27. package/library/core/client.d.js +5 -0
  28. package/library/core/client.d.ts +38 -6
  29. package/library/core/client.d.ts.map +1 -1
  30. package/library/core/client.js +198 -24
  31. package/library/core/request.d.js +5 -0
  32. package/library/core/response.d.js +5 -0
  33. package/library/core/response.d.ts +5 -1
  34. package/library/core/response.d.ts.map +1 -1
  35. package/library/core/response.js +85 -70
  36. package/library/core/server-client.d.js +5 -0
  37. package/library/core/server-client.d.ts +3 -1
  38. package/library/core/server-client.d.ts.map +1 -1
  39. package/library/core/server-client.js +19 -9
  40. package/library/core/server.d.js +5 -0
  41. package/library/core/server.d.ts +11 -3
  42. package/library/core/server.d.ts.map +1 -1
  43. package/library/core/server.js +112 -54
  44. package/library/index.d.ts +1 -1
  45. package/library/index.js +2 -2
  46. package/library/interceptors/index.d.js +5 -0
  47. package/library/interceptors/index.d.ts +4 -0
  48. package/library/interceptors/index.d.ts.map +1 -1
  49. package/library/interceptors/index.js +7 -0
  50. package/library/message/channel.d.js +5 -0
  51. package/library/message/channel.d.ts +3 -1
  52. package/library/message/channel.d.ts.map +1 -1
  53. package/library/message/dispatcher.d.js +5 -0
  54. package/library/message/dispatcher.d.ts +7 -2
  55. package/library/message/dispatcher.d.ts.map +1 -1
  56. package/library/message/dispatcher.js +47 -2
  57. package/library/message/index.d.js +25 -0
  58. package/library/stream/file-stream.d.js +4 -0
  59. package/library/stream/file-stream.d.ts +5 -0
  60. package/library/stream/file-stream.d.ts.map +1 -1
  61. package/library/stream/file-stream.js +41 -12
  62. package/library/stream/index.d.js +58 -0
  63. package/library/stream/readable-stream.d.js +5 -0
  64. package/library/stream/readable-stream.d.ts.map +1 -1
  65. package/library/stream/readable-stream.js +32 -30
  66. package/library/stream/types.d.js +5 -0
  67. package/library/stream/types.d.ts +18 -0
  68. package/library/stream/types.d.ts.map +1 -1
  69. package/library/stream/writable-stream.d.js +5 -0
  70. package/library/stream/writable-stream.d.ts +1 -0
  71. package/library/stream/writable-stream.d.ts.map +1 -1
  72. package/library/stream/writable-stream.js +7 -2
  73. package/library/types/index.d.js +5 -0
  74. package/library/types/index.d.ts +79 -19
  75. package/library/types/index.d.ts.map +1 -1
  76. package/library/utils/cache.d.js +5 -0
  77. package/library/utils/cache.d.ts +24 -0
  78. package/library/utils/cache.d.ts.map +1 -1
  79. package/library/utils/cache.js +76 -0
  80. package/library/utils/cookie.d.js +5 -0
  81. package/library/utils/debug.d.js +5 -0
  82. package/library/utils/debug.d.ts.map +1 -1
  83. package/library/utils/debug.js +382 -20
  84. package/library/utils/index.d.js +94 -0
  85. package/library/utils/index.d.ts +5 -0
  86. package/library/utils/index.d.ts.map +1 -1
  87. package/library/utils/index.js +14 -1
  88. package/library/utils/path-match.d.js +5 -0
  89. package/library/utils/protocol.d.js +5 -0
  90. package/package.json +16 -2
  91. package/react/library/__tests__/index.test.d.ts +2 -0
  92. package/react/library/__tests__/index.test.d.ts.map +1 -0
  93. package/react/library/__tests__/index.test.tsx +770 -0
  94. package/react/library/index.d.ts +118 -0
  95. package/react/library/index.d.ts.map +1 -0
  96. package/react/library/index.js +232 -0
@@ -1,7 +1,7 @@
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
5
 
6
6
  /**
7
7
  * Create test iframe
@@ -64,7 +64,8 @@ describe('requestIframeClient and requestIframeServer', () => {
64
64
  __requestIframe__: 1,
65
65
  type: 'ack',
66
66
  requestId: msg.requestId,
67
- path: msg.path
67
+ path: msg.path,
68
+ role: MessageRole.SERVER
68
69
  },
69
70
  origin
70
71
  })
@@ -79,7 +80,8 @@ describe('requestIframeClient and requestIframeServer', () => {
79
80
  requestId: msg.requestId,
80
81
  data: { result: 'success' },
81
82
  status: 200,
82
- statusText: 'OK'
83
+ statusText: 'OK',
84
+ role: MessageRole.SERVER
83
85
  },
84
86
  origin
85
87
  })
@@ -157,7 +159,8 @@ describe('requestIframeClient and requestIframeServer', () => {
157
159
  __requestIframe__: 1,
158
160
  type: 'pong',
159
161
  requestId: msg.requestId,
160
- secretKey: msg.secretKey
162
+ secretKey: msg.secretKey,
163
+ role: MessageRole.SERVER
161
164
  },
162
165
  origin
163
166
  })
@@ -199,7 +202,8 @@ describe('requestIframeClient and requestIframeServer', () => {
199
202
  __requestIframe__: 1,
200
203
  type: 'ack',
201
204
  requestId: msg.requestId,
202
- path: msg.path
205
+ path: msg.path,
206
+ role: MessageRole.SERVER
203
207
  },
204
208
  origin
205
209
  })
@@ -215,7 +219,8 @@ describe('requestIframeClient and requestIframeServer', () => {
215
219
  requestId: msg.requestId,
216
220
  data: { success: true },
217
221
  status: 200,
218
- statusText: 'OK'
222
+ statusText: 'OK',
223
+ role: MessageRole.SERVER
219
224
  },
220
225
  origin
221
226
  })
@@ -262,7 +267,8 @@ describe('requestIframeClient and requestIframeServer', () => {
262
267
  __requestIframe__: 1,
263
268
  type: 'ack',
264
269
  requestId: msg.requestId,
265
- path: msg.path
270
+ path: msg.path,
271
+ role: MessageRole.SERVER
266
272
  },
267
273
  origin
268
274
  })
@@ -278,7 +284,8 @@ describe('requestIframeClient and requestIframeServer', () => {
278
284
  requestId: msg.requestId,
279
285
  data: { success: true },
280
286
  status: 200,
281
- statusText: 'OK'
287
+ statusText: 'OK',
288
+ role: MessageRole.SERVER
282
289
  },
283
290
  origin
284
291
  })
@@ -328,7 +335,8 @@ describe('requestIframeClient and requestIframeServer', () => {
328
335
  __requestIframe__: 1,
329
336
  type: 'ack',
330
337
  requestId: msg.requestId,
331
- path: msg.path
338
+ path: msg.path,
339
+ role: MessageRole.SERVER
332
340
  },
333
341
  origin
334
342
  })
@@ -347,7 +355,8 @@ describe('requestIframeClient and requestIframeServer', () => {
347
355
  code: 'METHOD_NOT_FOUND'
348
356
  },
349
357
  status: 404,
350
- statusText: 'Not Found'
358
+ statusText: 'Not Found',
359
+ role: MessageRole.SERVER
351
360
  },
352
361
  origin
353
362
  })
@@ -389,7 +398,8 @@ describe('requestIframeClient and requestIframeServer', () => {
389
398
  __requestIframe__: 1,
390
399
  type: 'ack',
391
400
  requestId: msg.requestId,
392
- path: msg.path
401
+ path: msg.path,
402
+ role: MessageRole.SERVER
393
403
  },
394
404
  origin
395
405
  })
@@ -403,7 +413,8 @@ describe('requestIframeClient and requestIframeServer', () => {
403
413
  __requestIframe__: 1,
404
414
  type: 'async',
405
415
  requestId: msg.requestId,
406
- path: msg.path
416
+ path: msg.path,
417
+ role: MessageRole.SERVER
407
418
  },
408
419
  origin
409
420
  })
@@ -420,7 +431,8 @@ describe('requestIframeClient and requestIframeServer', () => {
420
431
  requestId: msg.requestId,
421
432
  data: { result: 'async success' },
422
433
  status: 200,
423
- statusText: 'OK'
434
+ statusText: 'OK',
435
+ role: MessageRole.SERVER
424
436
  },
425
437
  origin
426
438
  })
@@ -705,11 +717,16 @@ describe('requestIframeClient and requestIframeServer', () => {
705
717
  const server = requestIframeServer();
706
718
 
707
719
  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
- });
720
+ try {
721
+ const fileContent = 'Hello World';
722
+ await res.sendFile(fileContent, {
723
+ mimeType: 'text/plain',
724
+ fileName: 'test.txt'
725
+ });
726
+ } catch (error) {
727
+ console.error('Error in sendFile:', error);
728
+ throw error;
729
+ }
713
730
  });
714
731
 
715
732
  // Simulate request from iframe
@@ -727,22 +744,43 @@ describe('requestIframeClient and requestIframeServer', () => {
727
744
  source: mockContentWindow as any
728
745
  })
729
746
  );
730
- await new Promise((resolve) => setTimeout(resolve, 100));
747
+ // Wait for async handler to complete
748
+ await new Promise((resolve) => setTimeout(resolve, 1000));
731
749
 
732
- // Verify sendFile was called
750
+ // Verify sendFile was called - now it uses stream
733
751
  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
752
 
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');
753
+ // Debug: Check all message types sent
754
+ const allCalls = mockContentWindow.postMessage.mock.calls;
755
+ const messageTypes = allCalls.map(call => call[0]?.type).filter(Boolean);
756
+ if (messageTypes.length === 0) {
757
+ throw new Error('No messages were sent to mockContentWindow.postMessage');
758
+ }
759
+
760
+ const streamStartCall = allCalls.find(
761
+ (call: any[]) => call[0]?.type === 'stream_start'
762
+ );
763
+ if (!streamStartCall) {
764
+ throw new Error(`stream_start not found. Message types sent: ${messageTypes.join(', ')}`);
745
765
  }
766
+ expect(streamStartCall).toBeDefined();
767
+ const streamBody = streamStartCall![0].body;
768
+ expect(streamBody.type).toBe('file');
769
+ expect(streamBody.autoResolve).toBe(true);
770
+ expect(streamBody.metadata?.mimeType).toBe('text/plain');
771
+ expect(streamBody.metadata?.filename).toBe('test.txt');
772
+
773
+ // Verify stream_data was sent
774
+ const streamDataCall = mockContentWindow.postMessage.mock.calls.find(
775
+ (call: any[]) => call[0]?.type === 'stream_data'
776
+ );
777
+ expect(streamDataCall).toBeDefined();
778
+
779
+ // Verify stream_end was sent
780
+ const streamEndCall = mockContentWindow.postMessage.mock.calls.find(
781
+ (call: any[]) => call[0]?.type === 'stream_end'
782
+ );
783
+ expect(streamEndCall).toBeDefined();
746
784
 
747
785
  server.destroy();
748
786
  cleanupIframe(iframe);
@@ -784,13 +822,17 @@ describe('requestIframeClient and requestIframeServer', () => {
784
822
  source: mockContentWindow as any
785
823
  })
786
824
  );
787
- await new Promise((resolve) => setTimeout(resolve, 100));
825
+ await new Promise((resolve) => setTimeout(resolve, 200));
788
826
 
789
- const fileCall = mockContentWindow.postMessage.mock.calls.find(
790
- (call: any[]) => call[0]?.type === 'response' && call[0]?.fileData
827
+ // Verify stream_start was sent
828
+ const streamStartCall = mockContentWindow.postMessage.mock.calls.find(
829
+ (call: any[]) => call[0]?.type === 'stream_start'
791
830
  );
792
- expect(fileCall).toBeDefined();
793
- expect(fileCall![0].fileData.mimeType).toBe('text/plain');
831
+ expect(streamStartCall).toBeDefined();
832
+ const streamBody = streamStartCall![0].body;
833
+ expect(streamBody.type).toBe('file');
834
+ expect(streamBody.autoResolve).toBe(true);
835
+ expect(streamBody.metadata?.mimeType).toBe('text/plain');
794
836
 
795
837
  server.destroy();
796
838
  cleanupIframe(iframe);
@@ -829,13 +871,17 @@ describe('requestIframeClient and requestIframeServer', () => {
829
871
  source: mockContentWindow as any
830
872
  })
831
873
  );
832
- await new Promise((resolve) => setTimeout(resolve, 100));
874
+ await new Promise((resolve) => setTimeout(resolve, 200));
833
875
 
834
- const fileCall = mockContentWindow.postMessage.mock.calls.find(
835
- (call: any[]) => call[0]?.type === 'response' && call[0]?.fileData
876
+ // Verify stream_start was sent
877
+ const streamStartCall = mockContentWindow.postMessage.mock.calls.find(
878
+ (call: any[]) => call[0]?.type === 'stream_start'
836
879
  );
837
- expect(fileCall).toBeDefined();
838
- expect(fileCall[0].fileData.fileName).toBe('test.txt');
880
+ expect(streamStartCall).toBeDefined();
881
+ const streamBody = streamStartCall![0].body;
882
+ expect(streamBody.type).toBe('file');
883
+ expect(streamBody.autoResolve).toBe(true);
884
+ expect(streamBody.metadata?.filename).toBe('test.txt');
839
885
 
840
886
  server.destroy();
841
887
  cleanupIframe(iframe);
@@ -845,52 +891,14 @@ describe('requestIframeClient and requestIframeServer', () => {
845
891
  const origin = 'https://example.com';
846
892
  const iframe = createTestIframe(origin);
847
893
 
848
- let responseMessage: any = null;
849
894
  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
- })
895
+ postMessage: jest.fn()
887
896
  };
888
897
  Object.defineProperty(iframe, 'contentWindow', {
889
898
  value: mockContentWindow,
890
899
  writable: true
891
900
  });
892
901
 
893
- const client = requestIframeClient(iframe);
894
902
  const server = requestIframeServer();
895
903
 
896
904
  server.on('getFileAck', async (req, res) => {
@@ -916,19 +924,150 @@ describe('requestIframeClient and requestIframeServer', () => {
916
924
  );
917
925
  await new Promise((resolve) => setTimeout(resolve, 150));
918
926
 
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'
927
+ // Verify stream_start was sent with requireAck
928
+ const streamStartCall = mockContentWindow.postMessage.mock.calls.find(
929
+ (call: any[]) => call[0]?.type === 'stream_start'
922
930
  );
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
- }
931
+ expect(streamStartCall).toBeDefined();
932
+ const streamBody = streamStartCall![0].body;
933
+ expect(streamBody.type).toBe('file');
934
+ expect(streamBody.autoResolve).toBe(true);
928
935
 
929
936
  server.destroy();
930
937
  cleanupIframe(iframe);
931
938
  });
939
+
940
+ it('should auto-resolve file stream to fileData on client side', async () => {
941
+ const origin = 'https://example.com';
942
+ const iframe = createTestIframe(origin);
943
+
944
+ const mockContentWindow = {
945
+ postMessage: jest.fn((msg: PostMessageData) => {
946
+ if (msg.type === 'request') {
947
+ // Send ACK first
948
+ window.dispatchEvent(
949
+ new MessageEvent('message', {
950
+ data: {
951
+ __requestIframe__: 1,
952
+ type: 'ack',
953
+ requestId: msg.requestId,
954
+ path: msg.path,
955
+ role: MessageRole.SERVER
956
+ },
957
+ origin
958
+ })
959
+ );
960
+ // Then send stream_start
961
+ setTimeout(() => {
962
+ const streamId = 'stream-test';
963
+ const fileContent = btoa('Hello World');
964
+
965
+ // Send stream_start
966
+ window.dispatchEvent(
967
+ new MessageEvent('message', {
968
+ data: {
969
+ __requestIframe__: 1,
970
+ timestamp: Date.now(),
971
+ type: 'stream_start',
972
+ requestId: msg.requestId,
973
+ status: 200,
974
+ statusText: 'OK',
975
+ headers: {
976
+ 'Content-Type': 'text/plain',
977
+ 'Content-Disposition': 'attachment; filename="test.txt"'
978
+ },
979
+ body: {
980
+ streamId,
981
+ type: 'file',
982
+ chunked: false,
983
+ autoResolve: true,
984
+ metadata: {
985
+ filename: 'test.txt',
986
+ mimeType: 'text/plain'
987
+ }
988
+ },
989
+ role: MessageRole.SERVER
990
+ },
991
+ origin
992
+ })
993
+ );
994
+
995
+ // Send stream_data
996
+ setTimeout(() => {
997
+ window.dispatchEvent(
998
+ new MessageEvent('message', {
999
+ data: {
1000
+ __requestIframe__: 1,
1001
+ timestamp: Date.now(),
1002
+ type: 'stream_data',
1003
+ requestId: msg.requestId,
1004
+ body: {
1005
+ streamId,
1006
+ data: fileContent,
1007
+ done: true
1008
+ },
1009
+ role: MessageRole.SERVER
1010
+ },
1011
+ origin
1012
+ })
1013
+ );
1014
+
1015
+ // Send stream_end
1016
+ setTimeout(() => {
1017
+ window.dispatchEvent(
1018
+ new MessageEvent('message', {
1019
+ data: {
1020
+ __requestIframe__: 1,
1021
+ timestamp: Date.now(),
1022
+ type: 'stream_end',
1023
+ requestId: msg.requestId,
1024
+ body: {
1025
+ streamId
1026
+ },
1027
+ role: MessageRole.SERVER
1028
+ },
1029
+ origin
1030
+ })
1031
+ );
1032
+ }, 100);
1033
+ }, 100);
1034
+ }, 100);
1035
+ }
1036
+ })
1037
+ };
1038
+ Object.defineProperty(iframe, 'contentWindow', {
1039
+ value: mockContentWindow,
1040
+ writable: true
1041
+ });
1042
+
1043
+ const client = requestIframeClient(iframe);
1044
+
1045
+ const response = await client.send('getFile', undefined, {
1046
+ ackTimeout: 1000,
1047
+ timeout: 10000
1048
+ }) as any;
1049
+
1050
+ // Verify that data is a File object (auto-resolved from stream)
1051
+ expect(response.data).toBeInstanceOf(File);
1052
+ const file = response.data as File;
1053
+ expect(file.name).toBe('test.txt');
1054
+ expect(file.type).toBe('text/plain');
1055
+
1056
+ // Verify file content using FileReader or arrayBuffer
1057
+ const fileContent = await new Promise<string>((resolve) => {
1058
+ const reader = new FileReader();
1059
+ reader.onload = () => {
1060
+ resolve(reader.result as string);
1061
+ };
1062
+ reader.readAsText(file);
1063
+ });
1064
+ expect(fileContent).toBe('Hello World');
1065
+
1066
+ // Verify that stream is not present (because it was auto-resolved)
1067
+ expect(response.stream).toBeUndefined();
1068
+
1069
+ cleanupIframe(iframe);
1070
+ }, 20000);
932
1071
  });
933
1072
 
934
1073
  describe('server.map', () => {
@@ -1219,11 +1358,19 @@ describe('requestIframeClient and requestIframeServer', () => {
1219
1358
  );
1220
1359
  await new Promise((resolve) => setTimeout(resolve, 50));
1221
1360
 
1361
+ // Wait for response to be sent
1362
+ await new Promise((resolve) => setTimeout(resolve, 100));
1363
+
1222
1364
  // Simulate client receiving response
1223
1365
  const responseCall = mockContentWindow.postMessage.mock.calls.find(
1224
1366
  (call: any[]) => call[0]?.type === 'response'
1225
1367
  );
1226
- if (responseCall) {
1368
+ expect(responseCall).toBeDefined();
1369
+ if (responseCall && responseCall[0]) {
1370
+ // Verify response contains Set-Cookie header
1371
+ expect(responseCall[0].headers).toBeDefined();
1372
+ expect(responseCall[0].headers[HttpHeader.SET_COOKIE]).toBeDefined();
1373
+
1227
1374
  window.dispatchEvent(
1228
1375
  new MessageEvent('message', {
1229
1376
  data: responseCall[0],
@@ -1232,6 +1379,9 @@ describe('requestIframeClient and requestIframeServer', () => {
1232
1379
  })
1233
1380
  );
1234
1381
  }
1382
+
1383
+ // Wait for response to be processed
1384
+ await responsePromise;
1235
1385
  await new Promise((resolve) => setTimeout(resolve, 50));
1236
1386
 
1237
1387
  // Verify client automatically saved server-set cookies
@@ -1258,7 +1408,8 @@ describe('requestIframeClient and requestIframeServer', () => {
1258
1408
  __requestIframe__: 1,
1259
1409
  type: 'ack',
1260
1410
  requestId: msg.requestId,
1261
- path: msg.path
1411
+ path: msg.path,
1412
+ role: MessageRole.SERVER
1262
1413
  },
1263
1414
  origin
1264
1415
  })
@@ -1271,7 +1422,8 @@ describe('requestIframeClient and requestIframeServer', () => {
1271
1422
  requestId: msg.requestId,
1272
1423
  data: { result: 'success' },
1273
1424
  status: 200,
1274
- requireAck: true
1425
+ requireAck: true,
1426
+ role: MessageRole.SERVER
1275
1427
  };
1276
1428
  responseMessage = response;
1277
1429
  window.dispatchEvent(
@@ -1334,7 +1486,8 @@ describe('requestIframeClient and requestIframeServer', () => {
1334
1486
  __requestIframe__: 1,
1335
1487
  type: 'ack',
1336
1488
  requestId: msg.requestId,
1337
- path: msg.path
1489
+ path: msg.path,
1490
+ role: MessageRole.SERVER
1338
1491
  },
1339
1492
  origin
1340
1493
  })
@@ -1348,7 +1501,8 @@ describe('requestIframeClient and requestIframeServer', () => {
1348
1501
  requestId: msg.requestId,
1349
1502
  data: { json: true },
1350
1503
  status: 200,
1351
- requireAck: true
1504
+ requireAck: true,
1505
+ role: MessageRole.SERVER
1352
1506
  },
1353
1507
  origin
1354
1508
  })
@@ -1409,7 +1563,8 @@ describe('requestIframeClient and requestIframeServer', () => {
1409
1563
  __requestIframe__: 1,
1410
1564
  type: 'ack',
1411
1565
  requestId: msg.requestId,
1412
- path: msg.path
1566
+ path: msg.path,
1567
+ role: MessageRole.SERVER
1413
1568
  },
1414
1569
  origin
1415
1570
  })
@@ -1423,7 +1578,8 @@ describe('requestIframeClient and requestIframeServer', () => {
1423
1578
  requestId: msg.requestId,
1424
1579
  data: { error: 'Not Found' },
1425
1580
  status: 404,
1426
- statusText: 'Not Found'
1581
+ statusText: 'Not Found',
1582
+ role: MessageRole.SERVER
1427
1583
  },
1428
1584
  origin
1429
1585
  })
@@ -1991,7 +2147,8 @@ describe('requestIframeClient and requestIframeServer', () => {
1991
2147
  __requestIframe__: 1,
1992
2148
  type: 'ack',
1993
2149
  requestId: msg.requestId,
1994
- path: msg.path
2150
+ path: msg.path,
2151
+ role: MessageRole.SERVER
1995
2152
  },
1996
2153
  origin
1997
2154
  })
@@ -2007,7 +2164,8 @@ describe('requestIframeClient and requestIframeServer', () => {
2007
2164
  streamId: 'stream-123',
2008
2165
  type: 'data',
2009
2166
  chunked: true
2010
- }
2167
+ },
2168
+ role: MessageRole.SERVER
2011
2169
  },
2012
2170
  origin
2013
2171
  })
@@ -2073,7 +2231,8 @@ describe('requestIframeClient and requestIframeServer', () => {
2073
2231
  __requestIframe__: 1,
2074
2232
  type: 'ack',
2075
2233
  requestId: msg.requestId,
2076
- path: msg.path
2234
+ path: msg.path,
2235
+ role: MessageRole.SERVER
2077
2236
  },
2078
2237
  origin
2079
2238
  })
@@ -2089,7 +2248,8 @@ describe('requestIframeClient and requestIframeServer', () => {
2089
2248
  streamId: 'stream-123',
2090
2249
  type: 'data',
2091
2250
  chunked: true
2092
- }
2251
+ },
2252
+ role: MessageRole.SERVER
2093
2253
  },
2094
2254
  origin
2095
2255
  })
@@ -2131,7 +2291,7 @@ describe('requestIframeClient and requestIframeServer', () => {
2131
2291
  source: mockContentWindow as any
2132
2292
  })
2133
2293
  );
2134
- await new Promise((resolve) => setTimeout(resolve, 150));
2294
+ await new Promise((resolve) => setTimeout(resolve, 200));
2135
2295
 
2136
2296
  const streamStartCall = mockContentWindow.postMessage.mock.calls.find(
2137
2297
  (call: any[]) => call[0]?.type === 'stream_start'
@@ -2168,6 +2328,76 @@ describe('requestIframeClient and requestIframeServer', () => {
2168
2328
  cleanupIframe(iframe);
2169
2329
  });
2170
2330
 
2331
+ it('should handle client open/close/destroy methods', () => {
2332
+ const origin = 'https://example.com';
2333
+ const iframe = createTestIframe(origin);
2334
+
2335
+ const mockContentWindow = {
2336
+ postMessage: jest.fn()
2337
+ };
2338
+ Object.defineProperty(iframe, 'contentWindow', {
2339
+ value: mockContentWindow,
2340
+ writable: true
2341
+ });
2342
+
2343
+ const client = requestIframeClient(iframe);
2344
+
2345
+ expect(client.isOpen).toBe(true);
2346
+
2347
+ client.close();
2348
+ expect(client.isOpen).toBe(false);
2349
+
2350
+ client.open();
2351
+ expect(client.isOpen).toBe(true);
2352
+
2353
+ // Test destroy
2354
+ client.setCookie('test', 'value');
2355
+ expect(client.getCookie('test')).toBe('value');
2356
+
2357
+ client.destroy();
2358
+ expect(client.isOpen).toBe(false);
2359
+ // Cookies should be cleared after destroy
2360
+ expect(client.getCookie('test')).toBeUndefined();
2361
+
2362
+ cleanupIframe(iframe);
2363
+ });
2364
+
2365
+ it('should clear interceptors on destroy', () => {
2366
+ const origin = 'https://example.com';
2367
+ const iframe = createTestIframe(origin);
2368
+
2369
+ const mockContentWindow = {
2370
+ postMessage: jest.fn()
2371
+ };
2372
+ Object.defineProperty(iframe, 'contentWindow', {
2373
+ value: mockContentWindow,
2374
+ writable: true
2375
+ });
2376
+
2377
+ const client = requestIframeClient(iframe);
2378
+
2379
+ // Add interceptors
2380
+ const requestInterceptor = jest.fn((config) => config);
2381
+ const responseInterceptor = jest.fn((response) => response);
2382
+
2383
+ client.interceptors.request.use(requestInterceptor);
2384
+ client.interceptors.response.use(responseInterceptor);
2385
+
2386
+ // Destroy should clear interceptors
2387
+ client.destroy();
2388
+
2389
+ // Interceptors should be cleared (handlers array should be empty)
2390
+ let interceptorCount = 0;
2391
+ client.interceptors.request.forEach(() => { interceptorCount++; });
2392
+ expect(interceptorCount).toBe(0);
2393
+
2394
+ interceptorCount = 0;
2395
+ client.interceptors.response.forEach(() => { interceptorCount++; });
2396
+ expect(interceptorCount).toBe(0);
2397
+
2398
+ cleanupIframe(iframe);
2399
+ });
2400
+
2171
2401
  it('should handle server off method', async () => {
2172
2402
  const origin = 'https://example.com';
2173
2403
  const iframe = createTestIframe(origin);
@@ -2212,5 +2442,1993 @@ describe('requestIframeClient and requestIframeServer', () => {
2212
2442
  server.destroy();
2213
2443
  cleanupIframe(iframe);
2214
2444
  });
2445
+
2446
+ it('should return unregister function from on method', () => {
2447
+ const origin = 'https://example.com';
2448
+ const iframe = createTestIframe(origin);
2449
+
2450
+ const mockContentWindow = {
2451
+ postMessage: jest.fn()
2452
+ };
2453
+ Object.defineProperty(iframe, 'contentWindow', {
2454
+ value: mockContentWindow,
2455
+ writable: true
2456
+ });
2457
+
2458
+ const server = requestIframeServer();
2459
+
2460
+ // on method should return an unregister function
2461
+ const unregister = server.on('test', (req, res) => {
2462
+ res.send({});
2463
+ });
2464
+
2465
+ expect(typeof unregister).toBe('function');
2466
+
2467
+ // Use the returned function to unregister
2468
+ unregister();
2469
+
2470
+ // Verify handler is removed
2471
+ const requestId = 'req-unregister';
2472
+ window.dispatchEvent(
2473
+ new MessageEvent('message', {
2474
+ data: {
2475
+ __requestIframe__: 1,
2476
+ type: 'request',
2477
+ requestId: requestId,
2478
+ path: 'test',
2479
+ body: {}
2480
+ },
2481
+ origin,
2482
+ source: mockContentWindow as any
2483
+ })
2484
+ );
2485
+
2486
+ // Should not find handler (will send error)
2487
+ const errorCall = mockContentWindow.postMessage.mock.calls.find(
2488
+ (call: any[]) => call[0]?.type === 'error' && call[0]?.error?.code === 'METHOD_NOT_FOUND'
2489
+ );
2490
+ expect(errorCall).toBeDefined();
2491
+
2492
+ server.destroy();
2493
+ cleanupIframe(iframe);
2494
+ });
2495
+
2496
+ it('should support batch unregister with off method', async () => {
2497
+ const origin = 'https://example.com';
2498
+ const iframe = createTestIframe(origin);
2499
+
2500
+ const mockContentWindow = {
2501
+ postMessage: jest.fn()
2502
+ };
2503
+ Object.defineProperty(iframe, 'contentWindow', {
2504
+ value: mockContentWindow,
2505
+ writable: true
2506
+ });
2507
+
2508
+ const server = requestIframeServer();
2509
+
2510
+ // Register multiple handlers
2511
+ server.on('path1', (req, res) => res.send({ path: '1' }));
2512
+ server.on('path2', (req, res) => res.send({ path: '2' }));
2513
+ server.on('path3', (req, res) => res.send({ path: '3' }));
2514
+
2515
+ // Batch unregister
2516
+ server.off(['path1', 'path2']);
2517
+
2518
+ // path1 and path2 should be removed
2519
+ const requestId1 = 'req-1';
2520
+ window.dispatchEvent(
2521
+ new MessageEvent('message', {
2522
+ data: {
2523
+ __requestIframe__: 1,
2524
+ type: 'request',
2525
+ requestId: requestId1,
2526
+ path: 'path1',
2527
+ body: {}
2528
+ },
2529
+ origin,
2530
+ source: mockContentWindow as any
2531
+ })
2532
+ );
2533
+ await new Promise((resolve) => setTimeout(resolve, 50));
2534
+
2535
+ const errorCall1 = mockContentWindow.postMessage.mock.calls.find(
2536
+ (call: any[]) => call[0]?.type === 'error' && call[0]?.error?.code === 'METHOD_NOT_FOUND'
2537
+ );
2538
+ expect(errorCall1).toBeDefined();
2539
+
2540
+ // path3 should still work
2541
+ const requestId3 = 'req-3';
2542
+ window.dispatchEvent(
2543
+ new MessageEvent('message', {
2544
+ data: {
2545
+ __requestIframe__: 1,
2546
+ type: 'request',
2547
+ requestId: requestId3,
2548
+ path: 'path3',
2549
+ body: {}
2550
+ },
2551
+ origin,
2552
+ source: mockContentWindow as any
2553
+ })
2554
+ );
2555
+ await new Promise((resolve) => setTimeout(resolve, 50));
2556
+
2557
+ const successCall = mockContentWindow.postMessage.mock.calls.find(
2558
+ (call: any[]) => call[0]?.type === 'response' && call[0]?.data?.path === '3'
2559
+ );
2560
+ expect(successCall).toBeDefined();
2561
+
2562
+ server.destroy();
2563
+ cleanupIframe(iframe);
2564
+ });
2565
+ });
2566
+
2567
+ describe('Client additional features', () => {
2568
+ it('should support postMessage method for stream handler', () => {
2569
+ const origin = 'https://example.com';
2570
+ const iframe = createTestIframe(origin);
2571
+ const mockContentWindow = {
2572
+ postMessage: jest.fn()
2573
+ };
2574
+ Object.defineProperty(iframe, 'contentWindow', {
2575
+ value: mockContentWindow,
2576
+ writable: true
2577
+ });
2578
+
2579
+ const client = requestIframeClient(iframe);
2580
+ const message = { type: 'test', data: 'value' };
2581
+
2582
+ // Access postMessage through stream handler interface
2583
+ (client as any).postMessage(message);
2584
+
2585
+ // Verify message was sent via dispatcher
2586
+ expect(mockContentWindow.postMessage).toHaveBeenCalled();
2587
+
2588
+ cleanupIframe(iframe);
2589
+ });
2590
+
2591
+ it('should handle function-type headers', async () => {
2592
+ const origin = 'https://example.com';
2593
+ const iframe = createTestIframe(origin);
2594
+ const mockContentWindow = {
2595
+ postMessage: jest.fn((msg: PostMessageData) => {
2596
+ if (msg.type === 'request') {
2597
+ window.dispatchEvent(
2598
+ new MessageEvent('message', {
2599
+ data: {
2600
+ __requestIframe__: 1,
2601
+ type: 'ack',
2602
+ requestId: msg.requestId,
2603
+ path: msg.path,
2604
+ role: MessageRole.SERVER
2605
+ },
2606
+ origin
2607
+ })
2608
+ );
2609
+ setTimeout(() => {
2610
+ window.dispatchEvent(
2611
+ new MessageEvent('message', {
2612
+ data: {
2613
+ __requestIframe__: 1,
2614
+ type: 'response',
2615
+ requestId: msg.requestId,
2616
+ data: { result: 'success' },
2617
+ status: 200,
2618
+ statusText: 'OK',
2619
+ role: MessageRole.SERVER
2620
+ },
2621
+ origin
2622
+ })
2623
+ );
2624
+ }, 10);
2625
+ }
2626
+ })
2627
+ };
2628
+ Object.defineProperty(iframe, 'contentWindow', {
2629
+ value: mockContentWindow,
2630
+ writable: true
2631
+ });
2632
+
2633
+ const client = requestIframeClient(iframe, {
2634
+ headers: {
2635
+ 'X-Dynamic': (config: RequestConfig) => `value-${config.path}`
2636
+ }
2637
+ });
2638
+
2639
+ await client.send('test', {});
2640
+
2641
+ const requestCall = mockContentWindow.postMessage.mock.calls.find(
2642
+ (call: any[]) => call[0]?.type === 'request'
2643
+ );
2644
+ expect(requestCall).toBeDefined();
2645
+ if (requestCall && requestCall[0]) {
2646
+ expect(requestCall[0].headers?.['X-Dynamic']).toBe('value-test');
2647
+ }
2648
+
2649
+ cleanupIframe(iframe);
2650
+ });
2651
+
2652
+ it('should handle isConnect timeout', async () => {
2653
+ const origin = 'https://example.com';
2654
+ const iframe = createTestIframe(origin);
2655
+ const mockContentWindow = {
2656
+ postMessage: jest.fn()
2657
+ };
2658
+ Object.defineProperty(iframe, 'contentWindow', {
2659
+ value: mockContentWindow,
2660
+ writable: true
2661
+ });
2662
+
2663
+ const client = requestIframeClient(iframe, { ackTimeout: 50 });
2664
+
2665
+ // Server doesn't respond, should timeout
2666
+ const connected = await client.isConnect();
2667
+ expect(connected).toBe(false);
2668
+
2669
+ cleanupIframe(iframe);
2670
+ });
2671
+
2672
+ it('should handle isConnect rejection', async () => {
2673
+ const origin = 'https://example.com';
2674
+ const iframe = createTestIframe(origin);
2675
+ const mockContentWindow = {
2676
+ postMessage: jest.fn((msg: PostMessageData) => {
2677
+ if (msg.type === 'ping') {
2678
+ // Simulate error by not sending pong
2679
+ setTimeout(() => {
2680
+ window.dispatchEvent(
2681
+ new MessageEvent('message', {
2682
+ data: {
2683
+ __requestIframe__: 1,
2684
+ type: 'error',
2685
+ requestId: msg.requestId,
2686
+ error: { message: 'Connection failed' },
2687
+ role: MessageRole.SERVER
2688
+ },
2689
+ origin
2690
+ })
2691
+ );
2692
+ }, 10);
2693
+ }
2694
+ })
2695
+ };
2696
+ Object.defineProperty(iframe, 'contentWindow', {
2697
+ value: mockContentWindow,
2698
+ writable: true
2699
+ });
2700
+
2701
+ const client = requestIframeClient(iframe);
2702
+
2703
+ const connected = await client.isConnect();
2704
+ expect(connected).toBe(false);
2705
+
2706
+ cleanupIframe(iframe);
2707
+ });
2708
+
2709
+ it('should remember targetServerId from ACK and use it in subsequent requests', async () => {
2710
+ const origin = 'https://example.com';
2711
+ const iframe = createTestIframe(origin);
2712
+ const serverId = 'server-123';
2713
+
2714
+ const mockContentWindow = {
2715
+ postMessage: jest.fn((msg: PostMessageData) => {
2716
+ if (msg.type === 'request') {
2717
+ window.dispatchEvent(
2718
+ new MessageEvent('message', {
2719
+ data: {
2720
+ __requestIframe__: 1,
2721
+ type: 'ack',
2722
+ requestId: msg.requestId,
2723
+ path: msg.path,
2724
+ role: MessageRole.SERVER,
2725
+ creatorId: serverId
2726
+ },
2727
+ origin
2728
+ })
2729
+ );
2730
+ setTimeout(() => {
2731
+ window.dispatchEvent(
2732
+ new MessageEvent('message', {
2733
+ data: {
2734
+ __requestIframe__: 1,
2735
+ type: 'response',
2736
+ requestId: msg.requestId,
2737
+ data: { result: 'success' },
2738
+ status: 200,
2739
+ statusText: 'OK',
2740
+ role: MessageRole.SERVER,
2741
+ creatorId: serverId
2742
+ },
2743
+ origin
2744
+ })
2745
+ );
2746
+ }, 10);
2747
+ }
2748
+ })
2749
+ };
2750
+ Object.defineProperty(iframe, 'contentWindow', {
2751
+ value: mockContentWindow,
2752
+ writable: true
2753
+ });
2754
+
2755
+ const client = requestIframeClient(iframe);
2756
+
2757
+ // First request - should remember serverId
2758
+ await client.send('test1', {});
2759
+
2760
+ // Second request - should use remembered serverId
2761
+ await client.send('test2', {});
2762
+
2763
+ const requestCalls = mockContentWindow.postMessage.mock.calls.filter(
2764
+ (call: any[]) => call[0]?.type === 'request'
2765
+ );
2766
+
2767
+ // First request may not have targetId (if serverId not remembered yet)
2768
+ // Second request should have targetId
2769
+ expect(requestCalls.length).toBeGreaterThanOrEqual(2);
2770
+ const secondRequest = requestCalls[requestCalls.length - 1];
2771
+ if (secondRequest) {
2772
+ expect(secondRequest[0].targetId).toBe(serverId);
2773
+ }
2774
+
2775
+ cleanupIframe(iframe);
2776
+ });
2777
+
2778
+ it('should not override existing targetServerId', async () => {
2779
+ const origin = 'https://example.com';
2780
+ const iframe = createTestIframe(origin);
2781
+ const existingServerId = 'existing-server';
2782
+ const newServerId = 'new-server';
2783
+
2784
+ const mockContentWindow = {
2785
+ postMessage: jest.fn((msg: PostMessageData) => {
2786
+ if (msg.type === 'request') {
2787
+ window.dispatchEvent(
2788
+ new MessageEvent('message', {
2789
+ data: {
2790
+ __requestIframe__: 1,
2791
+ type: 'ack',
2792
+ requestId: msg.requestId,
2793
+ path: msg.path,
2794
+ role: MessageRole.SERVER,
2795
+ creatorId: newServerId
2796
+ },
2797
+ origin
2798
+ })
2799
+ );
2800
+ setTimeout(() => {
2801
+ window.dispatchEvent(
2802
+ new MessageEvent('message', {
2803
+ data: {
2804
+ __requestIframe__: 1,
2805
+ type: 'response',
2806
+ requestId: msg.requestId,
2807
+ data: { result: 'success' },
2808
+ status: 200,
2809
+ statusText: 'OK',
2810
+ role: MessageRole.SERVER,
2811
+ creatorId: newServerId
2812
+ },
2813
+ origin
2814
+ })
2815
+ );
2816
+ }, 10);
2817
+ }
2818
+ })
2819
+ };
2820
+ Object.defineProperty(iframe, 'contentWindow', {
2821
+ value: mockContentWindow,
2822
+ writable: true
2823
+ });
2824
+
2825
+ const client = requestIframeClient(iframe);
2826
+
2827
+ // Set existing targetServerId
2828
+ (client as any)._targetServerId = existingServerId;
2829
+
2830
+ // Send request with explicit targetId
2831
+ await client.send('test', {}, { targetId: existingServerId });
2832
+
2833
+ const requestCall = mockContentWindow.postMessage.mock.calls.find(
2834
+ (call: any[]) => call[0]?.type === 'request'
2835
+ );
2836
+ expect(requestCall).toBeDefined();
2837
+ if (requestCall) {
2838
+ expect(requestCall[0].targetId).toBe(existingServerId);
2839
+ }
2840
+
2841
+ // targetServerId should not be overridden
2842
+ expect((client as any)._targetServerId).toBe(existingServerId);
2843
+
2844
+ cleanupIframe(iframe);
2845
+ });
2846
+
2847
+ it('should handle setCookie with expires option', () => {
2848
+ const origin = 'https://example.com';
2849
+ const iframe = createTestIframe(origin);
2850
+ const mockContentWindow = {
2851
+ postMessage: jest.fn()
2852
+ };
2853
+ Object.defineProperty(iframe, 'contentWindow', {
2854
+ value: mockContentWindow,
2855
+ writable: true
2856
+ });
2857
+
2858
+ const client = requestIframeClient(iframe);
2859
+ const expires = new Date(Date.now() + 3600000); // 1 hour from now
2860
+
2861
+ client.setCookie('token', 'value', { expires });
2862
+
2863
+ expect(client.getCookie('token')).toBe('value');
2864
+
2865
+ cleanupIframe(iframe);
2866
+ });
2867
+
2868
+ it('should handle setCookie with maxAge option', () => {
2869
+ const origin = 'https://example.com';
2870
+ const iframe = createTestIframe(origin);
2871
+ const mockContentWindow = {
2872
+ postMessage: jest.fn()
2873
+ };
2874
+ Object.defineProperty(iframe, 'contentWindow', {
2875
+ value: mockContentWindow,
2876
+ writable: true
2877
+ });
2878
+
2879
+ const client = requestIframeClient(iframe);
2880
+
2881
+ client.setCookie('token', 'value', { maxAge: 3600 });
2882
+
2883
+ expect(client.getCookie('token')).toBe('value');
2884
+
2885
+ cleanupIframe(iframe);
2886
+ });
2887
+
2888
+ it('should handle getServer method', () => {
2889
+ const origin = 'https://example.com';
2890
+ const iframe = createTestIframe(origin);
2891
+ const mockContentWindow = {
2892
+ postMessage: jest.fn()
2893
+ };
2894
+ Object.defineProperty(iframe, 'contentWindow', {
2895
+ value: mockContentWindow,
2896
+ writable: true
2897
+ });
2898
+
2899
+ const client = requestIframeClient(iframe);
2900
+ const server = (client as any).getServer();
2901
+
2902
+ expect(server).toBeDefined();
2903
+ expect(server.isOpen).toBe(true);
2904
+
2905
+ cleanupIframe(iframe);
2906
+ });
2907
+
2908
+ it('should handle non-autoResolve file stream', async () => {
2909
+ const origin = 'https://example.com';
2910
+ const iframe = createTestIframe(origin);
2911
+ const mockContentWindow = {
2912
+ postMessage: jest.fn((msg: PostMessageData) => {
2913
+ if (msg.type === 'request') {
2914
+ window.dispatchEvent(
2915
+ new MessageEvent('message', {
2916
+ data: {
2917
+ __requestIframe__: 1,
2918
+ type: 'ack',
2919
+ requestId: msg.requestId,
2920
+ path: msg.path,
2921
+ role: MessageRole.SERVER
2922
+ },
2923
+ origin
2924
+ })
2925
+ );
2926
+ setTimeout(() => {
2927
+ const streamId = 'stream-test';
2928
+ window.dispatchEvent(
2929
+ new MessageEvent('message', {
2930
+ data: {
2931
+ __requestIframe__: 1,
2932
+ timestamp: Date.now(),
2933
+ type: 'stream_start',
2934
+ requestId: msg.requestId,
2935
+ status: 200,
2936
+ statusText: 'OK',
2937
+ body: {
2938
+ streamId,
2939
+ type: 'file',
2940
+ chunked: false,
2941
+ autoResolve: false, // Not auto-resolve
2942
+ metadata: {
2943
+ filename: 'test.txt',
2944
+ mimeType: 'text/plain'
2945
+ }
2946
+ },
2947
+ role: MessageRole.SERVER
2948
+ },
2949
+ origin
2950
+ })
2951
+ );
2952
+ setTimeout(() => {
2953
+ window.dispatchEvent(
2954
+ new MessageEvent('message', {
2955
+ data: {
2956
+ __requestIframe__: 1,
2957
+ timestamp: Date.now(),
2958
+ type: 'stream_data',
2959
+ requestId: msg.requestId,
2960
+ body: {
2961
+ streamId,
2962
+ data: btoa('Hello World'),
2963
+ done: true
2964
+ },
2965
+ role: MessageRole.SERVER
2966
+ },
2967
+ origin
2968
+ })
2969
+ );
2970
+ setTimeout(() => {
2971
+ window.dispatchEvent(
2972
+ new MessageEvent('message', {
2973
+ data: {
2974
+ __requestIframe__: 1,
2975
+ timestamp: Date.now(),
2976
+ type: 'stream_end',
2977
+ requestId: msg.requestId,
2978
+ body: { streamId },
2979
+ role: MessageRole.SERVER
2980
+ },
2981
+ origin
2982
+ })
2983
+ );
2984
+ }, 10);
2985
+ }, 10);
2986
+ }, 10);
2987
+ }
2988
+ })
2989
+ };
2990
+ Object.defineProperty(iframe, 'contentWindow', {
2991
+ value: mockContentWindow,
2992
+ writable: true
2993
+ });
2994
+
2995
+ const client = requestIframeClient(iframe);
2996
+ const response = await client.send('getFile', {}, {
2997
+ ackTimeout: 1000,
2998
+ timeout: 10000
2999
+ }) as any;
3000
+
3001
+ expect(response.stream).toBeDefined();
3002
+ expect(response.data).not.toBeInstanceOf(File); // Not auto-resolved, data is not a File
3003
+
3004
+ cleanupIframe(iframe);
3005
+ }, 20000);
3006
+
3007
+ it('should handle regular data stream (non-file)', async () => {
3008
+ const origin = 'https://example.com';
3009
+ const iframe = createTestIframe(origin);
3010
+ const mockContentWindow = {
3011
+ postMessage: jest.fn((msg: PostMessageData) => {
3012
+ if (msg.type === 'request') {
3013
+ window.dispatchEvent(
3014
+ new MessageEvent('message', {
3015
+ data: {
3016
+ __requestIframe__: 1,
3017
+ type: 'ack',
3018
+ requestId: msg.requestId,
3019
+ path: msg.path,
3020
+ role: MessageRole.SERVER
3021
+ },
3022
+ origin
3023
+ })
3024
+ );
3025
+ setTimeout(() => {
3026
+ const streamId = 'stream-test';
3027
+ window.dispatchEvent(
3028
+ new MessageEvent('message', {
3029
+ data: {
3030
+ __requestIframe__: 1,
3031
+ timestamp: Date.now(),
3032
+ type: 'stream_start',
3033
+ requestId: msg.requestId,
3034
+ status: 200,
3035
+ statusText: 'OK',
3036
+ body: {
3037
+ streamId,
3038
+ type: 'data',
3039
+ chunked: true
3040
+ },
3041
+ role: MessageRole.SERVER
3042
+ },
3043
+ origin
3044
+ })
3045
+ );
3046
+ setTimeout(() => {
3047
+ window.dispatchEvent(
3048
+ new MessageEvent('message', {
3049
+ data: {
3050
+ __requestIframe__: 1,
3051
+ timestamp: Date.now(),
3052
+ type: 'stream_data',
3053
+ requestId: msg.requestId,
3054
+ body: {
3055
+ streamId,
3056
+ data: btoa('chunk1'),
3057
+ done: false
3058
+ },
3059
+ role: MessageRole.SERVER
3060
+ },
3061
+ origin
3062
+ })
3063
+ );
3064
+ setTimeout(() => {
3065
+ window.dispatchEvent(
3066
+ new MessageEvent('message', {
3067
+ data: {
3068
+ __requestIframe__: 1,
3069
+ timestamp: Date.now(),
3070
+ type: 'stream_data',
3071
+ requestId: msg.requestId,
3072
+ body: {
3073
+ streamId,
3074
+ data: btoa('chunk2'),
3075
+ done: true
3076
+ },
3077
+ role: MessageRole.SERVER
3078
+ },
3079
+ origin
3080
+ })
3081
+ );
3082
+ setTimeout(() => {
3083
+ window.dispatchEvent(
3084
+ new MessageEvent('message', {
3085
+ data: {
3086
+ __requestIframe__: 1,
3087
+ timestamp: Date.now(),
3088
+ type: 'stream_end',
3089
+ requestId: msg.requestId,
3090
+ body: { streamId },
3091
+ role: MessageRole.SERVER
3092
+ },
3093
+ origin
3094
+ })
3095
+ );
3096
+ }, 10);
3097
+ }, 10);
3098
+ }, 10);
3099
+ }, 10);
3100
+ }
3101
+ })
3102
+ };
3103
+ Object.defineProperty(iframe, 'contentWindow', {
3104
+ value: mockContentWindow,
3105
+ writable: true
3106
+ });
3107
+
3108
+ const client = requestIframeClient(iframe);
3109
+ const response = await client.send('getStream', {}, {
3110
+ ackTimeout: 1000,
3111
+ timeout: 10000
3112
+ }) as any;
3113
+
3114
+ expect(response.stream).toBeDefined();
3115
+ expect(response.stream.type).toBe('data');
3116
+
3117
+ cleanupIframe(iframe);
3118
+ }, 20000);
3119
+
3120
+ it('should handle dispatchStreamMessage for stream messages', async () => {
3121
+ const origin = 'https://example.com';
3122
+ const iframe = createTestIframe(origin);
3123
+ const mockContentWindow = {
3124
+ postMessage: jest.fn()
3125
+ };
3126
+ Object.defineProperty(iframe, 'contentWindow', {
3127
+ value: mockContentWindow,
3128
+ writable: true
3129
+ });
3130
+
3131
+ const client = requestIframeClient(iframe);
3132
+ const streamId = 'test-stream';
3133
+ const handler = jest.fn();
3134
+
3135
+ // Register stream handler
3136
+ (client as any).registerStreamHandler(streamId, handler);
3137
+
3138
+ // Dispatch stream message
3139
+ window.dispatchEvent(
3140
+ new MessageEvent('message', {
3141
+ data: {
3142
+ __requestIframe__: 1,
3143
+ type: 'stream_data',
3144
+ requestId: 'req123',
3145
+ body: {
3146
+ streamId,
3147
+ data: 'test',
3148
+ type: 'data'
3149
+ },
3150
+ role: MessageRole.SERVER
3151
+ },
3152
+ origin
3153
+ })
3154
+ );
3155
+
3156
+ await new Promise(resolve => setTimeout(resolve, 50));
3157
+
3158
+ expect(handler).toHaveBeenCalled();
3159
+
3160
+ cleanupIframe(iframe);
3161
+ });
3162
+
3163
+ it('should handle error in response interceptor rejected callback', async () => {
3164
+ const origin = 'https://example.com';
3165
+ const iframe = createTestIframe(origin);
3166
+ const mockContentWindow = {
3167
+ postMessage: jest.fn((msg: PostMessageData) => {
3168
+ if (msg.type === 'request') {
3169
+ window.dispatchEvent(
3170
+ new MessageEvent('message', {
3171
+ data: {
3172
+ __requestIframe__: 1,
3173
+ type: 'ack',
3174
+ requestId: msg.requestId,
3175
+ path: msg.path,
3176
+ role: MessageRole.SERVER
3177
+ },
3178
+ origin
3179
+ })
3180
+ );
3181
+ setTimeout(() => {
3182
+ window.dispatchEvent(
3183
+ new MessageEvent('message', {
3184
+ data: {
3185
+ __requestIframe__: 1,
3186
+ type: 'error',
3187
+ requestId: msg.requestId,
3188
+ error: { message: 'Test error', code: 'TEST_ERROR' },
3189
+ status: 500,
3190
+ statusText: 'Internal Server Error',
3191
+ role: MessageRole.SERVER
3192
+ },
3193
+ origin
3194
+ })
3195
+ );
3196
+ }, 10);
3197
+ }
3198
+ })
3199
+ };
3200
+ Object.defineProperty(iframe, 'contentWindow', {
3201
+ value: mockContentWindow,
3202
+ writable: true
3203
+ });
3204
+
3205
+ const client = requestIframeClient(iframe);
3206
+
3207
+ // Add error interceptor that rejects
3208
+ client.interceptors.response.use(
3209
+ (response) => response,
3210
+ (error) => {
3211
+ // Reject to test the catch path
3212
+ return Promise.reject(error);
3213
+ }
3214
+ );
3215
+
3216
+ try {
3217
+ await client.send('test', {});
3218
+ fail('Should have thrown error');
3219
+ } catch (error: any) {
3220
+ expect(error.message).toBe('Test error');
3221
+ }
3222
+
3223
+ cleanupIframe(iframe);
3224
+ });
3225
+ });
3226
+
3227
+ describe('Server additional features', () => {
3228
+ it('should handle protocol version error', async () => {
3229
+ const origin = 'https://example.com';
3230
+ const iframe = createTestIframe(origin);
3231
+ const mockContentWindow = {
3232
+ postMessage: jest.fn()
3233
+ };
3234
+ Object.defineProperty(iframe, 'contentWindow', {
3235
+ value: mockContentWindow,
3236
+ writable: true
3237
+ });
3238
+
3239
+ const server = requestIframeServer();
3240
+
3241
+ // Send message with incompatible version
3242
+ window.dispatchEvent(
3243
+ new MessageEvent('message', {
3244
+ data: {
3245
+ __requestIframe__: 0, // Incompatible version
3246
+ timestamp: Date.now(),
3247
+ type: 'request',
3248
+ requestId: 'req123',
3249
+ path: 'test',
3250
+ role: MessageRole.CLIENT
3251
+ },
3252
+ origin,
3253
+ source: mockContentWindow as any
3254
+ })
3255
+ );
3256
+
3257
+ await new Promise(resolve => setTimeout(resolve, 50));
3258
+
3259
+ expect(mockContentWindow.postMessage).toHaveBeenCalledWith(
3260
+ expect.objectContaining({
3261
+ type: 'error',
3262
+ requestId: 'req123'
3263
+ }),
3264
+ origin
3265
+ );
3266
+
3267
+ server.destroy();
3268
+ cleanupIframe(iframe);
3269
+ });
3270
+
3271
+ it('should handle handler returning undefined result', async () => {
3272
+ const origin = 'https://example.com';
3273
+ const iframe = createTestIframe(origin);
3274
+ const mockContentWindow = {
3275
+ postMessage: jest.fn()
3276
+ };
3277
+ Object.defineProperty(iframe, 'contentWindow', {
3278
+ value: mockContentWindow,
3279
+ writable: true
3280
+ });
3281
+
3282
+ const server = requestIframeServer();
3283
+
3284
+ server.on('test', (req, res) => {
3285
+ // Handler doesn't return anything (undefined)
3286
+ // This should trigger NO_RESPONSE_SENT error
3287
+ });
3288
+
3289
+ window.dispatchEvent(
3290
+ new MessageEvent('message', {
3291
+ data: {
3292
+ __requestIframe__: 1,
3293
+ timestamp: Date.now(),
3294
+ type: 'request',
3295
+ requestId: 'req123',
3296
+ path: 'test',
3297
+ role: MessageRole.CLIENT,
3298
+ targetId: server.id
3299
+ },
3300
+ origin,
3301
+ source: mockContentWindow as any
3302
+ })
3303
+ );
3304
+
3305
+ await new Promise(resolve => setTimeout(resolve, 100));
3306
+
3307
+ const errorCall = mockContentWindow.postMessage.mock.calls.find(
3308
+ (call: any[]) => call[0]?.type === 'error' && call[0]?.requestId === 'req123'
3309
+ );
3310
+ expect(errorCall).toBeDefined();
3311
+ if (errorCall && errorCall[0]) {
3312
+ expect(errorCall[0]).toMatchObject({
3313
+ type: 'error',
3314
+ requestId: 'req123',
3315
+ error: expect.objectContaining({
3316
+ code: 'NO_RESPONSE'
3317
+ })
3318
+ });
3319
+ }
3320
+
3321
+ server.destroy();
3322
+ cleanupIframe(iframe);
3323
+ });
3324
+
3325
+ it('should skip processing when message already handled by another server', async () => {
3326
+ const origin = 'https://example.com';
3327
+ const iframe = createTestIframe(origin);
3328
+ const mockContentWindow = {
3329
+ postMessage: jest.fn()
3330
+ };
3331
+ Object.defineProperty(iframe, 'contentWindow', {
3332
+ value: mockContentWindow,
3333
+ writable: true
3334
+ });
3335
+
3336
+ const server1 = requestIframeServer();
3337
+ const server2 = requestIframeServer();
3338
+
3339
+ const handler1 = jest.fn((req, res) => res.send({ server: 1 }));
3340
+ const handler2 = jest.fn((req, res) => res.send({ server: 2 }));
3341
+
3342
+ server1.on('test', handler1);
3343
+ server2.on('test', handler2);
3344
+
3345
+ // Create a context that indicates message was already handled
3346
+ const messageData = {
3347
+ __requestIframe__: 1,
3348
+ timestamp: Date.now(),
3349
+ type: 'request' as const,
3350
+ requestId: 'req123',
3351
+ path: 'test',
3352
+ role: MessageRole.CLIENT,
3353
+ targetId: server1.id
3354
+ };
3355
+
3356
+ window.dispatchEvent(
3357
+ new MessageEvent('message', {
3358
+ data: messageData,
3359
+ origin,
3360
+ source: mockContentWindow as any
3361
+ })
3362
+ );
3363
+
3364
+ await new Promise(resolve => setTimeout(resolve, 100));
3365
+
3366
+ // Only server1 should handle it (because of targetId)
3367
+ expect(handler1).toHaveBeenCalled();
3368
+ expect(handler2).not.toHaveBeenCalled();
3369
+
3370
+ server1.destroy();
3371
+ server2.destroy();
3372
+ cleanupIframe(iframe);
3373
+ });
3374
+
3375
+ it('should handle ack timeout in registerPendingAck', async () => {
3376
+ const origin = 'https://example.com';
3377
+ const iframe = createTestIframe(origin);
3378
+ const mockContentWindow = {
3379
+ postMessage: jest.fn()
3380
+ };
3381
+ Object.defineProperty(iframe, 'contentWindow', {
3382
+ value: mockContentWindow,
3383
+ writable: true
3384
+ });
3385
+
3386
+ const server = requestIframeServer({ ackTimeout: 50 });
3387
+
3388
+ server.on('test', (req, res) => {
3389
+ // Send response with requireAck, but client never sends 'received'
3390
+ res.send({ result: 'success' }, { requireAck: true });
3391
+ });
3392
+
3393
+ window.dispatchEvent(
3394
+ new MessageEvent('message', {
3395
+ data: {
3396
+ __requestIframe__: 1,
3397
+ timestamp: Date.now(),
3398
+ type: 'request',
3399
+ requestId: 'req123',
3400
+ path: 'test',
3401
+ role: MessageRole.CLIENT,
3402
+ targetId: server.id
3403
+ },
3404
+ origin,
3405
+ source: mockContentWindow as any
3406
+ })
3407
+ );
3408
+
3409
+ // Wait for ack timeout
3410
+ await new Promise(resolve => setTimeout(resolve, 150));
3411
+
3412
+ // Server should have sent response
3413
+ expect(mockContentWindow.postMessage).toHaveBeenCalledWith(
3414
+ expect.objectContaining({
3415
+ type: 'response',
3416
+ requestId: 'req123'
3417
+ }),
3418
+ origin
3419
+ );
3420
+
3421
+ server.destroy();
3422
+ cleanupIframe(iframe);
3423
+ });
3424
+
3425
+ it('should handle middleware that sends response early', async () => {
3426
+ const origin = 'https://example.com';
3427
+ const iframe = createTestIframe(origin);
3428
+ const mockContentWindow = {
3429
+ postMessage: jest.fn()
3430
+ };
3431
+ Object.defineProperty(iframe, 'contentWindow', {
3432
+ value: mockContentWindow,
3433
+ writable: true
3434
+ });
3435
+
3436
+ const server = requestIframeServer();
3437
+
3438
+ const middleware = jest.fn((req, res, next) => {
3439
+ res.send({ middleware: true });
3440
+ // Don't call next() - response already sent
3441
+ });
3442
+
3443
+ const handler = jest.fn((req, res) => {
3444
+ res.send({ handler: true });
3445
+ });
3446
+
3447
+ server.use(middleware);
3448
+ server.on('test', handler);
3449
+
3450
+ window.dispatchEvent(
3451
+ new MessageEvent('message', {
3452
+ data: {
3453
+ __requestIframe__: 1,
3454
+ timestamp: Date.now(),
3455
+ type: 'request',
3456
+ requestId: 'req123',
3457
+ path: 'test',
3458
+ role: MessageRole.CLIENT,
3459
+ targetId: server.id
3460
+ },
3461
+ origin,
3462
+ source: mockContentWindow as any
3463
+ })
3464
+ );
3465
+
3466
+ await new Promise(resolve => setTimeout(resolve, 100));
3467
+
3468
+ // Middleware should be called
3469
+ expect(middleware).toHaveBeenCalled();
3470
+ // Handler should NOT be called because response was already sent
3471
+ expect(handler).not.toHaveBeenCalled();
3472
+
3473
+ // Response should be from middleware
3474
+ expect(mockContentWindow.postMessage).toHaveBeenCalledWith(
3475
+ expect.objectContaining({
3476
+ type: 'response',
3477
+ requestId: 'req123',
3478
+ data: { middleware: true }
3479
+ }),
3480
+ origin
3481
+ );
3482
+
3483
+ server.destroy();
3484
+ cleanupIframe(iframe);
3485
+ });
3486
+
3487
+ it('should handle map return cleanup function', () => {
3488
+ const server = requestIframeServer();
3489
+
3490
+ const handler1 = jest.fn((req, res) => res.send({}));
3491
+ const handler2 = jest.fn((req, res) => res.send({}));
3492
+
3493
+ const cleanup = server.map({
3494
+ 'path1': handler1,
3495
+ 'path2': handler2
3496
+ });
3497
+
3498
+ // Cleanup should unregister all handlers
3499
+ cleanup();
3500
+
3501
+ // Verify handlers are unregistered
3502
+ expect(server).toBeDefined();
3503
+
3504
+ server.destroy();
3505
+ });
3506
+ });
3507
+
3508
+ describe('Cache utilities', () => {
3509
+ it('should test server cache functions', () => {
3510
+ const { getCachedServer, cacheServer, removeCachedServer, clearServerCache } = require('../utils/cache');
3511
+ const { requestIframeServer } = require('../api/server');
3512
+
3513
+ // Test getCachedServer with no id
3514
+ expect(getCachedServer('key1')).toBeNull();
3515
+ expect(getCachedServer(undefined, undefined)).toBeNull();
3516
+
3517
+ // Test cacheServer with no id
3518
+ const server1 = requestIframeServer({ id: 'server1', secretKey: 'key1' });
3519
+ cacheServer(server1, 'key1', 'server1');
3520
+
3521
+ // Test getCachedServer with id
3522
+ const cached = getCachedServer('key1', 'server1');
3523
+ expect(cached).toBe(server1);
3524
+
3525
+ // Test removeCachedServer with no id
3526
+ removeCachedServer('key1'); // Should not throw
3527
+ removeCachedServer(undefined, undefined); // Should not throw
3528
+
3529
+ // Test removeCachedServer with id
3530
+ removeCachedServer('key1', 'server1');
3531
+ expect(getCachedServer('key1', 'server1')).toBeNull();
3532
+
3533
+ // Test clearServerCache
3534
+ const server2 = requestIframeServer({ id: 'server2', secretKey: 'key2' });
3535
+ cacheServer(server2, 'key2', 'server2');
3536
+ clearServerCache();
3537
+ expect(getCachedServer('key2', 'server2')).toBeNull();
3538
+
3539
+ server1.destroy();
3540
+ server2.destroy();
3541
+ });
3542
+
3543
+ it('should test clearMessageChannelCache', () => {
3544
+ const { clearMessageChannelCache, getOrCreateMessageChannel } = require('../utils/cache');
3545
+
3546
+ // Create a channel
3547
+ const channel1 = getOrCreateMessageChannel('test-key');
3548
+ expect(channel1).toBeDefined();
3549
+
3550
+ // Clear cache
3551
+ clearMessageChannelCache();
3552
+
3553
+ // Create another channel - should be new instance
3554
+ const channel2 = getOrCreateMessageChannel('test-key');
3555
+ expect(channel2).toBeDefined();
3556
+
3557
+ channel1.release();
3558
+ channel2.release();
3559
+ });
3560
+ });
3561
+
3562
+ describe('Additional edge cases', () => {
3563
+ it('should handle headers in request options', async () => {
3564
+ const origin = 'https://example.com';
3565
+ const iframe = createTestIframe(origin);
3566
+ const mockContentWindow = {
3567
+ postMessage: jest.fn((msg: PostMessageData) => {
3568
+ if (msg.type === 'request') {
3569
+ window.dispatchEvent(
3570
+ new MessageEvent('message', {
3571
+ data: {
3572
+ __requestIframe__: 1,
3573
+ type: 'ack',
3574
+ requestId: msg.requestId,
3575
+ path: msg.path,
3576
+ role: MessageRole.SERVER
3577
+ },
3578
+ origin
3579
+ })
3580
+ );
3581
+ setTimeout(() => {
3582
+ window.dispatchEvent(
3583
+ new MessageEvent('message', {
3584
+ data: {
3585
+ __requestIframe__: 1,
3586
+ type: 'response',
3587
+ requestId: msg.requestId,
3588
+ data: { result: 'success' },
3589
+ status: 200,
3590
+ statusText: 'OK',
3591
+ role: MessageRole.SERVER
3592
+ },
3593
+ origin
3594
+ })
3595
+ );
3596
+ }, 10);
3597
+ }
3598
+ })
3599
+ };
3600
+ Object.defineProperty(iframe, 'contentWindow', {
3601
+ value: mockContentWindow,
3602
+ writable: true
3603
+ });
3604
+
3605
+ const client = requestIframeClient(iframe, {
3606
+ headers: {
3607
+ 'X-Initial': 'initial-value'
3608
+ }
3609
+ });
3610
+
3611
+ // Send request with additional headers
3612
+ await client.send('test', {}, {
3613
+ headers: {
3614
+ 'X-Request': 'request-value',
3615
+ 'X-Dynamic': (config: RequestConfig) => `dynamic-${config.path}`
3616
+ }
3617
+ });
3618
+
3619
+ const requestCall = mockContentWindow.postMessage.mock.calls.find(
3620
+ (call: any[]) => call[0]?.type === 'request'
3621
+ );
3622
+ expect(requestCall).toBeDefined();
3623
+ if (requestCall && requestCall[0]) {
3624
+ expect(requestCall[0].headers?.['X-Initial']).toBe('initial-value');
3625
+ expect(requestCall[0].headers?.['X-Request']).toBe('request-value');
3626
+ expect(requestCall[0].headers?.['X-Dynamic']).toBe('dynamic-test');
3627
+ }
3628
+
3629
+ cleanupIframe(iframe);
3630
+ });
3631
+
3632
+ it('should handle isConnect with error response', async () => {
3633
+ const origin = 'https://example.com';
3634
+ const iframe = createTestIframe(origin);
3635
+ const mockContentWindow = {
3636
+ postMessage: jest.fn((msg: PostMessageData) => {
3637
+ if (msg.type === 'ping') {
3638
+ // Send error instead of pong
3639
+ setTimeout(() => {
3640
+ window.dispatchEvent(
3641
+ new MessageEvent('message', {
3642
+ data: {
3643
+ __requestIframe__: 1,
3644
+ type: 'error',
3645
+ requestId: msg.requestId,
3646
+ error: { message: 'Connection error' },
3647
+ role: MessageRole.SERVER
3648
+ },
3649
+ origin
3650
+ })
3651
+ );
3652
+ }, 10);
3653
+ }
3654
+ })
3655
+ };
3656
+ Object.defineProperty(iframe, 'contentWindow', {
3657
+ value: mockContentWindow,
3658
+ writable: true
3659
+ });
3660
+
3661
+ const client = requestIframeClient(iframe, { ackTimeout: 1000 });
3662
+
3663
+ const connected = await client.isConnect();
3664
+ expect(connected).toBe(false);
3665
+
3666
+ cleanupIframe(iframe);
3667
+ });
3668
+
3669
+ it('should handle response interceptor without rejected callback', async () => {
3670
+ const origin = 'https://example.com';
3671
+ const iframe = createTestIframe(origin);
3672
+ const mockContentWindow = {
3673
+ postMessage: jest.fn((msg: PostMessageData) => {
3674
+ if (msg.type === 'request') {
3675
+ window.dispatchEvent(
3676
+ new MessageEvent('message', {
3677
+ data: {
3678
+ __requestIframe__: 1,
3679
+ type: 'ack',
3680
+ requestId: msg.requestId,
3681
+ path: msg.path,
3682
+ role: MessageRole.SERVER
3683
+ },
3684
+ origin
3685
+ })
3686
+ );
3687
+ setTimeout(() => {
3688
+ window.dispatchEvent(
3689
+ new MessageEvent('message', {
3690
+ data: {
3691
+ __requestIframe__: 1,
3692
+ type: 'error',
3693
+ requestId: msg.requestId,
3694
+ error: { message: 'Test error', code: 'TEST_ERROR' },
3695
+ status: 500,
3696
+ statusText: 'Internal Server Error',
3697
+ role: MessageRole.SERVER
3698
+ },
3699
+ origin
3700
+ })
3701
+ );
3702
+ }, 10);
3703
+ }
3704
+ })
3705
+ };
3706
+ Object.defineProperty(iframe, 'contentWindow', {
3707
+ value: mockContentWindow,
3708
+ writable: true
3709
+ });
3710
+
3711
+ const client = requestIframeClient(iframe);
3712
+
3713
+ // Add response interceptor without rejected callback
3714
+ client.interceptors.response.use(
3715
+ (response) => response
3716
+ // No rejected callback - should test the Promise.reject path
3717
+ );
3718
+
3719
+ try {
3720
+ await client.send('test', {});
3721
+ fail('Should have thrown error');
3722
+ } catch (error: any) {
3723
+ expect(error.message).toBe('Test error');
3724
+ }
3725
+
3726
+ cleanupIframe(iframe);
3727
+ });
3728
+
3729
+ it('should handle request timeout', async () => {
3730
+ const origin = 'https://example.com';
3731
+ const iframe = createTestIframe(origin);
3732
+ const mockContentWindow = {
3733
+ postMessage: jest.fn((msg: PostMessageData) => {
3734
+ if (msg.type === 'request') {
3735
+ // Send ACK but never send response
3736
+ window.dispatchEvent(
3737
+ new MessageEvent('message', {
3738
+ data: {
3739
+ __requestIframe__: 1,
3740
+ type: 'ack',
3741
+ requestId: msg.requestId,
3742
+ path: msg.path,
3743
+ role: MessageRole.SERVER
3744
+ },
3745
+ origin
3746
+ })
3747
+ );
3748
+ // Don't send response - should timeout
3749
+ }
3750
+ })
3751
+ };
3752
+ Object.defineProperty(iframe, 'contentWindow', {
3753
+ value: mockContentWindow,
3754
+ writable: true
3755
+ });
3756
+
3757
+ const client = requestIframeClient(iframe, { timeout: 50 });
3758
+
3759
+ try {
3760
+ await client.send('test', {});
3761
+ fail('Should have timed out');
3762
+ } catch (error: any) {
3763
+ expect(error.message).toContain('timeout');
3764
+ }
3765
+
3766
+ cleanupIframe(iframe);
3767
+ });
3768
+
3769
+ it('should handle async timeout', async () => {
3770
+ const origin = 'https://example.com';
3771
+ const iframe = createTestIframe(origin);
3772
+ const mockContentWindow = {
3773
+ postMessage: jest.fn((msg: PostMessageData) => {
3774
+ if (msg.type === 'request') {
3775
+ window.dispatchEvent(
3776
+ new MessageEvent('message', {
3777
+ data: {
3778
+ __requestIframe__: 1,
3779
+ type: 'ack',
3780
+ requestId: msg.requestId,
3781
+ path: msg.path,
3782
+ role: MessageRole.SERVER
3783
+ },
3784
+ origin
3785
+ })
3786
+ );
3787
+ setTimeout(() => {
3788
+ // Send ASYNC but never send response
3789
+ window.dispatchEvent(
3790
+ new MessageEvent('message', {
3791
+ data: {
3792
+ __requestIframe__: 1,
3793
+ type: 'async',
3794
+ requestId: msg.requestId,
3795
+ path: msg.path,
3796
+ role: MessageRole.SERVER
3797
+ },
3798
+ origin
3799
+ })
3800
+ );
3801
+ }, 10);
3802
+ // Don't send response - should timeout
3803
+ }
3804
+ })
3805
+ };
3806
+ Object.defineProperty(iframe, 'contentWindow', {
3807
+ value: mockContentWindow,
3808
+ writable: true
3809
+ });
3810
+
3811
+ const client = requestIframeClient(iframe, { asyncTimeout: 50 });
3812
+
3813
+ try {
3814
+ await client.send('test', {});
3815
+ fail('Should have timed out');
3816
+ } catch (error: any) {
3817
+ expect(error.message).toContain('timeout');
3818
+ }
3819
+
3820
+ cleanupIframe(iframe);
3821
+ });
3822
+
3823
+ it('should not override existing targetServerId when receiving ACK', async () => {
3824
+ const origin = 'https://example.com';
3825
+ const iframe = createTestIframe(origin);
3826
+ const existingServerId = 'existing-server';
3827
+ const newServerId = 'new-server';
3828
+
3829
+ const mockContentWindow = {
3830
+ postMessage: jest.fn((msg: PostMessageData) => {
3831
+ if (msg.type === 'request') {
3832
+ window.dispatchEvent(
3833
+ new MessageEvent('message', {
3834
+ data: {
3835
+ __requestIframe__: 1,
3836
+ type: 'ack',
3837
+ requestId: msg.requestId,
3838
+ path: msg.path,
3839
+ role: MessageRole.SERVER,
3840
+ creatorId: newServerId
3841
+ },
3842
+ origin
3843
+ })
3844
+ );
3845
+ setTimeout(() => {
3846
+ window.dispatchEvent(
3847
+ new MessageEvent('message', {
3848
+ data: {
3849
+ __requestIframe__: 1,
3850
+ type: 'response',
3851
+ requestId: msg.requestId,
3852
+ data: { result: 'success' },
3853
+ status: 200,
3854
+ statusText: 'OK',
3855
+ role: MessageRole.SERVER,
3856
+ creatorId: newServerId
3857
+ },
3858
+ origin
3859
+ })
3860
+ );
3861
+ }, 10);
3862
+ }
3863
+ })
3864
+ };
3865
+ Object.defineProperty(iframe, 'contentWindow', {
3866
+ value: mockContentWindow,
3867
+ writable: true
3868
+ });
3869
+
3870
+ const client = requestIframeClient(iframe);
3871
+
3872
+ // Set existing targetServerId
3873
+ (client as any)._targetServerId = existingServerId;
3874
+
3875
+ await client.send('test', {});
3876
+
3877
+ // targetServerId should not be overridden
3878
+ expect((client as any)._targetServerId).toBe(existingServerId);
3879
+
3880
+ cleanupIframe(iframe);
3881
+ });
3882
+
3883
+ it('should handle response with requireAck', async () => {
3884
+ const origin = 'https://example.com';
3885
+ const iframe = createTestIframe(origin);
3886
+ const mockContentWindow = {
3887
+ postMessage: jest.fn((msg: PostMessageData) => {
3888
+ if (msg.type === 'request') {
3889
+ window.dispatchEvent(
3890
+ new MessageEvent('message', {
3891
+ data: {
3892
+ __requestIframe__: 1,
3893
+ type: 'ack',
3894
+ requestId: msg.requestId,
3895
+ path: msg.path,
3896
+ role: MessageRole.SERVER
3897
+ },
3898
+ origin
3899
+ })
3900
+ );
3901
+ setTimeout(() => {
3902
+ window.dispatchEvent(
3903
+ new MessageEvent('message', {
3904
+ data: {
3905
+ __requestIframe__: 1,
3906
+ type: 'response',
3907
+ requestId: msg.requestId,
3908
+ data: { result: 'success' },
3909
+ status: 200,
3910
+ statusText: 'OK',
3911
+ role: MessageRole.SERVER,
3912
+ requireAck: true
3913
+ },
3914
+ origin
3915
+ })
3916
+ );
3917
+ }, 10);
3918
+ } else if (msg.type === 'received') {
3919
+ // Acknowledge receipt
3920
+ }
3921
+ })
3922
+ };
3923
+ Object.defineProperty(iframe, 'contentWindow', {
3924
+ value: mockContentWindow,
3925
+ writable: true
3926
+ });
3927
+
3928
+ const client = requestIframeClient(iframe);
3929
+ const response = await client.send('test', {});
3930
+
3931
+ expect(response.data).toEqual({ result: 'success' });
3932
+
3933
+ // Verify RECEIVED message was sent
3934
+ const receivedCall = mockContentWindow.postMessage.mock.calls.find(
3935
+ (call: any[]) => call[0]?.type === 'received'
3936
+ );
3937
+ expect(receivedCall).toBeDefined();
3938
+
3939
+ cleanupIframe(iframe);
3940
+ });
3941
+
3942
+ it('should handle handler returning a value', async () => {
3943
+ const origin = 'https://example.com';
3944
+ const iframe = createTestIframe(origin);
3945
+ const mockContentWindow = {
3946
+ postMessage: jest.fn()
3947
+ };
3948
+ Object.defineProperty(iframe, 'contentWindow', {
3949
+ value: mockContentWindow,
3950
+ writable: true
3951
+ });
3952
+
3953
+ const server = requestIframeServer();
3954
+
3955
+ // Handler returns a value (not undefined)
3956
+ server.on('test', (req, res) => {
3957
+ return { result: 'from-return' };
3958
+ });
3959
+
3960
+ window.dispatchEvent(
3961
+ new MessageEvent('message', {
3962
+ data: {
3963
+ __requestIframe__: 1,
3964
+ timestamp: Date.now(),
3965
+ type: 'request',
3966
+ requestId: 'req123',
3967
+ path: 'test',
3968
+ role: MessageRole.CLIENT,
3969
+ targetId: server.id
3970
+ },
3971
+ origin,
3972
+ source: mockContentWindow as any
3973
+ })
3974
+ );
3975
+
3976
+ await new Promise(resolve => setTimeout(resolve, 100));
3977
+
3978
+ // Should send response with returned value
3979
+ expect(mockContentWindow.postMessage).toHaveBeenCalledWith(
3980
+ expect.objectContaining({
3981
+ type: 'response',
3982
+ requestId: 'req123',
3983
+ data: { result: 'from-return' }
3984
+ }),
3985
+ origin
3986
+ );
3987
+
3988
+ server.destroy();
3989
+ cleanupIframe(iframe);
3990
+ });
3991
+
3992
+ it('should handle ack timeout in registerPendingAck reject callback', async () => {
3993
+ const origin = 'https://example.com';
3994
+ const iframe = createTestIframe(origin);
3995
+ const mockContentWindow = {
3996
+ postMessage: jest.fn()
3997
+ };
3998
+ Object.defineProperty(iframe, 'contentWindow', {
3999
+ value: mockContentWindow,
4000
+ writable: true
4001
+ });
4002
+
4003
+ const server = requestIframeServer({ ackTimeout: 50 });
4004
+
4005
+ server.on('test', (req, res) => {
4006
+ // Send response with requireAck, but client never sends 'received'
4007
+ res.send({ result: 'success' }, { requireAck: true });
4008
+ });
4009
+
4010
+ window.dispatchEvent(
4011
+ new MessageEvent('message', {
4012
+ data: {
4013
+ __requestIframe__: 1,
4014
+ timestamp: Date.now(),
4015
+ type: 'request',
4016
+ requestId: 'req123',
4017
+ path: 'test',
4018
+ role: MessageRole.CLIENT,
4019
+ targetId: server.id
4020
+ },
4021
+ origin,
4022
+ source: mockContentWindow as any
4023
+ })
4024
+ );
4025
+
4026
+ // Wait for ack timeout (reject callback should be called)
4027
+ await new Promise(resolve => setTimeout(resolve, 150));
4028
+
4029
+ // Server should have sent response
4030
+ expect(mockContentWindow.postMessage).toHaveBeenCalledWith(
4031
+ expect.objectContaining({
4032
+ type: 'response',
4033
+ requestId: 'req123'
4034
+ }),
4035
+ origin
4036
+ );
4037
+
4038
+ server.destroy();
4039
+ cleanupIframe(iframe);
4040
+ });
4041
+
4042
+ it('should skip middleware when response already sent', async () => {
4043
+ const origin = 'https://example.com';
4044
+ const iframe = createTestIframe(origin);
4045
+ const mockContentWindow = {
4046
+ postMessage: jest.fn()
4047
+ };
4048
+ Object.defineProperty(iframe, 'contentWindow', {
4049
+ value: mockContentWindow,
4050
+ writable: true
4051
+ });
4052
+
4053
+ const server = requestIframeServer();
4054
+
4055
+ const middleware1 = jest.fn((req, res, next) => {
4056
+ res.send({ middleware1: true });
4057
+ // Response sent, don't call next
4058
+ });
4059
+
4060
+ const middleware2 = jest.fn((req, res, next) => {
4061
+ next();
4062
+ });
4063
+
4064
+ const handler = jest.fn((req, res) => {
4065
+ res.send({ handler: true });
4066
+ });
4067
+
4068
+ server.use(middleware1);
4069
+ server.use(middleware2);
4070
+ server.on('test', handler);
4071
+
4072
+ window.dispatchEvent(
4073
+ new MessageEvent('message', {
4074
+ data: {
4075
+ __requestIframe__: 1,
4076
+ timestamp: Date.now(),
4077
+ type: 'request',
4078
+ requestId: 'req123',
4079
+ path: 'test',
4080
+ role: MessageRole.CLIENT,
4081
+ targetId: server.id
4082
+ },
4083
+ origin,
4084
+ source: mockContentWindow as any
4085
+ })
4086
+ );
4087
+
4088
+ await new Promise(resolve => setTimeout(resolve, 100));
4089
+
4090
+ // Middleware1 should be called
4091
+ expect(middleware1).toHaveBeenCalled();
4092
+ // Middleware2 should NOT be called because response was already sent in middleware1
4093
+ expect(middleware2).not.toHaveBeenCalled();
4094
+ // Handler should NOT be called because response was already sent
4095
+ expect(handler).not.toHaveBeenCalled();
4096
+
4097
+ server.destroy();
4098
+ cleanupIframe(iframe);
4099
+ });
4100
+
4101
+ it('should handle isConnect reject callback', async () => {
4102
+ const origin = 'https://example.com';
4103
+ const iframe = createTestIframe(origin);
4104
+ const mockContentWindow = {
4105
+ postMessage: jest.fn()
4106
+ };
4107
+ Object.defineProperty(iframe, 'contentWindow', {
4108
+ value: mockContentWindow,
4109
+ writable: true
4110
+ });
4111
+
4112
+ const client = requestIframeClient(iframe, { ackTimeout: 50 });
4113
+
4114
+ // Simulate error in pending request registration
4115
+ // This will trigger the reject callback
4116
+ const connected = await client.isConnect();
4117
+ expect(connected).toBe(false);
4118
+
4119
+ cleanupIframe(iframe);
4120
+ });
4121
+
4122
+ it('should handle stream messages via dispatchStreamMessage', async () => {
4123
+ const origin = 'https://example.com';
4124
+ const iframe = createTestIframe(origin);
4125
+ const mockContentWindow = {
4126
+ postMessage: jest.fn()
4127
+ };
4128
+ Object.defineProperty(iframe, 'contentWindow', {
4129
+ value: mockContentWindow,
4130
+ writable: true
4131
+ });
4132
+
4133
+ const client = requestIframeClient(iframe);
4134
+ const streamId = 'test-stream';
4135
+ const handler = jest.fn();
4136
+
4137
+ // Register stream handler
4138
+ (client as any).registerStreamHandler(streamId, handler);
4139
+
4140
+ // Dispatch stream_data message
4141
+ window.dispatchEvent(
4142
+ new MessageEvent('message', {
4143
+ data: {
4144
+ __requestIframe__: 1,
4145
+ type: 'stream_data',
4146
+ requestId: 'req123',
4147
+ body: {
4148
+ streamId,
4149
+ data: 'test',
4150
+ type: 'data'
4151
+ },
4152
+ role: MessageRole.SERVER
4153
+ },
4154
+ origin
4155
+ })
4156
+ );
4157
+
4158
+ await new Promise(resolve => setTimeout(resolve, 50));
4159
+
4160
+ expect(handler).toHaveBeenCalled();
4161
+
4162
+ // Dispatch stream_end message
4163
+ window.dispatchEvent(
4164
+ new MessageEvent('message', {
4165
+ data: {
4166
+ __requestIframe__: 1,
4167
+ type: 'stream_end',
4168
+ requestId: 'req123',
4169
+ body: {
4170
+ streamId
4171
+ },
4172
+ role: MessageRole.SERVER
4173
+ },
4174
+ origin
4175
+ })
4176
+ );
4177
+
4178
+ await new Promise(resolve => setTimeout(resolve, 50));
4179
+
4180
+ cleanupIframe(iframe);
4181
+ });
4182
+
4183
+ it('should handle error with requireAck', async () => {
4184
+ const origin = 'https://example.com';
4185
+ const iframe = createTestIframe(origin);
4186
+ const mockContentWindow = {
4187
+ postMessage: jest.fn((msg: PostMessageData) => {
4188
+ if (msg.type === 'request') {
4189
+ window.dispatchEvent(
4190
+ new MessageEvent('message', {
4191
+ data: {
4192
+ __requestIframe__: 1,
4193
+ type: 'ack',
4194
+ requestId: msg.requestId,
4195
+ path: msg.path,
4196
+ role: MessageRole.SERVER
4197
+ },
4198
+ origin
4199
+ })
4200
+ );
4201
+ setTimeout(() => {
4202
+ window.dispatchEvent(
4203
+ new MessageEvent('message', {
4204
+ data: {
4205
+ __requestIframe__: 1,
4206
+ type: 'error',
4207
+ requestId: msg.requestId,
4208
+ error: { message: 'Test error', code: 'TEST_ERROR' },
4209
+ status: 500,
4210
+ statusText: 'Internal Server Error',
4211
+ role: MessageRole.SERVER,
4212
+ requireAck: true
4213
+ },
4214
+ origin
4215
+ })
4216
+ );
4217
+ }, 10);
4218
+ }
4219
+ })
4220
+ };
4221
+ Object.defineProperty(iframe, 'contentWindow', {
4222
+ value: mockContentWindow,
4223
+ writable: true
4224
+ });
4225
+
4226
+ const client = requestIframeClient(iframe);
4227
+
4228
+ try {
4229
+ await client.send('test', {});
4230
+ fail('Should have thrown error');
4231
+ } catch (error: any) {
4232
+ expect(error.message).toBe('Test error');
4233
+ }
4234
+
4235
+ // Verify RECEIVED message was sent
4236
+ const receivedCall = mockContentWindow.postMessage.mock.calls.find(
4237
+ (call: any[]) => call[0]?.type === 'received'
4238
+ );
4239
+ expect(receivedCall).toBeDefined();
4240
+
4241
+ cleanupIframe(iframe);
4242
+ });
4243
+
4244
+ it('should handle error in pending request registration', async () => {
4245
+ const origin = 'https://example.com';
4246
+ const iframe = createTestIframe(origin);
4247
+ const mockContentWindow = {
4248
+ postMessage: jest.fn()
4249
+ };
4250
+ Object.defineProperty(iframe, 'contentWindow', {
4251
+ value: mockContentWindow,
4252
+ writable: true
4253
+ });
4254
+
4255
+ const client = requestIframeClient(iframe);
4256
+
4257
+ // Simulate error during request registration
4258
+ // This will trigger the error callback in _registerPendingRequest
4259
+ try {
4260
+ // Force an error by making the server unavailable
4261
+ await client.send('test', {}, { timeout: 50 });
4262
+ fail('Should have thrown error');
4263
+ } catch (error: any) {
4264
+ expect(error).toBeDefined();
4265
+ }
4266
+
4267
+ cleanupIframe(iframe);
4268
+ });
4269
+
4270
+ it('should handle message already handled by another server', async () => {
4271
+ const origin = 'https://example.com';
4272
+ const iframe = createTestIframe(origin);
4273
+ const mockContentWindow = {
4274
+ postMessage: jest.fn()
4275
+ };
4276
+ Object.defineProperty(iframe, 'contentWindow', {
4277
+ value: mockContentWindow,
4278
+ writable: true
4279
+ });
4280
+
4281
+ const server1 = requestIframeServer();
4282
+ const server2 = requestIframeServer();
4283
+
4284
+ const handler1 = jest.fn((req, res) => res.send({ server: 1 }));
4285
+ const handler2 = jest.fn((req, res) => res.send({ server: 2 }));
4286
+
4287
+ server1.on('test', handler1);
4288
+ server2.on('test', handler2);
4289
+
4290
+ // Create message context that indicates it was already handled
4291
+ // This simulates the case where context.handledBy is set
4292
+ const messageData = {
4293
+ __requestIframe__: 1,
4294
+ timestamp: Date.now(),
4295
+ type: 'request' as const,
4296
+ requestId: 'req123',
4297
+ path: 'test',
4298
+ role: MessageRole.CLIENT,
4299
+ targetId: server1.id
4300
+ };
4301
+
4302
+ // First server processes it
4303
+ window.dispatchEvent(
4304
+ new MessageEvent('message', {
4305
+ data: messageData,
4306
+ origin,
4307
+ source: mockContentWindow as any
4308
+ })
4309
+ );
4310
+
4311
+ await new Promise(resolve => setTimeout(resolve, 50));
4312
+
4313
+ // Only server1 should handle it (because of targetId)
4314
+ expect(handler1).toHaveBeenCalled();
4315
+ expect(handler2).not.toHaveBeenCalled();
4316
+
4317
+ server1.destroy();
4318
+ server2.destroy();
4319
+ cleanupIframe(iframe);
4320
+ });
4321
+
4322
+ it('should handle ack timeout reject callback', async () => {
4323
+ const origin = 'https://example.com';
4324
+ const iframe = createTestIframe(origin);
4325
+ const mockContentWindow = {
4326
+ postMessage: jest.fn()
4327
+ };
4328
+ Object.defineProperty(iframe, 'contentWindow', {
4329
+ value: mockContentWindow,
4330
+ writable: true
4331
+ });
4332
+
4333
+ const server = requestIframeServer({ ackTimeout: 50 });
4334
+
4335
+ server.on('test', (req, res) => {
4336
+ // Send response with requireAck, but client never sends 'received'
4337
+ // This will trigger ack timeout and the reject callback
4338
+ res.send({ result: 'success' }, { requireAck: true });
4339
+ });
4340
+
4341
+ window.dispatchEvent(
4342
+ new MessageEvent('message', {
4343
+ data: {
4344
+ __requestIframe__: 1,
4345
+ timestamp: Date.now(),
4346
+ type: 'request',
4347
+ requestId: 'req123',
4348
+ path: 'test',
4349
+ role: MessageRole.CLIENT,
4350
+ targetId: server.id
4351
+ },
4352
+ origin,
4353
+ source: mockContentWindow as any
4354
+ })
4355
+ );
4356
+
4357
+ // Wait for ack timeout (reject callback should be called)
4358
+ await new Promise(resolve => setTimeout(resolve, 150));
4359
+
4360
+ // Server should have sent response
4361
+ expect(mockContentWindow.postMessage).toHaveBeenCalledWith(
4362
+ expect.objectContaining({
4363
+ type: 'response',
4364
+ requestId: 'req123'
4365
+ }),
4366
+ origin
4367
+ );
4368
+
4369
+ server.destroy();
4370
+ cleanupIframe(iframe);
4371
+ });
4372
+
4373
+ it('should skip next middleware when response already sent', async () => {
4374
+ const origin = 'https://example.com';
4375
+ const iframe = createTestIframe(origin);
4376
+ const mockContentWindow = {
4377
+ postMessage: jest.fn()
4378
+ };
4379
+ Object.defineProperty(iframe, 'contentWindow', {
4380
+ value: mockContentWindow,
4381
+ writable: true
4382
+ });
4383
+
4384
+ const server = requestIframeServer();
4385
+
4386
+ const middleware1 = jest.fn((req, res, next) => {
4387
+ res.send({ middleware1: true });
4388
+ // Response sent, but still call next to test the res._sent check
4389
+ next();
4390
+ });
4391
+
4392
+ const middleware2 = jest.fn((req, res, next) => {
4393
+ // This should not execute because res._sent is true
4394
+ next();
4395
+ });
4396
+
4397
+ const handler = jest.fn((req, res) => {
4398
+ res.send({ handler: true });
4399
+ });
4400
+
4401
+ server.use(middleware1);
4402
+ server.use(middleware2);
4403
+ server.on('test', handler);
4404
+
4405
+ window.dispatchEvent(
4406
+ new MessageEvent('message', {
4407
+ data: {
4408
+ __requestIframe__: 1,
4409
+ timestamp: Date.now(),
4410
+ type: 'request',
4411
+ requestId: 'req123',
4412
+ path: 'test',
4413
+ role: MessageRole.CLIENT,
4414
+ targetId: server.id
4415
+ },
4416
+ origin,
4417
+ source: mockContentWindow as any
4418
+ })
4419
+ );
4420
+
4421
+ await new Promise(resolve => setTimeout(resolve, 100));
4422
+
4423
+ // Middleware1 should be called
4424
+ expect(middleware1).toHaveBeenCalled();
4425
+ // Middleware2's next() should check res._sent and return early, so handler should not be called
4426
+ // Note: middleware2 itself may or may not be called depending on implementation
4427
+ // Handler should NOT be called because response was already sent
4428
+ expect(handler).not.toHaveBeenCalled();
4429
+
4430
+ server.destroy();
4431
+ cleanupIframe(iframe);
4432
+ });
2215
4433
  });
2216
4434
  });