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.
- package/QUICKSTART.CN.md +35 -8
- package/QUICKSTART.md +35 -8
- package/README.CN.md +177 -24
- package/README.md +237 -19
- package/library/__tests__/channel.test.ts +16 -4
- package/library/__tests__/coverage-branches.test.ts +356 -0
- package/library/__tests__/debug.test.ts +22 -0
- package/library/__tests__/dispatcher.test.ts +8 -4
- package/library/__tests__/requestIframe.test.ts +1243 -87
- package/library/__tests__/stream.test.ts +92 -16
- package/library/__tests__/utils.test.ts +41 -1
- package/library/api/client.d.ts.map +1 -1
- package/library/api/client.js +1 -0
- package/library/constants/index.d.ts +2 -0
- package/library/constants/index.d.ts.map +1 -1
- package/library/constants/index.js +3 -1
- package/library/constants/messages.d.ts +3 -0
- package/library/constants/messages.d.ts.map +1 -1
- package/library/constants/messages.js +3 -0
- package/library/core/client-server.d.ts +4 -0
- package/library/core/client-server.d.ts.map +1 -1
- package/library/core/client-server.js +45 -22
- package/library/core/client.d.ts +36 -4
- package/library/core/client.d.ts.map +1 -1
- package/library/core/client.js +508 -285
- package/library/core/request.d.ts +3 -1
- package/library/core/request.d.ts.map +1 -1
- package/library/core/request.js +2 -1
- package/library/core/response.d.ts +26 -4
- package/library/core/response.d.ts.map +1 -1
- package/library/core/response.js +192 -112
- package/library/core/server.d.ts +13 -0
- package/library/core/server.d.ts.map +1 -1
- package/library/core/server.js +221 -6
- package/library/index.d.ts +2 -1
- package/library/index.d.ts.map +1 -1
- package/library/index.js +39 -3
- package/library/message/channel.d.ts +2 -2
- package/library/message/channel.d.ts.map +1 -1
- package/library/message/channel.js +5 -1
- package/library/message/dispatcher.d.ts +2 -2
- package/library/message/dispatcher.d.ts.map +1 -1
- package/library/message/dispatcher.js +6 -5
- package/library/stream/index.d.ts +11 -1
- package/library/stream/index.d.ts.map +1 -1
- package/library/stream/index.js +21 -3
- package/library/stream/types.d.ts +2 -2
- package/library/stream/types.d.ts.map +1 -1
- package/library/stream/writable-stream.d.ts +1 -1
- package/library/stream/writable-stream.d.ts.map +1 -1
- package/library/stream/writable-stream.js +87 -47
- package/library/types/index.d.ts +29 -5
- package/library/types/index.d.ts.map +1 -1
- package/library/utils/debug.d.ts.map +1 -1
- package/library/utils/debug.js +6 -2
- package/library/utils/error.d.ts +21 -0
- package/library/utils/error.d.ts.map +1 -0
- package/library/utils/error.js +34 -0
- package/library/utils/index.d.ts +21 -0
- package/library/utils/index.d.ts.map +1 -1
- package/library/utils/index.js +141 -2
- package/library/utils/path-match.d.ts +16 -0
- package/library/utils/path-match.d.ts.map +1 -1
- package/library/utils/path-match.js +65 -0
- package/package.json +2 -1
- package/react/library/__tests__/index.test.tsx +44 -22
- package/react/library/index.d.ts.map +1 -1
- package/react/library/index.js +81 -23
- 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 (
|
|
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.
|
|
2026
|
-
|
|
2027
|
-
|
|
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
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
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
|
|
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
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
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
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
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
|
|
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
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
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
|
-
|
|
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
|
});
|