request-iframe 0.0.3 → 0.0.5

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 (69) hide show
  1. package/QUICKSTART.CN.md +35 -8
  2. package/QUICKSTART.md +35 -8
  3. package/README.CN.md +177 -24
  4. package/README.md +237 -19
  5. package/library/__tests__/channel.test.ts +16 -4
  6. package/library/__tests__/coverage-branches.test.ts +356 -0
  7. package/library/__tests__/debug.test.ts +22 -0
  8. package/library/__tests__/dispatcher.test.ts +8 -4
  9. package/library/__tests__/requestIframe.test.ts +1243 -87
  10. package/library/__tests__/stream.test.ts +92 -16
  11. package/library/__tests__/utils.test.ts +41 -1
  12. package/library/api/client.d.ts.map +1 -1
  13. package/library/api/client.js +1 -0
  14. package/library/constants/index.d.ts +2 -0
  15. package/library/constants/index.d.ts.map +1 -1
  16. package/library/constants/index.js +3 -1
  17. package/library/constants/messages.d.ts +3 -0
  18. package/library/constants/messages.d.ts.map +1 -1
  19. package/library/constants/messages.js +3 -0
  20. package/library/core/client-server.d.ts +4 -0
  21. package/library/core/client-server.d.ts.map +1 -1
  22. package/library/core/client-server.js +45 -22
  23. package/library/core/client.d.ts +36 -4
  24. package/library/core/client.d.ts.map +1 -1
  25. package/library/core/client.js +508 -285
  26. package/library/core/request.d.ts +3 -1
  27. package/library/core/request.d.ts.map +1 -1
  28. package/library/core/request.js +2 -1
  29. package/library/core/response.d.ts +26 -4
  30. package/library/core/response.d.ts.map +1 -1
  31. package/library/core/response.js +192 -112
  32. package/library/core/server.d.ts +13 -0
  33. package/library/core/server.d.ts.map +1 -1
  34. package/library/core/server.js +221 -6
  35. package/library/index.d.ts +2 -1
  36. package/library/index.d.ts.map +1 -1
  37. package/library/index.js +39 -3
  38. package/library/message/channel.d.ts +2 -2
  39. package/library/message/channel.d.ts.map +1 -1
  40. package/library/message/channel.js +5 -1
  41. package/library/message/dispatcher.d.ts +2 -2
  42. package/library/message/dispatcher.d.ts.map +1 -1
  43. package/library/message/dispatcher.js +6 -5
  44. package/library/stream/index.d.ts +11 -1
  45. package/library/stream/index.d.ts.map +1 -1
  46. package/library/stream/index.js +21 -3
  47. package/library/stream/types.d.ts +2 -2
  48. package/library/stream/types.d.ts.map +1 -1
  49. package/library/stream/writable-stream.d.ts +1 -1
  50. package/library/stream/writable-stream.d.ts.map +1 -1
  51. package/library/stream/writable-stream.js +87 -47
  52. package/library/types/index.d.ts +29 -5
  53. package/library/types/index.d.ts.map +1 -1
  54. package/library/utils/debug.d.ts.map +1 -1
  55. package/library/utils/debug.js +6 -2
  56. package/library/utils/error.d.ts +21 -0
  57. package/library/utils/error.d.ts.map +1 -0
  58. package/library/utils/error.js +34 -0
  59. package/library/utils/index.d.ts +21 -0
  60. package/library/utils/index.d.ts.map +1 -1
  61. package/library/utils/index.js +141 -2
  62. package/library/utils/path-match.d.ts +16 -0
  63. package/library/utils/path-match.d.ts.map +1 -1
  64. package/library/utils/path-match.js +65 -0
  65. package/package.json +2 -1
  66. package/react/library/__tests__/index.test.tsx +44 -22
  67. package/react/library/index.d.ts.map +1 -1
  68. package/react/library/index.js +81 -23
  69. 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, MessageRole, Messages } from '../constants';
4
+ import { HttpHeader, MessageRole, Messages, ErrorCode } 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
@@ -110,6 +123,251 @@ describe('requestIframeClient and requestIframeServer', () => {
110
123
  cleanupIframe(iframe);
111
124
  });
