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.
Files changed (84) hide show
  1. package/QUICKSTART.CN.md +35 -8
  2. package/QUICKSTART.md +35 -8
  3. package/README.CN.md +439 -36
  4. package/README.md +496 -30
  5. package/library/__tests__/channel.test.ts +420 -0
  6. package/library/__tests__/coverage-branches.test.ts +356 -0
  7. package/library/__tests__/debug.test.ts +588 -0
  8. package/library/__tests__/dispatcher.test.ts +481 -0
  9. package/library/__tests__/requestIframe.test.ts +3163 -185
  10. package/library/__tests__/server.test.ts +738 -0
  11. package/library/__tests__/stream.test.ts +46 -15
  12. package/library/api/client.d.ts.map +1 -1
  13. package/library/api/client.js +12 -6
  14. package/library/api/server.d.ts +4 -3
  15. package/library/api/server.d.ts.map +1 -1
  16. package/library/api/server.js +25 -7
  17. package/library/constants/index.d.ts +14 -4
  18. package/library/constants/index.d.ts.map +1 -1
  19. package/library/constants/index.js +15 -7
  20. package/library/constants/messages.d.ts +37 -0
  21. package/library/constants/messages.d.ts.map +1 -1
  22. package/library/constants/messages.js +38 -1
  23. package/library/core/client-server.d.ts +105 -0
  24. package/library/core/client-server.d.ts.map +1 -0
  25. package/library/core/client-server.js +289 -0
  26. package/library/core/client.d.ts +53 -10
  27. package/library/core/client.d.ts.map +1 -1
  28. package/library/core/client.js +529 -207
  29. package/library/core/request.d.ts +3 -1
  30. package/library/core/request.d.ts.map +1 -1
  31. package/library/core/request.js +2 -1
  32. package/library/core/response.d.ts +30 -4
  33. package/library/core/response.d.ts.map +1 -1
  34. package/library/core/response.js +176 -100
  35. package/library/core/server-client.d.ts +3 -1
  36. package/library/core/server-client.d.ts.map +1 -1
  37. package/library/core/server-client.js +19 -9
  38. package/library/core/server.d.ts +22 -1
  39. package/library/core/server.d.ts.map +1 -1
  40. package/library/core/server.js +304 -55
  41. package/library/index.d.ts +3 -2
  42. package/library/index.d.ts.map +1 -1
  43. package/library/index.js +34 -5
  44. package/library/interceptors/index.d.ts.map +1 -1
  45. package/library/message/channel.d.ts +3 -1
  46. package/library/message/channel.d.ts.map +1 -1
  47. package/library/message/dispatcher.d.ts +7 -2
  48. package/library/message/dispatcher.d.ts.map +1 -1
  49. package/library/message/dispatcher.js +48 -2
  50. package/library/message/index.d.ts.map +1 -1
  51. package/library/stream/file-stream.d.ts +5 -0
  52. package/library/stream/file-stream.d.ts.map +1 -1
  53. package/library/stream/file-stream.js +41 -12
  54. package/library/stream/index.d.ts +11 -1
  55. package/library/stream/index.d.ts.map +1 -1
  56. package/library/stream/index.js +21 -3
  57. package/library/stream/readable-stream.d.ts.map +1 -1
  58. package/library/stream/readable-stream.js +32 -30
  59. package/library/stream/types.d.ts +20 -2
  60. package/library/stream/types.d.ts.map +1 -1
  61. package/library/stream/writable-stream.d.ts +2 -1
  62. package/library/stream/writable-stream.d.ts.map +1 -1
  63. package/library/stream/writable-stream.js +13 -10
  64. package/library/types/index.d.ts +106 -32
  65. package/library/types/index.d.ts.map +1 -1
  66. package/library/utils/cache.d.ts +24 -0
  67. package/library/utils/cache.d.ts.map +1 -1
  68. package/library/utils/cache.js +76 -0
  69. package/library/utils/cookie.d.ts.map +1 -1
  70. package/library/utils/debug.d.ts.map +1 -1
  71. package/library/utils/debug.js +382 -20
  72. package/library/utils/index.d.ts +19 -0
  73. package/library/utils/index.d.ts.map +1 -1
  74. package/library/utils/index.js +113 -2
  75. package/library/utils/path-match.d.ts +16 -0
  76. package/library/utils/path-match.d.ts.map +1 -1
  77. package/library/utils/path-match.js +65 -0
  78. package/library/utils/protocol.d.ts.map +1 -1
  79. package/package.json +4 -1
  80. package/react/library/__tests__/index.test.tsx +274 -281
  81. package/react/library/index.d.ts +4 -3
  82. package/react/library/index.d.ts.map +1 -1
  83. package/react/library/index.js +225 -158
  84. package/react/package.json +7 -0
