request-iframe 0.0.1
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 +269 -0
- package/QUICKSTART.md +269 -0
- package/README.CN.md +1369 -0
- package/README.md +1016 -0
- package/library/__tests__/interceptors.test.ts +124 -0
- package/library/__tests__/requestIframe.test.ts +2216 -0
- package/library/__tests__/stream.test.ts +650 -0
- package/library/__tests__/utils.test.ts +433 -0
- package/library/api/client.d.ts +16 -0
- package/library/api/client.d.ts.map +1 -0
- package/library/api/client.js +72 -0
- package/library/api/server.d.ts +16 -0
- package/library/api/server.d.ts.map +1 -0
- package/library/api/server.js +44 -0
- package/library/constants/index.d.ts +209 -0
- package/library/constants/index.d.ts.map +1 -0
- package/library/constants/index.js +260 -0
- package/library/constants/messages.d.ts +80 -0
- package/library/constants/messages.d.ts.map +1 -0
- package/library/constants/messages.js +123 -0
- package/library/core/client.d.ts +99 -0
- package/library/core/client.d.ts.map +1 -0
- package/library/core/client.js +440 -0
- package/library/core/message-handler.d.ts +110 -0
- package/library/core/message-handler.d.ts.map +1 -0
- package/library/core/message-handler.js +320 -0
- package/library/core/request-response.d.ts +59 -0
- package/library/core/request-response.d.ts.map +1 -0
- package/library/core/request-response.js +337 -0
- package/library/core/request.d.ts +17 -0
- package/library/core/request.d.ts.map +1 -0
- package/library/core/request.js +34 -0
- package/library/core/response.d.ts +51 -0
- package/library/core/response.d.ts.map +1 -0
- package/library/core/response.js +323 -0
- package/library/core/server-base.d.ts +86 -0
- package/library/core/server-base.d.ts.map +1 -0
- package/library/core/server-base.js +257 -0
- package/library/core/server-client.d.ts +99 -0
- package/library/core/server-client.d.ts.map +1 -0
- package/library/core/server-client.js +256 -0
- package/library/core/server.d.ts +82 -0
- package/library/core/server.d.ts.map +1 -0
- package/library/core/server.js +338 -0
- package/library/index.d.ts +16 -0
- package/library/index.d.ts.map +1 -0
- package/library/index.js +211 -0
- package/library/interceptors/index.d.ts +41 -0
- package/library/interceptors/index.d.ts.map +1 -0
- package/library/interceptors/index.js +126 -0
- package/library/message/channel.d.ts +107 -0
- package/library/message/channel.d.ts.map +1 -0
- package/library/message/channel.js +184 -0
- package/library/message/dispatcher.d.ts +119 -0
- package/library/message/dispatcher.d.ts.map +1 -0
- package/library/message/dispatcher.js +249 -0
- package/library/message/index.d.ts +5 -0
- package/library/message/index.d.ts.map +1 -0
- package/library/message/index.js +25 -0
- package/library/stream/file-stream.d.ts +48 -0
- package/library/stream/file-stream.d.ts.map +1 -0
- package/library/stream/file-stream.js +240 -0
- package/library/stream/index.d.ts +15 -0
- package/library/stream/index.d.ts.map +1 -0
- package/library/stream/index.js +83 -0
- package/library/stream/readable-stream.d.ts +83 -0
- package/library/stream/readable-stream.d.ts.map +1 -0
- package/library/stream/readable-stream.js +249 -0
- package/library/stream/types.d.ts +165 -0
- package/library/stream/types.d.ts.map +1 -0
- package/library/stream/types.js +5 -0
- package/library/stream/writable-stream.d.ts +60 -0
- package/library/stream/writable-stream.d.ts.map +1 -0
- package/library/stream/writable-stream.js +348 -0
- package/library/types/index.d.ts +408 -0
- package/library/types/index.d.ts.map +1 -0
- package/library/types/index.js +5 -0
- package/library/utils/cache.d.ts +19 -0
- package/library/utils/cache.d.ts.map +1 -0
- package/library/utils/cache.js +83 -0
- package/library/utils/cookie.d.ts +117 -0
- package/library/utils/cookie.d.ts.map +1 -0
- package/library/utils/cookie.js +365 -0
- package/library/utils/debug.d.ts +11 -0
- package/library/utils/debug.d.ts.map +1 -0
- package/library/utils/debug.js +162 -0
- package/library/utils/index.d.ts +13 -0
- package/library/utils/index.d.ts.map +1 -0
- package/library/utils/index.js +132 -0
- package/library/utils/path-match.d.ts +17 -0
- package/library/utils/path-match.d.ts.map +1 -0
- package/library/utils/path-match.js +90 -0
- package/library/utils/protocol.d.ts +61 -0
- package/library/utils/protocol.d.ts.map +1 -0
- package/library/utils/protocol.js +169 -0
- package/package.json +58 -0
|
@@ -0,0 +1,650 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stream functionality tests
|
|
3
|
+
*/
|
|
4
|
+
import {
|
|
5
|
+
IframeWritableStream,
|
|
6
|
+
IframeReadableStream,
|
|
7
|
+
IframeFileWritableStream,
|
|
8
|
+
IframeFileReadableStream,
|
|
9
|
+
isIframeReadableStream,
|
|
10
|
+
isIframeFileStream,
|
|
11
|
+
StreamMessageHandler
|
|
12
|
+
} from '../stream';
|
|
13
|
+
|
|
14
|
+
describe('Stream', () => {
|
|
15
|
+
describe('IframeWritableStream', () => {
|
|
16
|
+
let mockTargetWindow: Window;
|
|
17
|
+
let mockPostMessage: jest.Mock;
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
mockPostMessage = jest.fn();
|
|
21
|
+
mockTargetWindow = {
|
|
22
|
+
postMessage: mockPostMessage
|
|
23
|
+
} as any;
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should create stream with default options', () => {
|
|
27
|
+
const stream = new IframeWritableStream();
|
|
28
|
+
|
|
29
|
+
expect(stream.streamId).toBeDefined();
|
|
30
|
+
expect(stream.type).toBe('data');
|
|
31
|
+
expect(stream.chunked).toBe(true);
|
|
32
|
+
expect(stream.state).toBe('pending');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should create stream with custom options', () => {
|
|
36
|
+
const stream = new IframeWritableStream({
|
|
37
|
+
type: 'file',
|
|
38
|
+
chunked: false,
|
|
39
|
+
metadata: { foo: 'bar' }
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
expect(stream.type).toBe('file');
|
|
43
|
+
expect(stream.chunked).toBe(false);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should throw error when starting without binding', async () => {
|
|
47
|
+
const stream = new IframeWritableStream();
|
|
48
|
+
|
|
49
|
+
await expect(stream.start()).rejects.toThrow();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should generate unique stream IDs', () => {
|
|
53
|
+
const stream1 = new IframeWritableStream();
|
|
54
|
+
const stream2 = new IframeWritableStream();
|
|
55
|
+
|
|
56
|
+
expect(stream1.streamId).not.toBe(stream2.streamId);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should start stream with iterator', async () => {
|
|
60
|
+
const stream = new IframeWritableStream({
|
|
61
|
+
iterator: async function* () {
|
|
62
|
+
yield 'chunk1';
|
|
63
|
+
yield 'chunk2';
|
|
64
|
+
yield 'chunk3';
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
stream._bind({
|
|
69
|
+
requestId: 'req-123',
|
|
70
|
+
targetWindow: mockTargetWindow,
|
|
71
|
+
targetOrigin: 'https://example.com',
|
|
72
|
+
secretKey: 'test'
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
await stream.start();
|
|
76
|
+
|
|
77
|
+
expect(stream.state).toBe('ended');
|
|
78
|
+
// start + 3 data chunks + end = 5 calls
|
|
79
|
+
expect(mockPostMessage).toHaveBeenCalledTimes(5);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should start stream with next function', async () => {
|
|
83
|
+
let callCount = 0;
|
|
84
|
+
const stream = new IframeWritableStream({
|
|
85
|
+
next: () => {
|
|
86
|
+
callCount++;
|
|
87
|
+
if (callCount === 1) {
|
|
88
|
+
return { data: 'chunk1', done: false };
|
|
89
|
+
} else if (callCount === 2) {
|
|
90
|
+
return { data: 'chunk2', done: false };
|
|
91
|
+
} else {
|
|
92
|
+
return { data: 'chunk3', done: true };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
stream._bind({
|
|
98
|
+
requestId: 'req-123',
|
|
99
|
+
targetWindow: mockTargetWindow,
|
|
100
|
+
targetOrigin: 'https://example.com',
|
|
101
|
+
secretKey: 'test'
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
await stream.start();
|
|
105
|
+
|
|
106
|
+
expect(stream.state).toBe('ended');
|
|
107
|
+
// start + 3 data chunks + end = 5 calls
|
|
108
|
+
expect(mockPostMessage).toHaveBeenCalledTimes(5);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('should start stream without data source', async () => {
|
|
112
|
+
const stream = new IframeWritableStream();
|
|
113
|
+
|
|
114
|
+
stream._bind({
|
|
115
|
+
requestId: 'req-123',
|
|
116
|
+
targetWindow: mockTargetWindow,
|
|
117
|
+
targetOrigin: 'https://example.com',
|
|
118
|
+
secretKey: 'test'
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
await stream.start();
|
|
122
|
+
|
|
123
|
+
expect(stream.state).toBe('ended');
|
|
124
|
+
expect(mockPostMessage).toHaveBeenCalledTimes(2); // start + end
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('should handle iterator error', async () => {
|
|
128
|
+
const stream = new IframeWritableStream({
|
|
129
|
+
iterator: async function* () {
|
|
130
|
+
yield 'chunk1';
|
|
131
|
+
throw new Error('Iterator error');
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
stream._bind({
|
|
136
|
+
requestId: 'req-123',
|
|
137
|
+
targetWindow: mockTargetWindow,
|
|
138
|
+
targetOrigin: 'https://example.com',
|
|
139
|
+
secretKey: 'test'
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
await stream.start();
|
|
143
|
+
|
|
144
|
+
expect(stream.state).toBe('error');
|
|
145
|
+
const errorCall = mockPostMessage.mock.calls.find((call: any[]) =>
|
|
146
|
+
call[0]?.type === 'stream_error'
|
|
147
|
+
);
|
|
148
|
+
expect(errorCall).toBeDefined();
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('should handle next function error', async () => {
|
|
152
|
+
const stream = new IframeWritableStream({
|
|
153
|
+
next: () => {
|
|
154
|
+
throw new Error('Next error');
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
stream._bind({
|
|
159
|
+
requestId: 'req-123',
|
|
160
|
+
targetWindow: mockTargetWindow,
|
|
161
|
+
targetOrigin: 'https://example.com',
|
|
162
|
+
secretKey: 'test'
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
await stream.start();
|
|
166
|
+
|
|
167
|
+
expect(stream.state).toBe('error');
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('should cancel stream', () => {
|
|
171
|
+
const stream = new IframeWritableStream();
|
|
172
|
+
|
|
173
|
+
stream._bind({
|
|
174
|
+
requestId: 'req-123',
|
|
175
|
+
targetWindow: mockTargetWindow,
|
|
176
|
+
targetOrigin: 'https://example.com',
|
|
177
|
+
secretKey: 'test'
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
stream.cancel('User cancelled');
|
|
181
|
+
|
|
182
|
+
expect(stream.state).toBe('cancelled');
|
|
183
|
+
const cancelCall = mockPostMessage.mock.calls.find((call: any[]) =>
|
|
184
|
+
call[0]?.type === 'stream_cancel'
|
|
185
|
+
);
|
|
186
|
+
expect(cancelCall).toBeDefined();
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('should not cancel if already ended', () => {
|
|
190
|
+
const stream = new IframeWritableStream();
|
|
191
|
+
stream._bind({
|
|
192
|
+
requestId: 'req-123',
|
|
193
|
+
targetWindow: mockTargetWindow,
|
|
194
|
+
targetOrigin: 'https://example.com',
|
|
195
|
+
secretKey: 'test'
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// Manually set state to ended (simulating already ended)
|
|
199
|
+
(stream as any)._state = 'ended';
|
|
200
|
+
mockPostMessage.mockClear();
|
|
201
|
+
|
|
202
|
+
stream.cancel('User cancelled');
|
|
203
|
+
|
|
204
|
+
expect(stream.state).toBe('ended');
|
|
205
|
+
expect(mockPostMessage).not.toHaveBeenCalled();
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('should use channel if provided', async () => {
|
|
209
|
+
const mockChannel = {
|
|
210
|
+
send: jest.fn()
|
|
211
|
+
} as any;
|
|
212
|
+
|
|
213
|
+
const stream = new IframeWritableStream();
|
|
214
|
+
|
|
215
|
+
stream._bind({
|
|
216
|
+
requestId: 'req-123',
|
|
217
|
+
targetWindow: mockTargetWindow,
|
|
218
|
+
targetOrigin: 'https://example.com',
|
|
219
|
+
secretKey: 'test',
|
|
220
|
+
channel: mockChannel
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
await stream.start();
|
|
224
|
+
|
|
225
|
+
expect(mockChannel.send).toHaveBeenCalled();
|
|
226
|
+
expect(mockPostMessage).not.toHaveBeenCalled();
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('should handle stream cancellation before start', () => {
|
|
230
|
+
const stream = new IframeWritableStream({
|
|
231
|
+
iterator: async function* () {
|
|
232
|
+
yield 'chunk1';
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
stream._bind({
|
|
237
|
+
requestId: 'req-123',
|
|
238
|
+
targetWindow: mockTargetWindow,
|
|
239
|
+
targetOrigin: 'https://example.com',
|
|
240
|
+
secretKey: 'test'
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
stream.cancel('User cancelled');
|
|
244
|
+
|
|
245
|
+
expect(stream.state).toBe('cancelled');
|
|
246
|
+
const cancelCall = mockPostMessage.mock.calls.find(
|
|
247
|
+
(call: any[]) => call[0]?.type === 'stream_cancel'
|
|
248
|
+
);
|
|
249
|
+
expect(cancelCall).toBeDefined();
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('should handle stream error during iteration', async () => {
|
|
253
|
+
const stream = new IframeWritableStream({
|
|
254
|
+
iterator: async function* () {
|
|
255
|
+
yield 'chunk1';
|
|
256
|
+
throw new Error('Stream error');
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
stream._bind({
|
|
261
|
+
requestId: 'req-123',
|
|
262
|
+
targetWindow: mockTargetWindow,
|
|
263
|
+
targetOrigin: 'https://example.com',
|
|
264
|
+
secretKey: 'test'
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
await stream.start();
|
|
268
|
+
|
|
269
|
+
expect(stream.state).toBe('error');
|
|
270
|
+
const errorCall = mockPostMessage.mock.calls.find(
|
|
271
|
+
(call: any[]) => call[0]?.type === 'stream_error'
|
|
272
|
+
);
|
|
273
|
+
expect(errorCall).toBeDefined();
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it('should not end if already cancelled', async () => {
|
|
277
|
+
const stream = new IframeWritableStream({
|
|
278
|
+
iterator: async function* () {
|
|
279
|
+
yield 'chunk1';
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
stream._bind({
|
|
284
|
+
requestId: 'req-123',
|
|
285
|
+
targetWindow: mockTargetWindow,
|
|
286
|
+
targetOrigin: 'https://example.com',
|
|
287
|
+
secretKey: 'test'
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
const startPromise = stream.start();
|
|
291
|
+
stream.cancel('Cancelled');
|
|
292
|
+
await startPromise;
|
|
293
|
+
|
|
294
|
+
expect(stream.state).toBe('cancelled');
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
describe('IframeReadableStream', () => {
|
|
299
|
+
let mockHandler: StreamMessageHandler;
|
|
300
|
+
let registeredHandlers: Map<string, (data: any) => void>;
|
|
301
|
+
|
|
302
|
+
beforeEach(() => {
|
|
303
|
+
registeredHandlers = new Map();
|
|
304
|
+
mockHandler = {
|
|
305
|
+
registerStreamHandler: jest.fn((streamId, handler) => {
|
|
306
|
+
registeredHandlers.set(streamId, handler);
|
|
307
|
+
}),
|
|
308
|
+
unregisterStreamHandler: jest.fn((streamId) => {
|
|
309
|
+
registeredHandlers.delete(streamId);
|
|
310
|
+
}),
|
|
311
|
+
postMessage: jest.fn()
|
|
312
|
+
};
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it('should create readable stream', () => {
|
|
316
|
+
const stream = new IframeReadableStream(
|
|
317
|
+
'test-stream-id',
|
|
318
|
+
'test-request-id',
|
|
319
|
+
mockHandler,
|
|
320
|
+
{ type: 'data', chunked: true }
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
expect(stream.streamId).toBe('test-stream-id');
|
|
324
|
+
expect(stream.type).toBe('data');
|
|
325
|
+
expect(stream.chunked).toBe(true);
|
|
326
|
+
expect(stream.state).toBe('pending');
|
|
327
|
+
expect(mockHandler.registerStreamHandler).toHaveBeenCalledWith(
|
|
328
|
+
'test-stream-id',
|
|
329
|
+
expect.any(Function)
|
|
330
|
+
);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it('should handle stream data', async () => {
|
|
334
|
+
const stream = new IframeReadableStream<string>(
|
|
335
|
+
'test-stream-id',
|
|
336
|
+
'test-request-id',
|
|
337
|
+
mockHandler
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
const handler = registeredHandlers.get('test-stream-id');
|
|
341
|
+
expect(handler).toBeDefined();
|
|
342
|
+
|
|
343
|
+
// Send data
|
|
344
|
+
handler!({ type: 'data', streamId: 'test-stream-id', data: 'chunk1' });
|
|
345
|
+
handler!({ type: 'data', streamId: 'test-stream-id', data: 'chunk2', done: true });
|
|
346
|
+
|
|
347
|
+
// Read data
|
|
348
|
+
const result = await stream.read();
|
|
349
|
+
expect(result).toEqual(['chunk1', 'chunk2']);
|
|
350
|
+
expect(stream.state).toBe('ended');
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it('should handle stream end', async () => {
|
|
354
|
+
const stream = new IframeReadableStream<string>(
|
|
355
|
+
'test-stream-id',
|
|
356
|
+
'test-request-id',
|
|
357
|
+
mockHandler
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
const handler = registeredHandlers.get('test-stream-id');
|
|
361
|
+
|
|
362
|
+
const onEndCallback = jest.fn();
|
|
363
|
+
stream.onEnd(onEndCallback);
|
|
364
|
+
|
|
365
|
+
handler!({ type: 'data', streamId: 'test-stream-id', data: 'test' });
|
|
366
|
+
handler!({ type: 'end', streamId: 'test-stream-id' });
|
|
367
|
+
|
|
368
|
+
await stream.read();
|
|
369
|
+
expect(onEndCallback).toHaveBeenCalled();
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
it('should handle stream error', async () => {
|
|
373
|
+
const stream = new IframeReadableStream<string>(
|
|
374
|
+
'test-stream-id',
|
|
375
|
+
'test-request-id',
|
|
376
|
+
mockHandler
|
|
377
|
+
);
|
|
378
|
+
|
|
379
|
+
const handler = registeredHandlers.get('test-stream-id');
|
|
380
|
+
|
|
381
|
+
const onErrorCallback = jest.fn();
|
|
382
|
+
stream.onError(onErrorCallback);
|
|
383
|
+
|
|
384
|
+
handler!({ type: 'error', streamId: 'test-stream-id', error: 'Test error' });
|
|
385
|
+
|
|
386
|
+
await expect(stream.read()).rejects.toThrow();
|
|
387
|
+
expect(onErrorCallback).toHaveBeenCalled();
|
|
388
|
+
expect(stream.state).toBe('error');
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it('should support async iterator', async () => {
|
|
392
|
+
const stream = new IframeReadableStream<string>(
|
|
393
|
+
'test-stream-id',
|
|
394
|
+
'test-request-id',
|
|
395
|
+
mockHandler
|
|
396
|
+
);
|
|
397
|
+
|
|
398
|
+
const handler = registeredHandlers.get('test-stream-id');
|
|
399
|
+
|
|
400
|
+
// Simulate async data sending
|
|
401
|
+
setTimeout(() => {
|
|
402
|
+
handler!({ type: 'data', streamId: 'test-stream-id', data: 'a' });
|
|
403
|
+
handler!({ type: 'data', streamId: 'test-stream-id', data: 'b' });
|
|
404
|
+
handler!({ type: 'end', streamId: 'test-stream-id' });
|
|
405
|
+
}, 10);
|
|
406
|
+
|
|
407
|
+
const chunks: string[] = [];
|
|
408
|
+
for await (const chunk of stream) {
|
|
409
|
+
chunks.push(chunk);
|
|
410
|
+
if (chunks.length >= 2) break; // Prevent infinite wait
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
expect(chunks).toEqual(['a', 'b']);
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
it('should cancel stream', () => {
|
|
417
|
+
const stream = new IframeReadableStream<string>(
|
|
418
|
+
'test-stream-id',
|
|
419
|
+
'test-request-id',
|
|
420
|
+
mockHandler
|
|
421
|
+
);
|
|
422
|
+
|
|
423
|
+
stream.cancel('User cancelled');
|
|
424
|
+
|
|
425
|
+
expect(stream.state).toBe('cancelled');
|
|
426
|
+
expect(mockHandler.postMessage).toHaveBeenCalled();
|
|
427
|
+
expect(mockHandler.unregisterStreamHandler).toHaveBeenCalledWith('test-stream-id');
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
it('should not cancel if already ended', () => {
|
|
431
|
+
const stream = new IframeReadableStream<string>(
|
|
432
|
+
'test-stream-id',
|
|
433
|
+
'test-request-id',
|
|
434
|
+
mockHandler
|
|
435
|
+
);
|
|
436
|
+
|
|
437
|
+
const handler = registeredHandlers.get('test-stream-id');
|
|
438
|
+
handler!({ type: 'end', streamId: 'test-stream-id' });
|
|
439
|
+
|
|
440
|
+
jest.clearAllMocks();
|
|
441
|
+
stream.cancel('User cancelled');
|
|
442
|
+
|
|
443
|
+
expect(stream.state).toBe('ended');
|
|
444
|
+
expect(mockHandler.postMessage).not.toHaveBeenCalled();
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
it('should not cancel if already in error state', () => {
|
|
448
|
+
const stream = new IframeReadableStream<string>(
|
|
449
|
+
'test-stream-id',
|
|
450
|
+
'test-request-id',
|
|
451
|
+
mockHandler
|
|
452
|
+
);
|
|
453
|
+
|
|
454
|
+
const handler = registeredHandlers.get('test-stream-id');
|
|
455
|
+
handler!({ type: 'error', streamId: 'test-stream-id', error: 'Error' });
|
|
456
|
+
|
|
457
|
+
jest.clearAllMocks();
|
|
458
|
+
stream.cancel('User cancelled');
|
|
459
|
+
|
|
460
|
+
expect(stream.state).toBe('error');
|
|
461
|
+
expect(mockHandler.postMessage).not.toHaveBeenCalled();
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
it('should handle stream data with done flag', async () => {
|
|
465
|
+
const stream = new IframeReadableStream<string>(
|
|
466
|
+
'test-stream-id',
|
|
467
|
+
'test-request-id',
|
|
468
|
+
mockHandler
|
|
469
|
+
);
|
|
470
|
+
|
|
471
|
+
const handler = registeredHandlers.get('test-stream-id');
|
|
472
|
+
handler!({ type: 'data', streamId: 'test-stream-id', data: 'chunk1', done: true });
|
|
473
|
+
|
|
474
|
+
const result = await stream.read();
|
|
475
|
+
expect(result).toBe('chunk1');
|
|
476
|
+
expect(stream.state).toBe('ended');
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
it('should handle onEnd callback when stream already ended', () => {
|
|
480
|
+
const stream = new IframeReadableStream<string>(
|
|
481
|
+
'test-stream-id',
|
|
482
|
+
'test-request-id',
|
|
483
|
+
mockHandler
|
|
484
|
+
);
|
|
485
|
+
|
|
486
|
+
const handler = registeredHandlers.get('test-stream-id');
|
|
487
|
+
handler!({ type: 'end', streamId: 'test-stream-id' });
|
|
488
|
+
|
|
489
|
+
const callback = jest.fn();
|
|
490
|
+
stream.onEnd(callback);
|
|
491
|
+
expect(callback).toHaveBeenCalled();
|
|
492
|
+
});
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
describe('IframeFileWritableStream', () => {
|
|
496
|
+
it('should create file stream with filename', () => {
|
|
497
|
+
const stream = new IframeFileWritableStream({
|
|
498
|
+
filename: 'test.txt',
|
|
499
|
+
mimeType: 'text/plain',
|
|
500
|
+
size: 1024
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
expect(stream.type).toBe('file');
|
|
504
|
+
expect(stream.filename).toBe('test.txt');
|
|
505
|
+
expect(stream.mimeType).toBe('text/plain');
|
|
506
|
+
expect(stream.size).toBe(1024);
|
|
507
|
+
});
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
describe('IframeFileReadableStream', () => {
|
|
511
|
+
let mockHandler: StreamMessageHandler;
|
|
512
|
+
let registeredHandlers: Map<string, (data: any) => void>;
|
|
513
|
+
|
|
514
|
+
beforeEach(() => {
|
|
515
|
+
registeredHandlers = new Map();
|
|
516
|
+
mockHandler = {
|
|
517
|
+
registerStreamHandler: jest.fn((streamId, handler) => {
|
|
518
|
+
registeredHandlers.set(streamId, handler);
|
|
519
|
+
}),
|
|
520
|
+
unregisterStreamHandler: jest.fn((streamId) => {
|
|
521
|
+
registeredHandlers.delete(streamId);
|
|
522
|
+
}),
|
|
523
|
+
postMessage: jest.fn()
|
|
524
|
+
};
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
it('should create file readable stream', () => {
|
|
528
|
+
const stream = new IframeFileReadableStream(
|
|
529
|
+
'test-stream-id',
|
|
530
|
+
'test-request-id',
|
|
531
|
+
mockHandler,
|
|
532
|
+
{
|
|
533
|
+
filename: 'test.txt',
|
|
534
|
+
mimeType: 'text/plain',
|
|
535
|
+
size: 100
|
|
536
|
+
}
|
|
537
|
+
);
|
|
538
|
+
|
|
539
|
+
expect(stream.type).toBe('file');
|
|
540
|
+
expect(stream.filename).toBe('test.txt');
|
|
541
|
+
expect(stream.mimeType).toBe('text/plain');
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
it('should decode base64 data', async () => {
|
|
545
|
+
const stream = new IframeFileReadableStream(
|
|
546
|
+
'test-stream-id',
|
|
547
|
+
'test-request-id',
|
|
548
|
+
mockHandler
|
|
549
|
+
);
|
|
550
|
+
|
|
551
|
+
const handler = registeredHandlers.get('test-stream-id');
|
|
552
|
+
|
|
553
|
+
// Send base64 encoded data
|
|
554
|
+
const testData = 'Hello, World!';
|
|
555
|
+
const base64Data = btoa(testData);
|
|
556
|
+
handler!({ type: 'data', streamId: 'test-stream-id', data: base64Data, done: true });
|
|
557
|
+
|
|
558
|
+
const result = await stream.read();
|
|
559
|
+
expect(result).toBeInstanceOf(Uint8Array);
|
|
560
|
+
|
|
561
|
+
// Decode to verify content (manually convert Uint8Array to string)
|
|
562
|
+
let decoded = '';
|
|
563
|
+
for (let i = 0; i < result.length; i++) {
|
|
564
|
+
decoded += String.fromCharCode(result[i]);
|
|
565
|
+
}
|
|
566
|
+
expect(decoded).toBe(testData);
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
it('should read as Blob', async () => {
|
|
570
|
+
const stream = new IframeFileReadableStream(
|
|
571
|
+
'test-stream-id',
|
|
572
|
+
'test-request-id',
|
|
573
|
+
mockHandler,
|
|
574
|
+
{ mimeType: 'text/plain' }
|
|
575
|
+
);
|
|
576
|
+
|
|
577
|
+
const handler = registeredHandlers.get('test-stream-id');
|
|
578
|
+
const base64Data = btoa('test');
|
|
579
|
+
handler!({ type: 'data', streamId: 'test-stream-id', data: base64Data, done: true });
|
|
580
|
+
|
|
581
|
+
const blob = await stream.readAsBlob();
|
|
582
|
+
expect(blob).toBeInstanceOf(Blob);
|
|
583
|
+
expect(blob.type).toBe('text/plain');
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
it('should read as ArrayBuffer', async () => {
|
|
587
|
+
const stream = new IframeFileReadableStream(
|
|
588
|
+
'test-stream-id',
|
|
589
|
+
'test-request-id',
|
|
590
|
+
mockHandler
|
|
591
|
+
);
|
|
592
|
+
|
|
593
|
+
const handler = registeredHandlers.get('test-stream-id');
|
|
594
|
+
const base64Data = btoa('test');
|
|
595
|
+
handler!({ type: 'data', streamId: 'test-stream-id', data: base64Data, done: true });
|
|
596
|
+
|
|
597
|
+
const buffer = await stream.readAsArrayBuffer();
|
|
598
|
+
expect(buffer).toBeInstanceOf(ArrayBuffer);
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
it('should read as Data URL', async () => {
|
|
602
|
+
const stream = new IframeFileReadableStream(
|
|
603
|
+
'test-stream-id',
|
|
604
|
+
'test-request-id',
|
|
605
|
+
mockHandler,
|
|
606
|
+
{ mimeType: 'text/plain' }
|
|
607
|
+
);
|
|
608
|
+
|
|
609
|
+
const handler = registeredHandlers.get('test-stream-id');
|
|
610
|
+
const base64Data = btoa('test');
|
|
611
|
+
handler!({ type: 'data', streamId: 'test-stream-id', data: base64Data, done: true });
|
|
612
|
+
|
|
613
|
+
const dataUrl = await stream.readAsDataURL();
|
|
614
|
+
expect(dataUrl).toMatch(/^data:text\/plain;base64,/);
|
|
615
|
+
});
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
describe('Type guards', () => {
|
|
619
|
+
let mockHandler: StreamMessageHandler;
|
|
620
|
+
|
|
621
|
+
beforeEach(() => {
|
|
622
|
+
mockHandler = {
|
|
623
|
+
registerStreamHandler: jest.fn(),
|
|
624
|
+
unregisterStreamHandler: jest.fn(),
|
|
625
|
+
postMessage: jest.fn()
|
|
626
|
+
};
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
it('isIframeReadableStream should return true for IframeReadableStream', () => {
|
|
630
|
+
const stream = new IframeReadableStream('id', 'reqId', mockHandler);
|
|
631
|
+
expect(isIframeReadableStream(stream)).toBe(true);
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
it('isIframeReadableStream should return false for non-stream objects', () => {
|
|
635
|
+
expect(isIframeReadableStream({})).toBe(false);
|
|
636
|
+
expect(isIframeReadableStream(null)).toBe(false);
|
|
637
|
+
expect(isIframeReadableStream('string')).toBe(false);
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
it('isIframeFileStream should return true for IframeFileReadableStream', () => {
|
|
641
|
+
const stream = new IframeFileReadableStream('id', 'reqId', mockHandler);
|
|
642
|
+
expect(isIframeFileStream(stream)).toBe(true);
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
it('isIframeFileStream should return false for regular IframeReadableStream', () => {
|
|
646
|
+
const stream = new IframeReadableStream('id', 'reqId', mockHandler);
|
|
647
|
+
expect(isIframeFileStream(stream)).toBe(false);
|
|
648
|
+
});
|
|
649
|
+
});
|
|
650
|
+
});
|