112
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
+
113
371
  it('should throw error when iframe.contentWindow is unavailable', () => {
114
372
  const iframe = document.createElement('iframe');
115
373
  iframe.src = 'https://example.com/test.html';
@@ -525,6 +783,314 @@ describe('requestIframeClient and requestIframeServer', () => {
525
783
  });
526
784
  });
527
785
 
786
+ describe('secretKey message isolation', () => {
787
+ it('should successfully communicate when client and server use the same secretKey', async () => {
788
+ const origin = 'https://example.com';
789
+ const iframe = createTestIframe(origin);
790
+
791
+ const mockContentWindow = {
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
+ })
833
+ };
834
+ Object.defineProperty(iframe, 'contentWindow', {
835
+ value: mockContentWindow,
836
+ writable: true
837
+ });
838
+
839
+ const client = requestIframeClient(iframe, { secretKey: 'test-key' });
840
+ const server = requestIframeServer({ secretKey: 'test-key' });
841
+
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
+
528
1094
  describe('Middleware', () => {
529
1095
  it('should support global middleware', async () => {
530
1096
  const origin = 'https://example.com';
@@ -701,7 +1267,7 @@ describe('requestIframeClient and requestIframeServer', () => {
701
1267
  });
702
1268
 
703
1269
  describe('sendFile', () => {
704
- it('should support sending file (base64 encoded)', async () => {
1270
+ it('should support sending file (stream)', async () => {
705
1271
  const origin = 'https://example.com';
706
1272
  const iframe = createTestIframe(origin);
707
1273
 
@@ -1070,6 +1636,157 @@ describe('requestIframeClient and requestIframeServer', () => {
1070
1636
  }, 20000);
1071
1637
  });
1072
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
+ });
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();
1785
+ server.destroy();
1786
+ cleanupIframe(iframe);
1787
+ });
1788
+ });
1789
+
1073
1790
  describe('server.map', () => {
1074
1791
  it('should register multiple event handlers at once', async () => {
1075
1792
  const origin = 'https://example.com';
@@ -2012,122 +2729,355 @@ describe('requestIframeClient and requestIframeServer', () => {
2012
2729
  const origin = 'https://example.com';
2013
2730
  const iframe = createTestIframe(origin);
2014
2731
 
2015
- const mockContentWindow = {
2016
- postMessage: jest.fn()
2732
+ const mockContentWindow = {
2733
+ postMessage: jest.fn()
2734
+ };
2735
+ Object.defineProperty(iframe, 'contentWindow', {
2736
+ value: mockContentWindow,
2737
+ writable: true
2738
+ });
2739
+
2740
+ const server = requestIframeServer();
2741
+
2742
+ server.use(async (req, res, next) => {
2743
+ await new Promise(resolve => setTimeout(resolve, 10));
2744
+ throw new Error('Async middleware error');
2745
+ });
2746
+
2747
+ server.on('test', (req, res) => {
2748
+ res.send({});
2749
+ });
2750
+
2751
+ const requestId = 'req-middleware-async-error';
2752
+ window.dispatchEvent(
2753
+ new MessageEvent('message', {
2754
+ data: {
2755
+ __requestIframe__: 1,
2756
+ type: 'request',
2757
+ requestId: requestId,
2758
+ path: 'test',
2759
+ body: {}
2760
+ },
2761
+ origin,
2762
+ source: mockContentWindow as any
2763
+ })
2764
+ );
2765
+ await new Promise((resolve) => setTimeout(resolve, 150));
2766
+
2767
+ const errorCall = mockContentWindow.postMessage.mock.calls.find(
2768
+ (call: any[]) => call[0]?.type === 'error' ||
2769
+ (call[0]?.type === 'response' && call[0]?.status === 500)
2770
+ );
2771
+ expect(errorCall).toBeDefined();
2772
+
2773
+ server.destroy();
2774
+ cleanupIframe(iframe);
2775
+ });
2776
+
2777
+ it('should handle request without path', async () => {
2778
+ const origin = 'https://example.com';
2779
+ const iframe = createTestIframe(origin);
2780
+
2781
+ const mockContentWindow = {
2782
+ postMessage: jest.fn()
2783
+ };
2784
+ Object.defineProperty(iframe, 'contentWindow', {
2785
+ value: mockContentWindow,
2786
+ writable: true
2787
+ });
2788
+
2789
+ const server = requestIframeServer();
2790
+
2791
+ const requestId = 'req-no-path';
2792
+ window.dispatchEvent(
2793
+ new MessageEvent('message', {
2794
+ data: {
2795
+ __requestIframe__: 1,
2796
+ type: 'request',
2797
+ requestId: requestId,
2798
+ body: {}
2799
+ },
2800
+ origin,
2801
+ source: mockContentWindow as any
2802
+ })
2803
+ );
2804
+ await new Promise((resolve) => setTimeout(resolve, 50));
2805
+
2806
+ // Should not crash, but also shouldn't process the request (no path means early return)
2807
+ // Server should still send ACK, but won't process further
2808
+ const ackCall = mockContentWindow.postMessage.mock.calls.find(
2809
+ (call: any[]) => call[0]?.type === 'ack'
2810
+ );
2811
+ // Server may or may not send ACK if path is missing, but should not crash
2812
+ expect(() => server.destroy()).not.toThrow();
2813
+
2814
+ cleanupIframe(iframe);
2815
+ });
2816
+
2817
+ it('should handle request without source', async () => {
2818
+ const origin = 'https://example.com';
2819
+ const iframe = createTestIframe(origin);
2820
+
2821
+ const mockContentWindow = {
2822
+ postMessage: jest.fn()
2823
+ };
2824
+ Object.defineProperty(iframe, 'contentWindow', {
2825
+ value: mockContentWindow,
2826
+ writable: true
2827
+ });
2828
+
2829
+ const server = requestIframeServer();
2830
+
2831
+ const requestId = 'req-no-source';
2832
+ window.dispatchEvent(
2833
+ new MessageEvent('message', {
2834
+ data: {
2835
+ __requestIframe__: 1,
2836
+ type: 'request',
2837
+ requestId: requestId,
2838
+ path: 'test',
2839
+ body: {}
2840
+ },
2841
+ origin
2842
+ // Intentionally no source
2843
+ })
2844
+ );
2845
+ await new Promise((resolve) => setTimeout(resolve, 50));
2846
+
2847
+ // Should not crash
2848
+ server.destroy();
2849
+ cleanupIframe(iframe);
2850
+ });
2851
+ });
2852
+
2853
+ describe('client send various body types', () => {
2854
+ it('should send plain object and server receives JSON + Content-Type', async () => {
2855
+ const origin = 'https://example.com';
2856
+ const iframe = createTestIframe(origin);
2857
+
2858
+ const mockContentWindow: any = {
2859
+ postMessage: jest.fn((msg: PostMessageData) => {
2860
+ window.dispatchEvent(
2861
+ new MessageEvent('message', {
2862
+ data: msg,
2863
+ origin,
2864
+ source: mockContentWindow as any
2865
+ })
2866
+ );
2867
+ })
2868
+ };
2869
+ Object.defineProperty(iframe, 'contentWindow', { value: mockContentWindow, writable: true });
2870
+
2871
+ const client = requestIframeClient(iframe);
2872
+ const server = requestIframeServer();
2873
+
2874
+ server.on('echoObject', (req, res) => {
2875
+ expect(req.headers[HttpHeader.CONTENT_TYPE]).toBe('application/json');
2876
+ res.send({ ok: true, received: req.body });
2877
+ });
2878
+
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
+ );
2901
+ })
2902
+ };
2903
+ Object.defineProperty(iframe, 'contentWindow', { value: mockContentWindow, writable: true });
2904
+
2905
+ const client = requestIframeClient(iframe);
2906
+ const server = requestIframeServer();
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();
2918
+ server.destroy();
2919
+ cleanupIframe(iframe);
2920
+ });
2921
+
2922
+ it('should send URLSearchParams and server receives correct Content-Type', async () => {
2923
+ const origin = 'https://example.com';
2924
+ const iframe = createTestIframe(origin);
2925
+
2926
+ const mockContentWindow: any = {
2927
+ postMessage: jest.fn((msg: PostMessageData) => {
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
+ })
2017
2973
  };
2018
- Object.defineProperty(iframe, 'contentWindow', {
2019
- value: mockContentWindow,
2020
- writable: true
2021
- });
2974
+ Object.defineProperty(iframe, 'contentWindow', { value: mockContentWindow, writable: true });
2022
2975
 
2976
+ const client = requestIframeClient(iframe);
2023
2977
  const server = requestIframeServer();
2024
2978
 
2025
- server.use(async (req, res, next) => {
2026
- await new Promise(resolve => setTimeout(resolve, 10));
2027
- throw new Error('Async middleware error');
2028
- });
2029
-
2030
- server.on('test', (req, res) => {
2031
- res.send({});
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 });
2032
2984
  });
2033
2985
 
2034
- const requestId = 'req-middleware-async-error';
2035
- window.dispatchEvent(
2036
- new MessageEvent('message', {
2037
- data: {
2038
- __requestIframe__: 1,
2039
- type: 'request',
2040
- requestId: requestId,
2041
- path: 'test',
2042
- body: {}
2043
- },
2044
- origin,
2045
- source: mockContentWindow as any
2046
- })
2047
- );
2048
- await new Promise((resolve) => setTimeout(resolve, 150));
2049
-
2050
- const errorCall = mockContentWindow.postMessage.mock.calls.find(
2051
- (call: any[]) => call[0]?.type === 'error' ||
2052
- (call[0]?.type === 'response' && call[0]?.status === 500)
2053
- );
2054
- expect(errorCall).toBeDefined();
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');
2055
2990
 
2991
+ client.destroy();
2056
2992
  server.destroy();
2057
2993
  cleanupIframe(iframe);
2058
2994
  });
2059
2995
 
2060
- it('should handle request without path', async () => {
2996
+ it('should send stream from client to server and server receives req.stream', async () => {
2061
2997
  const origin = 'https://example.com';
2062
2998
  const iframe = createTestIframe(origin);
2063
2999
 
2064
- const mockContentWindow = {
2065
- postMessage: jest.fn()
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
+ })
2066
3010
  };
2067
- Object.defineProperty(iframe, 'contentWindow', {
2068
- value: mockContentWindow,
2069
- writable: true
2070
- });
3011
+ Object.defineProperty(iframe, 'contentWindow', { value: mockContentWindow, writable: true });
2071
3012
 
3013
+ const client = requestIframeClient(iframe);
2072
3014
  const server = requestIframeServer();
2073
3015
 
2074
- const requestId = 'req-no-path';
2075
- window.dispatchEvent(
2076
- new MessageEvent('message', {
2077
- data: {
2078
- __requestIframe__: 1,
2079
- type: 'request',
2080
- requestId: requestId,
2081
- body: {}
2082
- },
2083
- origin,
2084
- source: mockContentWindow as any
2085
- })
2086
- );
2087
- await new Promise((resolve) => setTimeout(resolve, 50));
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
+ });
2088
3025
 
2089
- // Should not crash, but also shouldn't process the request (no path means early return)
2090
- // Server should still send ACK, but won't process further
2091
- const ackCall = mockContentWindow.postMessage.mock.calls.find(
2092
- (call: any[]) => call[0]?.type === 'ack'
2093
- );
2094
- // Server may or may not send ACK if path is missing, but should not crash
2095
- expect(() => server.destroy()).not.toThrow();
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']);
2096
3036
 
3037
+ client.destroy();
3038
+ server.destroy();
2097
3039
  cleanupIframe(iframe);
2098
3040
  });
2099
3041
 
2100
- it('should handle request without source', async () => {
3042
+ it('should support client.sendFile with autoResolve (server receives File/Blob in req.body)', async () => {
2101
3043
  const origin = 'https://example.com';
2102
3044
  const iframe = createTestIframe(origin);
2103
3045
 
2104
- const mockContentWindow = {
2105
- postMessage: jest.fn()
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
+ })
2106
3056
  };
2107
- Object.defineProperty(iframe, 'contentWindow', {
2108
- value: mockContentWindow,
2109
- writable: true
2110
- });
3057
+ Object.defineProperty(iframe, 'contentWindow', { value: mockContentWindow, writable: true });
2111
3058
 
3059
+ const client = requestIframeClient(iframe);
2112
3060
  const server = requestIframeServer();
2113
3061
 
2114
- const requestId = 'req-no-source';
2115
- window.dispatchEvent(
2116
- new MessageEvent('message', {
2117
- data: {
2118
- __requestIframe__: 1,
2119
- type: 'request',
2120
- requestId: requestId,
2121
- path: 'test',
2122
- body: {}
2123
- },
2124
- origin
2125
- // Intentionally no source
2126
- })
2127
- );
2128
- await new Promise((resolve) => setTimeout(resolve, 50));
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
+ });
2129
3070
 
2130
- // Should not crash
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();
2131
3081
  server.destroy();
2132
3082
  cleanupIframe(iframe);
2133
3083
  });