@@ -0,0 +1,356 @@
1
+ import { requestIframeClient } from '../api/client';
2
+ import { requestIframeServer } from '../api/server';
3
+ import { clearServerCache } from '../utils/cache';
4
+ import { ErrorCode, getStatusText, Messages } from '../constants';
5
+ import {
6
+ InterceptorManager,
7
+ RequestInterceptorManager,
8
+ ResponseInterceptorManager,
9
+ runRequestInterceptors,
10
+ runResponseInterceptors
11
+ } from '../interceptors';
12
+ import { ServerResponseImpl } from '../core/response';
13
+ import { IframeFileReadableStream, IframeFileWritableStream } from '../stream';
14
+ import { setupClientDebugInterceptors, setupServerDebugListeners } from '../utils/debug';
15
+
16
+ jest.mock('../utils/debug', () => {
17
+ const actual = jest.requireActual('../utils/debug');
18
+ return {
19
+ ...actual,
20
+ setupClientDebugInterceptors: jest.fn(actual.setupClientDebugInterceptors),
21
+ setupServerDebugListeners: jest.fn(actual.setupServerDebugListeners)
22
+ };
23
+ });
24
+
25
+ describe('Coverage - branch focused tests', () => {
26
+ beforeEach(() => {
27
+ (setupClientDebugInterceptors as unknown as jest.Mock).mockClear();
28
+ (setupServerDebugListeners as unknown as jest.Mock).mockClear();
29
+ clearServerCache();
30
+ });
31
+
32
+ describe('src/api/client.ts', () => {
33
+ it('should create client with Window target', () => {
34
+ const client = requestIframeClient(window as any);
35
+ expect(client).toBeDefined();
36
+ expect((client as any).targetWindow || (client as any).targetOrigin).toBeDefined();
37
+ });
38
+
39
+ it('should throw IFRAME_NOT_READY when iframe.contentWindow is unavailable', () => {
40
+ const iframe = document.createElement('iframe');
41
+ Object.defineProperty(iframe, 'contentWindow', { value: null, writable: true });
42
+ try {
43
+ requestIframeClient(iframe as any);
44
+ throw new Error('should have thrown');
45
+ } catch (e: any) {
46
+ expect(e).toBeDefined();
47
+ expect(e.code).toBe(ErrorCode.IFRAME_NOT_READY);
48
+ }
49
+ });
50
+
51
+ it('should enable trace mode and register debug interceptors', () => {
52
+ const client = requestIframeClient(window as any, { trace: true } as any);
53
+ expect(client).toBeDefined();
54
+ expect(setupClientDebugInterceptors).toHaveBeenCalledTimes(1);
55
+ });
56
+ });
57
+
58
+ describe('src/api/server.ts', () => {
59
+ it('should cache server when id is provided', () => {
60
+ const s1 = requestIframeServer({ id: 'server-1' } as any);
61
+ const s2 = requestIframeServer({ id: 'server-1' } as any);
62
+ expect(s1).toBe(s2);
63
+ });
64
+
65
+ it('should enable trace mode and register server debug listeners', () => {
66
+ const s = requestIframeServer({ id: 'server-trace', trace: true } as any);
67
+ expect(s).toBeDefined();
68
+ expect(setupServerDebugListeners).toHaveBeenCalledTimes(1);
69
+ });
70
+ });
71
+
72
+ describe('src/constants/index.ts + messages.ts', () => {
73
+ it('getStatusText should return Unknown for unknown code', () => {
74
+ expect(getStatusText(999)).toBe('Unknown');
75
+ });
76
+
77
+ it('Messages proxy should return key when missing', () => {
78
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
79
+ expect((Messages as any).SOME_UNKNOWN_KEY).toBe('SOME_UNKNOWN_KEY');
80
+ });
81
+
82
+ it('formatMessage should keep placeholder when args missing', () => {
83
+ // {1} has no arg, should remain as "{1}"
84
+ expect(Messages.PROTOCOL_VERSION_TOO_LOW.replace('{0}', '0')).toContain('{1}');
85
+ });
86
+ });
87
+
88
+ describe('src/interceptors/index.ts', () => {
89
+ it('runRequestInterceptors should use rejected handler when provided', async () => {
90
+ const m = new RequestInterceptorManager();
91
+ m.use(() => {
92
+ throw new Error('boom');
93
+ });
94
+ m.use(
95
+ (cfg) => cfg,
96
+ (err) => ({ path: 'recovered', body: { message: err.message } } as any)
97
+ );
98
+ const out = await runRequestInterceptors(m, { path: 'x' } as any);
99
+ expect(out.path).toBe('recovered');
100
+ expect((out as any).body.message).toBe('boom');
101
+ });
102
+
103
+ it('runResponseInterceptors should reject when rejected handler is not provided', async () => {
104
+ const m = new ResponseInterceptorManager();
105
+ m.use(() => {
106
+ throw new Error('nope');
107
+ });
108
+ await expect(runResponseInterceptors(m, { data: 1 } as any)).rejects.toBeInstanceOf(Error);
109
+ });
110
+
111
+ it('InterceptorManager.eject should null out handler, and forEach should skip nulls', () => {
112
+ const mgr = new InterceptorManager<any>();
113
+ const id = mgr.use((x) => x);
114
+ mgr.eject(id);
115
+ const seen: any[] = [];
116
+ mgr.forEach((h) => seen.push(h));
117
+ expect(seen.length).toBe(0);
118
+ });
119
+ });
120
+
121
+ describe('src/core/response.ts (setHeader/cookie branches)', () => {
122
+ function createRes() {
123
+ const channel = { send: jest.fn() } as any;
124
+ const targetWindow = { postMessage: jest.fn() } as any;
125
+ return { res: new ServerResponseImpl('rid', '/p', undefined, targetWindow, '*', channel), channel };
126
+ }
127
+
128
+ it('setHeader should merge Set-Cookie arrays and strings', () => {
129
+ const { res } = createRes();
130
+ res.setHeader('Set-Cookie', 'a=1');
131
+ res.setHeader('Set-Cookie', ['b=2', 'c=3']);
132
+ res.setHeader('Set-Cookie', 'd=4');
133
+ const sc = (res.headers['Set-Cookie'] as string[]) || [];
134
+ expect(sc).toEqual(['a=1', 'b=2', 'c=3', 'd=4']);
135
+ });
136
+
137
+ it('setHeader should join non-Set-Cookie array values', () => {
138
+ const { res } = createRes();
139
+ res.setHeader('X-Test', ['a', 'b']);
140
+ expect(res.headers['X-Test']).toBe('a, b');
141
+ });
142
+
143
+ it('cookie should handle sameSite true/false/string branches', () => {
144
+ const { res } = createRes();
145
+ res.cookie('k1', 'v1', { sameSite: true });
146
+ res.cookie('k2', 'v2', { sameSite: false });
147
+ res.cookie('k3', 'v3', { sameSite: 'Lax' as any });
148
+ expect(Array.isArray(res.headers['Set-Cookie'])).toBe(true);
149
+ expect((res.headers['Set-Cookie'] as string[]).length).toBe(3);
150
+ });
151
+
152
+ it('clearCookie should append Set-Cookie', () => {
153
+ const { res } = createRes();
154
+ res.clearCookie('k', { path: '/' });
155
+ expect(Array.isArray(res.headers['Set-Cookie'])).toBe(true);
156
+ });
157
+ });
158
+
159
+ describe('src/stream/file-stream.ts (encode/decode/merge branches)', () => {
160
+ it('IframeFileWritableStream.encodeData should handle ArrayBuffer and other types', () => {
161
+ const ws = new IframeFileWritableStream({
162
+ filename: 'f',
163
+ mimeType: 'text/plain',
164
+ next: async () => ({ data: 'Zg==', done: true })
165
+ });
166
+ const ab = new Uint8Array([1, 2, 3]).buffer;
167
+ expect((ws as any).encodeData(ab)).toBeDefined();
168
+ expect((ws as any).encodeData(123)).toBe('123');
169
+ });
170
+
171
+ it('IframeFileReadableStream.decodeData should handle ArrayBuffer and unknown types', async () => {
172
+ const rh: any = {
173
+ registerStreamHandler: jest.fn(),
174
+ unregisterStreamHandler: jest.fn(),
175
+ postMessage: jest.fn()
176
+ };
177
+ const rs = new IframeFileReadableStream('sid', 'rid', rh);
178
+ const ab = new Uint8Array([7, 8]).buffer;
179
+ expect((rs as any).decodeData(ab)).toBeInstanceOf(Uint8Array);
180
+ expect((rs as any).decodeData(1)).toBeInstanceOf(Uint8Array);
181
+ });
182
+
183
+ it('mergeChunks should handle 0/1/many branches', () => {
184
+ const rh: any = {
185
+ registerStreamHandler: jest.fn(),
186
+ unregisterStreamHandler: jest.fn(),
187
+ postMessage: jest.fn()
188
+ };
189
+ const rs: any = new IframeFileReadableStream('sid', 'rid', rh);
190
+ rs.chunks = [];
191
+ expect(rs.mergeChunks()).toBeInstanceOf(Uint8Array);
192
+ rs.chunks = [new Uint8Array([1])];
193
+ expect(rs.mergeChunks()).toEqual(new Uint8Array([1]));
194
+ rs.chunks = [new Uint8Array([1, 2]), new Uint8Array([3])];
195
+ expect(rs.mergeChunks()).toEqual(new Uint8Array([1, 2, 3]));
196
+ });
197
+
198
+ it('readAsFile should prefer explicit fileName parameter', async () => {
199
+ const rh: any = {
200
+ registerStreamHandler: jest.fn(),
201
+ unregisterStreamHandler: jest.fn(),
202
+ postMessage: jest.fn()
203
+ };
204
+ const rs: any = new IframeFileReadableStream('sid', 'rid', rh, { filename: 'default.txt', mimeType: 'text/plain' });
205
+ rs.read = jest.fn().mockResolvedValue(new Uint8Array([65])); // 'A'
206
+ const f: File = await rs.readAsFile('explicit.txt');
207
+ expect(f.name).toBe('explicit.txt');
208
+ });
209
+ });
210
+
211
+ describe('src/utils/debug.ts branches', () => {
212
+ it('setupClientDebugInterceptors should log success for file/stream/plain and log error', async () => {
213
+ const requestUse = jest.fn();
214
+ const responseUse = jest.fn();
215
+ const fakeClient: any = {
216
+ interceptors: {
217
+ request: { use: requestUse },
218
+ response: { use: responseUse }
219
+ }
220
+ };
221
+
222
+ const infoSpy = jest.spyOn(console, 'info').mockImplementation(() => undefined);
223
+ const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => undefined);
224
+ const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => undefined);
225
+
226
+ setupClientDebugInterceptors(fakeClient);
227
+ expect(requestUse).toHaveBeenCalled();
228
+ expect(responseUse).toHaveBeenCalled();
229
+
230
+ const [onFulfilled, onRejected] = responseUse.mock.calls[0];
231
+
232
+ // File/Blob branch
233
+ await onFulfilled({ requestId: 'r', status: 200, statusText: 'OK', data: new Blob(['x'], { type: 'text/plain' }) });
234
+ // Stream branch
235
+ await onFulfilled({ requestId: 'r', status: 200, statusText: 'OK', data: { ok: true }, stream: { streamId: 's', type: 'data' } });
236
+ // Plain branch
237
+ await onFulfilled({ requestId: 'r', status: 200, statusText: 'OK', data: { ok: true } });
238
+
239
+ // Error branch
240
+ await expect(
241
+ onRejected({ requestId: 'r', code: 'X', message: 'bad' })
242
+ ).rejects.toBeDefined();
243
+
244
+ expect(infoSpy).toHaveBeenCalled();
245
+ expect(errorSpy).toHaveBeenCalled();
246
+
247
+ infoSpy.mockRestore();
248
+ warnSpy.mockRestore();
249
+ errorSpy.mockRestore();
250
+ });
251
+
252
+ it('setupServerDebugListeners should cover server-side debug branches (with and without sendStream)', async () => {
253
+ const infoSpy = jest.spyOn(console, 'info').mockImplementation(() => undefined);
254
+ const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => undefined);
255
+
256
+ // Fake server implementation shape used by setupServerDebugListeners
257
+ const middlewares: any[] = [];
258
+ const dispatcher: any = {
259
+ sendMessage: jest.fn((target: any, origin: string, type: string, requestId: string, data?: any) => {
260
+ // noop
261
+ void target;
262
+ void origin;
263
+ void type;
264
+ void requestId;
265
+ void data;
266
+ })
267
+ };
268
+
269
+ const serverImpl: any = {
270
+ messageDispatcher: dispatcher,
271
+ // Will be wrapped by setupServerMessageDebugging
272
+ handleRequest: jest.fn(),
273
+ runMiddlewares: jest.fn((req: any, res: any, cb: any) => cb())
274
+ };
275
+
276
+ const fakeServer: any = {
277
+ use: jest.fn((mw: any) => middlewares.push(mw)),
278
+ // allow setupServerDebugListeners to find impl fields via cast
279
+ messageDispatcher: dispatcher
280
+ };
281
+ // Attach impl fields directly on fakeServer (since code uses `server as any`)
282
+ Object.assign(fakeServer, serverImpl);
283
+
284
+ setupServerDebugListeners(fakeServer);
285
+ expect(fakeServer.use).toHaveBeenCalled();
286
+
287
+ // Grab the middleware registered by setupServerDebugListeners
288
+ const mw = middlewares[0];
289
+ expect(typeof mw).toBe('function');
290
+
291
+ const req: any = {
292
+ requestId: 'rid-1',
293
+ path: '/api/test',
294
+ body: { a: 1 },
295
+ origin: 'https://example.com',
296
+ headers: { h: 'v' },
297
+ cookies: { c: '1' }
298
+ };
299
+
300
+ // Case A: res has sendStream
301
+ const resA: any = {
302
+ statusCode: 200,
303
+ headers: {},
304
+ send: jest.fn(async () => true),
305
+ json: jest.fn(async () => true),
306
+ sendFile: jest.fn(async () => true),
307
+ sendStream: jest.fn(async () => undefined),
308
+ status: jest.fn(function (code: number) { this.statusCode = code; return this; }),
309
+ setHeader: jest.fn(function (name: string, value: any) { this.headers[name] = value; })
310
+ };
311
+
312
+ await new Promise<void>((resolve) => mw(req, resA, resolve));
313
+ // Trigger overridden methods (cover branches)
314
+ resA.status(201);
315
+ resA.setHeader('X-Test', 'v');
316
+ await resA.send({ ok: true }, { requireAck: true });
317
+ await resA.json({ ok: true }, { requireAck: false });
318
+ await resA.sendFile('content', { fileName: 'a.txt', mimeType: 'text/plain' });
319
+ await resA.sendStream({ streamId: 'sid-1' });
320
+
321
+ // Case B: res has NO sendStream (skip that override branch)
322
+ const resB: any = {
323
+ statusCode: 200,
324
+ headers: {},
325
+ send: jest.fn(async () => true),
326
+ json: jest.fn(async () => true),
327
+ sendFile: jest.fn(async () => true),
328
+ status: jest.fn(function (code: number) { this.statusCode = code; return this; }),
329
+ setHeader: jest.fn(function (name: string, value: any) { this.headers[name] = value; })
330
+ };
331
+ await new Promise<void>((resolve) => mw({ ...req, requestId: 'rid-2' }, resB, resolve));
332
+ await resB.send({ ok: true });
333
+
334
+ // Cover dispatcher message-level logging branches installed by setupServerMessageDebugging
335
+ fakeServer.messageDispatcher.sendMessage({} as any, '*', 'ack', 'rid-1', { path: '/p' });
336
+ fakeServer.messageDispatcher.sendMessage({} as any, '*', 'async', 'rid-1', { path: '/p' });
337
+ fakeServer.messageDispatcher.sendMessage({} as any, '*', 'stream_start', 'rid-1', { body: { streamId: 's', type: 'file', chunked: true, autoResolve: true, metadata: { a: 1 } } });
338
+ fakeServer.messageDispatcher.sendMessage({} as any, '*', 'stream_data', 'rid-1', { body: { streamId: 's', done: false, data: 'xxx' } });
339
+ fakeServer.messageDispatcher.sendMessage({} as any, '*', 'stream_end', 'rid-1', { body: { streamId: 's' } });
340
+ fakeServer.messageDispatcher.sendMessage({} as any, '*', 'error', 'rid-1', { status: 500, statusText: 'ERR', error: { message: 'x' }, path: '/p' });
341
+ fakeServer.messageDispatcher.sendMessage({} as any, '*', 'response', 'rid-1', { status: 200, statusText: 'OK', requireAck: false, path: '/p' });
342
+
343
+ // Cover handleRequest wrapper branch
344
+ fakeServer.handleRequest({ requestId: 'rid-3', path: '/p', role: 'client', creatorId: 'c' }, { origin: 'o' });
345
+ // Cover runMiddlewares wrapper branch
346
+ fakeServer.runMiddlewares({ requestId: 'rid-4', path: '/p' }, {} as any, () => undefined);
347
+
348
+ expect(infoSpy).toHaveBeenCalled();
349
+ expect(errorSpy).toHaveBeenCalled();
350
+
351
+ infoSpy.mockRestore();
352
+ errorSpy.mockRestore();
353
+ });
354
+ });
355
+ });
356
+