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