@@ -4431,4 +5381,210 @@ describe('requestIframeClient and requestIframeServer', () => {
4431
5381
  cleanupIframe(iframe);
4432
5382
  });
4433
5383
  });
5384
+
5385
+ describe('Target window closed detection', () => {
5386
+ it('should return true when target window is available', () => {
5387
+ const origin = 'https://example.com';
5388
+ const iframe = createTestIframe(origin);
5389
+
5390
+ const mockContentWindow = {
5391
+ closed: false,
5392
+ postMessage: jest.fn()
5393
+ } as any;
5394
+ Object.defineProperty(iframe, 'contentWindow', {
5395
+ value: mockContentWindow,
5396
+ writable: true
5397
+ });
5398
+
5399
+ const client = requestIframeClient(iframe);
5400
+ expect(client.isAvailable()).toBe(true);
5401
+
5402
+ cleanupIframe(iframe);
5403
+ });
5404
+
5405
+ it('should return false when target window is closed', () => {
5406
+ const origin = 'https://example.com';
5407
+ const iframe = createTestIframe(origin);
5408
+
5409
+ // Create a mock window that appears closed
5410
+ const mockContentWindow = {
5411
+ closed: true,
5412
+ postMessage: jest.fn()
5413
+ } as any;
5414
+ Object.defineProperty(iframe, 'contentWindow', {
5415
+ value: mockContentWindow,
5416
+ writable: true
5417
+ });
5418
+
5419
+ const client = requestIframeClient(iframe);
5420
+ expect(client.isAvailable()).toBe(false);
5421
+
5422
+ cleanupIframe(iframe);
5423
+ });
5424
+
5425
+ it('should reject client request when target window is closed', async () => {
5426
+ const origin = 'https://example.com';
5427
+ const iframe = createTestIframe(origin);
5428
+
5429
+ // Create a mock window that appears closed
5430
+ const mockContentWindow = {
5431
+ closed: true,
5432
+ document: null
5433
+ } as any;
5434
+ Object.defineProperty(iframe, 'contentWindow', {
5435
+ value: mockContentWindow,
5436
+ writable: true
5437
+ });
5438
+
5439
+ const client = requestIframeClient(iframe);
5440
+
5441
+ try {
5442
+ await client.send('test', { param: 'value' });
5443
+ throw new Error('Should have thrown');
5444
+ } catch (error: any) {
5445
+ expect(error.code).toBe(ErrorCode.TARGET_WINDOW_CLOSED);
5446
+ expect(error.message).toBe(Messages.TARGET_WINDOW_CLOSED);
5447
+ }
5448
+
5449
+ cleanupIframe(iframe);
5450
+ });
5451
+
5452
+ it('should reject client ping when target window is closed', async () => {
5453
+ const origin = 'https://example.com';
5454
+ const iframe = createTestIframe(origin);
5455
+
5456
+ // Create a mock window that appears closed
5457
+ const mockContentWindow = {
5458
+ closed: true,
5459
+ document: null
5460
+ } as any;
5461
+ Object.defineProperty(iframe, 'contentWindow', {
5462
+ value: mockContentWindow,
5463
+ writable: true
5464
+ });
5465
+
5466
+ const client = requestIframeClient(iframe);
5467
+
5468
+ try {
5469
+ await client.isConnect();
5470
+ throw new Error('Should have thrown');
5471
+ } catch (error: any) {
5472
+ expect(error.code).toBe(ErrorCode.TARGET_WINDOW_CLOSED);
5473
+ expect(error.message).toBe(Messages.TARGET_WINDOW_CLOSED);
5474
+ }
5475
+
5476
+ cleanupIframe(iframe);
5477
+ });
5478
+
5479
+ it('should throw error when server sends response to closed window', async () => {
5480
+ const origin = 'https://example.com';
5481
+ const iframe = createTestIframe(origin);
5482
+
5483
+ const mockContentWindow = {
5484
+ postMessage: jest.fn()
5485
+ };
5486
+ Object.defineProperty(iframe, 'contentWindow', {
5487
+ value: mockContentWindow,
5488
+ writable: true
5489
+ });
5490
+
5491
+ const server = requestIframeServer();
5492
+
5493
+ server.on('test', (req, res) => {
5494
+ // Simulate window being closed after request is received
5495
+ const closedWindow = {
5496
+ closed: true,
5497
+ document: null
5498
+ } as any;
5499
+ // Replace targetWindow in response object
5500
+ (res as any).targetWindow = closedWindow;
5501
+
5502
+ try {
5503
+ res.send({ result: 'success' });
5504
+ } catch (error: any) {
5505
+ expect(error.code).toBe(ErrorCode.TARGET_WINDOW_CLOSED);
5506
+ expect(error.message).toBe(Messages.TARGET_WINDOW_CLOSED);
5507
+ }
5508
+ });
5509
+
5510
+ // Send request
5511
+ window.dispatchEvent(
5512
+ new MessageEvent('message', {
5513
+ data: {
5514
+ __requestIframe__: 1,
5515
+ timestamp: Date.now(),
5516
+ type: 'request',
5517
+ requestId: 'req123',
5518
+ path: 'test',
5519
+ body: { param: 'value' },
5520
+ role: MessageRole.CLIENT,
5521
+ targetId: server.id
5522
+ },
5523
+ origin,
5524
+ source: mockContentWindow as any
5525
+ })
5526
+ );
5527
+
5528
+ await new Promise(resolve => setTimeout(resolve, 50));
5529
+
5530
+ server.destroy();
5531
+ cleanupIframe(iframe);
5532
+ });
5533
+
5534
+ it('should throw error when server sends stream to closed window', async () => {
5535
+ const origin = 'https://example.com';
5536
+ const iframe = createTestIframe(origin);
5537
+
5538
+ const mockContentWindow = {
5539
+ postMessage: jest.fn()
5540
+ };
5541
+ Object.defineProperty(iframe, 'contentWindow', {
5542
+ value: mockContentWindow,
5543
+ writable: true
5544
+ });
5545
+
5546
+ const server = requestIframeServer();
5547
+
5548
+ server.on('test', async (req, res) => {
5549
+ // Simulate window being closed after request is received
5550
+ const closedWindow = {
5551
+ closed: true,
5552
+ document: null
5553
+ } as any;
5554
+ // Replace targetWindow in response object
5555
+ (res as any).targetWindow = closedWindow;
5556
+
5557
+ const stream = new IframeWritableStream();
5558
+ try {
5559
+ await res.sendStream(stream);
5560
+ } catch (error: any) {
5561
+ expect(error.code).toBe(ErrorCode.TARGET_WINDOW_CLOSED);
5562
+ expect(error.message).toBe(Messages.TARGET_WINDOW_CLOSED);
5563
+ }
5564
+ });
5565
+
5566
+ // Send request
5567
+ window.dispatchEvent(
5568
+ new MessageEvent('message', {
5569
+ data: {
5570
+ __requestIframe__: 1,
5571
+ timestamp: Date.now(),
5572
+ type: 'request',
5573
+ requestId: 'req123',
5574
+ path: 'test',
5575
+ body: { param: 'value' },
5576
+ role: MessageRole.CLIENT,
5577
+ targetId: server.id
5578
+ },
5579
+ origin,
5580
+ source: mockContentWindow as any
5581
+ })
5582
+ );
5583
+
5584
+ await new Promise(resolve => setTimeout(resolve, 50));
5585
+
5586
+ server.destroy();
5587
+ cleanupIframe(iframe);
5588
+ });
5589
+ });
4434
5590
  });