@aspectly/transports 0.1.0
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/LICENSE +20 -0
- package/README.md +368 -0
- package/dist/BaseTransport-CxzIr1Ds.d.mts +80 -0
- package/dist/BaseTransport-CxzIr1Ds.d.ts +80 -0
- package/dist/cefsharp.d.mts +37 -0
- package/dist/cefsharp.d.ts +37 -0
- package/dist/cefsharp.js +65 -0
- package/dist/cefsharp.js.map +1 -0
- package/dist/cefsharp.mjs +62 -0
- package/dist/cefsharp.mjs.map +1 -0
- package/dist/iframe.d.mts +35 -0
- package/dist/iframe.d.ts +35 -0
- package/dist/iframe.js +75 -0
- package/dist/iframe.js.map +1 -0
- package/dist/iframe.mjs +72 -0
- package/dist/iframe.mjs.map +1 -0
- package/dist/index.d.mts +85 -0
- package/dist/index.d.ts +85 -0
- package/dist/index.js +374 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +358 -0
- package/dist/index.mjs.map +1 -0
- package/dist/react-native.d.mts +36 -0
- package/dist/react-native.d.ts +36 -0
- package/dist/react-native.js +65 -0
- package/dist/react-native.js.map +1 -0
- package/dist/react-native.mjs +62 -0
- package/dist/react-native.mjs.map +1 -0
- package/dist/window.d.mts +36 -0
- package/dist/window.d.ts +36 -0
- package/dist/window.js +79 -0
- package/dist/window.js.map +1 -0
- package/dist/window.mjs +76 -0
- package/dist/window.mjs.map +1 -0
- package/package.json +97 -0
- package/src/BaseTransport.test.ts +60 -0
- package/src/BaseTransport.ts +27 -0
- package/src/TransportRegistry.test.ts +345 -0
- package/src/TransportRegistry.ts +120 -0
- package/src/cefsharp.ts +3 -0
- package/src/iframe.ts +3 -0
- package/src/index.ts +26 -0
- package/src/react-native.ts +3 -0
- package/src/transports/CefSharpTransport.test.ts +187 -0
- package/src/transports/CefSharpTransport.ts +73 -0
- package/src/transports/IframeTransport.test.ts +212 -0
- package/src/transports/IframeTransport.ts +79 -0
- package/src/transports/NullTransport.test.ts +64 -0
- package/src/transports/NullTransport.ts +27 -0
- package/src/transports/PostMessageTransport.ts +50 -0
- package/src/transports/ReactNativeTransport.test.ts +196 -0
- package/src/transports/ReactNativeTransport.ts +73 -0
- package/src/transports/WindowTransport.ts +84 -0
- package/src/types.ts +69 -0
- package/src/window.ts +3 -0
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { TransportRegistry, detectTransport, registerTransport } from './TransportRegistry';
|
|
3
|
+
import type { TransportDetector } from './types';
|
|
4
|
+
import { NullTransport } from './transports/NullTransport';
|
|
5
|
+
|
|
6
|
+
describe('TransportRegistry', () => {
|
|
7
|
+
let consoleWarnSpy: ReturnType<typeof vi.spyOn>;
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
// Reset registry to clean state before each test
|
|
11
|
+
TransportRegistry.reset();
|
|
12
|
+
consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
TransportRegistry.reset();
|
|
17
|
+
consoleWarnSpy.mockRestore();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('should have built-in detectors registered by default', () => {
|
|
21
|
+
const detectors = TransportRegistry.getDetectors();
|
|
22
|
+
expect(detectors.length).toBe(5);
|
|
23
|
+
|
|
24
|
+
const names = detectors.map(d => d.name);
|
|
25
|
+
expect(names).toContain('cefsharp');
|
|
26
|
+
expect(names).toContain('react-native');
|
|
27
|
+
expect(names).toContain('iframe');
|
|
28
|
+
expect(names).toContain('window');
|
|
29
|
+
expect(names).toContain('postmessage');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should sort detectors by priority (descending)', () => {
|
|
33
|
+
const detectors = TransportRegistry.getDetectors();
|
|
34
|
+
|
|
35
|
+
// Should be sorted: cefsharp (100), react-native (90), iframe (80), window (70), postmessage (10)
|
|
36
|
+
expect(detectors[0].name).toBe('cefsharp');
|
|
37
|
+
expect(detectors[0].priority).toBe(100);
|
|
38
|
+
expect(detectors[1].name).toBe('react-native');
|
|
39
|
+
expect(detectors[1].priority).toBe(90);
|
|
40
|
+
expect(detectors[2].name).toBe('iframe');
|
|
41
|
+
expect(detectors[2].priority).toBe(80);
|
|
42
|
+
expect(detectors[3].name).toBe('window');
|
|
43
|
+
expect(detectors[3].priority).toBe(70);
|
|
44
|
+
expect(detectors[4].name).toBe('postmessage');
|
|
45
|
+
expect(detectors[4].priority).toBe(10);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should register new detector and sort by priority', () => {
|
|
49
|
+
const customDetector: TransportDetector = {
|
|
50
|
+
name: 'custom',
|
|
51
|
+
priority: 95,
|
|
52
|
+
detect: () => false,
|
|
53
|
+
createTransport: () => new NullTransport(),
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
TransportRegistry.register(customDetector);
|
|
57
|
+
const detectors = TransportRegistry.getDetectors();
|
|
58
|
+
|
|
59
|
+
expect(detectors.length).toBe(6);
|
|
60
|
+
// Should be: cefsharp (100), custom (95), react-native (90), iframe (80), window (70), postmessage (10)
|
|
61
|
+
expect(detectors[1].name).toBe('custom');
|
|
62
|
+
expect(detectors[1].priority).toBe(95);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should replace existing detector with same name', () => {
|
|
66
|
+
const customDetector1: TransportDetector = {
|
|
67
|
+
name: 'custom',
|
|
68
|
+
priority: 50,
|
|
69
|
+
detect: () => false,
|
|
70
|
+
createTransport: () => new NullTransport(),
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const customDetector2: TransportDetector = {
|
|
74
|
+
name: 'custom',
|
|
75
|
+
priority: 110,
|
|
76
|
+
detect: () => true,
|
|
77
|
+
createTransport: () => new NullTransport(),
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
TransportRegistry.register(customDetector1);
|
|
81
|
+
expect(TransportRegistry.getDetectors().length).toBe(6);
|
|
82
|
+
|
|
83
|
+
TransportRegistry.register(customDetector2);
|
|
84
|
+
expect(TransportRegistry.getDetectors().length).toBe(6); // Still 6, not 7
|
|
85
|
+
|
|
86
|
+
const detectors = TransportRegistry.getDetectors();
|
|
87
|
+
const customDetector = detectors.find(d => d.name === 'custom');
|
|
88
|
+
expect(customDetector?.priority).toBe(110); // Updated priority
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('should unregister detector by name', () => {
|
|
92
|
+
TransportRegistry.unregister('iframe');
|
|
93
|
+
const detectors = TransportRegistry.getDetectors();
|
|
94
|
+
|
|
95
|
+
expect(detectors.length).toBe(4);
|
|
96
|
+
expect(detectors.map(d => d.name)).not.toContain('iframe');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('should return readonly array from getDetectors', () => {
|
|
100
|
+
const detectors = TransportRegistry.getDetectors();
|
|
101
|
+
expect(Array.isArray(detectors)).toBe(true);
|
|
102
|
+
// TypeScript enforces readonly, but we can verify it's an array
|
|
103
|
+
expect(detectors.length).toBeGreaterThan(0);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('should detect and return first matching transport', () => {
|
|
107
|
+
// Create a detector that always matches
|
|
108
|
+
const mockTransport = new NullTransport();
|
|
109
|
+
const customDetector: TransportDetector = {
|
|
110
|
+
name: 'custom',
|
|
111
|
+
priority: 200, // Highest priority
|
|
112
|
+
detect: () => true,
|
|
113
|
+
createTransport: () => mockTransport,
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
TransportRegistry.register(customDetector);
|
|
117
|
+
const transport = TransportRegistry.detect();
|
|
118
|
+
|
|
119
|
+
expect(transport).toBe(mockTransport);
|
|
120
|
+
expect(transport.name).toBe('null'); // NullTransport
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('should cache detection result', () => {
|
|
124
|
+
const createTransportMock = vi.fn(() => new NullTransport());
|
|
125
|
+
const customDetector: TransportDetector = {
|
|
126
|
+
name: 'custom',
|
|
127
|
+
priority: 200,
|
|
128
|
+
detect: () => true,
|
|
129
|
+
createTransport: createTransportMock,
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
TransportRegistry.register(customDetector);
|
|
133
|
+
|
|
134
|
+
const transport1 = TransportRegistry.detect();
|
|
135
|
+
const transport2 = TransportRegistry.detect();
|
|
136
|
+
|
|
137
|
+
expect(transport1).toBe(transport2); // Same instance
|
|
138
|
+
expect(createTransportMock).toHaveBeenCalledTimes(1); // Only called once
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('should force re-detection when forceRedetect is true', () => {
|
|
142
|
+
const createTransportMock = vi.fn(() => new NullTransport());
|
|
143
|
+
const customDetector: TransportDetector = {
|
|
144
|
+
name: 'custom',
|
|
145
|
+
priority: 200,
|
|
146
|
+
detect: () => true,
|
|
147
|
+
createTransport: createTransportMock,
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
TransportRegistry.register(customDetector);
|
|
151
|
+
|
|
152
|
+
TransportRegistry.detect(); // First call
|
|
153
|
+
TransportRegistry.detect(true); // Force re-detect
|
|
154
|
+
|
|
155
|
+
expect(createTransportMock).toHaveBeenCalledTimes(2); // Called twice
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('should return NullTransport when no detector matches', () => {
|
|
159
|
+
// Unregister all built-in detectors
|
|
160
|
+
TransportRegistry.unregister('cefsharp');
|
|
161
|
+
TransportRegistry.unregister('react-native');
|
|
162
|
+
TransportRegistry.unregister('iframe');
|
|
163
|
+
TransportRegistry.unregister('postmessage');
|
|
164
|
+
|
|
165
|
+
const transport = TransportRegistry.detect();
|
|
166
|
+
|
|
167
|
+
expect(transport).toBeInstanceOf(NullTransport);
|
|
168
|
+
expect(transport.name).toBe('null');
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('should handle errors in detector gracefully', () => {
|
|
172
|
+
const faultyDetector: TransportDetector = {
|
|
173
|
+
name: 'faulty',
|
|
174
|
+
priority: 200,
|
|
175
|
+
detect: () => {
|
|
176
|
+
throw new Error('Detector error');
|
|
177
|
+
},
|
|
178
|
+
createTransport: () => new NullTransport(),
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
TransportRegistry.register(faultyDetector);
|
|
182
|
+
|
|
183
|
+
// Should not throw, should warn and continue
|
|
184
|
+
const transport = TransportRegistry.detect();
|
|
185
|
+
|
|
186
|
+
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
|
187
|
+
'[TransportRegistry] Detector "faulty" threw an error:',
|
|
188
|
+
expect.any(Error)
|
|
189
|
+
);
|
|
190
|
+
// Should fall back to next detector or NullTransport
|
|
191
|
+
expect(transport).toBeDefined();
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('should clear cached transport when clearCache is called', () => {
|
|
195
|
+
const createTransportMock = vi.fn(() => new NullTransport());
|
|
196
|
+
const customDetector: TransportDetector = {
|
|
197
|
+
name: 'custom',
|
|
198
|
+
priority: 200,
|
|
199
|
+
detect: () => true,
|
|
200
|
+
createTransport: createTransportMock,
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
TransportRegistry.register(customDetector);
|
|
204
|
+
|
|
205
|
+
TransportRegistry.detect(); // First call, caches result
|
|
206
|
+
TransportRegistry.clearCache();
|
|
207
|
+
TransportRegistry.detect(); // Second call after clearing cache
|
|
208
|
+
|
|
209
|
+
expect(createTransportMock).toHaveBeenCalledTimes(2); // Called twice
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('should clear cache when registering new detector', () => {
|
|
213
|
+
const detector1: TransportDetector = {
|
|
214
|
+
name: 'detector1',
|
|
215
|
+
priority: 200,
|
|
216
|
+
detect: () => true,
|
|
217
|
+
createTransport: () => new NullTransport(),
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
TransportRegistry.register(detector1);
|
|
221
|
+
TransportRegistry.detect(); // Cache result
|
|
222
|
+
|
|
223
|
+
const detector2: TransportDetector = {
|
|
224
|
+
name: 'detector2',
|
|
225
|
+
priority: 150,
|
|
226
|
+
detect: () => false,
|
|
227
|
+
createTransport: () => new NullTransport(),
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
TransportRegistry.register(detector2); // Should clear cache
|
|
231
|
+
|
|
232
|
+
// Cache should be cleared, so next detect() will re-run
|
|
233
|
+
// We can verify this indirectly by checking that detection happens again
|
|
234
|
+
expect(() => TransportRegistry.detect()).not.toThrow();
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('should clear cache when unregistering detector', () => {
|
|
238
|
+
TransportRegistry.detect(); // Cache result
|
|
239
|
+
TransportRegistry.unregister('iframe'); // Should clear cache
|
|
240
|
+
|
|
241
|
+
// Cache should be cleared
|
|
242
|
+
expect(() => TransportRegistry.detect()).not.toThrow();
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('should reset to built-in detectors only', () => {
|
|
246
|
+
const customDetector: TransportDetector = {
|
|
247
|
+
name: 'custom',
|
|
248
|
+
priority: 200,
|
|
249
|
+
detect: () => false,
|
|
250
|
+
createTransport: () => new NullTransport(),
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
TransportRegistry.register(customDetector);
|
|
254
|
+
expect(TransportRegistry.getDetectors().length).toBe(6);
|
|
255
|
+
|
|
256
|
+
TransportRegistry.reset();
|
|
257
|
+
const detectors = TransportRegistry.getDetectors();
|
|
258
|
+
|
|
259
|
+
expect(detectors.length).toBe(5);
|
|
260
|
+
expect(detectors.map(d => d.name)).toEqual(['cefsharp', 'react-native', 'iframe', 'window', 'postmessage']);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('should clear cache when reset is called', () => {
|
|
264
|
+
TransportRegistry.detect(); // Cache result
|
|
265
|
+
TransportRegistry.reset(); // Should clear cache
|
|
266
|
+
|
|
267
|
+
expect(() => TransportRegistry.detect()).not.toThrow();
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
describe('detectTransport convenience function', () => {
|
|
272
|
+
beforeEach(() => {
|
|
273
|
+
TransportRegistry.reset();
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
afterEach(() => {
|
|
277
|
+
TransportRegistry.reset();
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('should call TransportRegistry.detect()', () => {
|
|
281
|
+
const detectSpy = vi.spyOn(TransportRegistry, 'detect');
|
|
282
|
+
|
|
283
|
+
detectTransport();
|
|
284
|
+
|
|
285
|
+
expect(detectSpy).toHaveBeenCalledWith(false);
|
|
286
|
+
detectSpy.mockRestore();
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it('should pass forceRedetect parameter', () => {
|
|
290
|
+
const detectSpy = vi.spyOn(TransportRegistry, 'detect');
|
|
291
|
+
|
|
292
|
+
detectTransport(true);
|
|
293
|
+
|
|
294
|
+
expect(detectSpy).toHaveBeenCalledWith(true);
|
|
295
|
+
detectSpy.mockRestore();
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it('should return a Transport instance', () => {
|
|
299
|
+
const transport = detectTransport();
|
|
300
|
+
expect(transport).toBeDefined();
|
|
301
|
+
expect(typeof transport.name).toBe('string');
|
|
302
|
+
expect(typeof transport.isAvailable).toBe('function');
|
|
303
|
+
expect(typeof transport.send).toBe('function');
|
|
304
|
+
expect(typeof transport.subscribe).toBe('function');
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
describe('registerTransport convenience function', () => {
|
|
309
|
+
beforeEach(() => {
|
|
310
|
+
TransportRegistry.reset();
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
afterEach(() => {
|
|
314
|
+
TransportRegistry.reset();
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it('should call TransportRegistry.register()', () => {
|
|
318
|
+
const registerSpy = vi.spyOn(TransportRegistry, 'register');
|
|
319
|
+
const customDetector: TransportDetector = {
|
|
320
|
+
name: 'custom',
|
|
321
|
+
priority: 100,
|
|
322
|
+
detect: () => false,
|
|
323
|
+
createTransport: () => new NullTransport(),
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
registerTransport(customDetector);
|
|
327
|
+
|
|
328
|
+
expect(registerSpy).toHaveBeenCalledWith(customDetector);
|
|
329
|
+
registerSpy.mockRestore();
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it('should actually register the detector', () => {
|
|
333
|
+
const customDetector: TransportDetector = {
|
|
334
|
+
name: 'custom',
|
|
335
|
+
priority: 100,
|
|
336
|
+
detect: () => false,
|
|
337
|
+
createTransport: () => new NullTransport(),
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
registerTransport(customDetector);
|
|
341
|
+
|
|
342
|
+
const detectors = TransportRegistry.getDetectors();
|
|
343
|
+
expect(detectors.map(d => d.name)).toContain('custom');
|
|
344
|
+
});
|
|
345
|
+
});
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import type { Transport, TransportDetector } from './types';
|
|
2
|
+
import { cefSharpDetector } from './transports/CefSharpTransport';
|
|
3
|
+
import { reactNativeDetector } from './transports/ReactNativeTransport';
|
|
4
|
+
import { iframeDetector } from './transports/IframeTransport';
|
|
5
|
+
import { windowDetector } from './transports/WindowTransport';
|
|
6
|
+
import { postMessageDetector } from './transports/PostMessageTransport';
|
|
7
|
+
import { NullTransport } from './transports/NullTransport';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Registry for transport detectors
|
|
11
|
+
* Manages auto-detection of the current environment
|
|
12
|
+
*/
|
|
13
|
+
class TransportRegistryClass {
|
|
14
|
+
private detectors: TransportDetector[] = [];
|
|
15
|
+
private cachedTransport: Transport | null = null;
|
|
16
|
+
|
|
17
|
+
constructor() {
|
|
18
|
+
// Register built-in detectors
|
|
19
|
+
this.register(cefSharpDetector);
|
|
20
|
+
this.register(reactNativeDetector);
|
|
21
|
+
this.register(iframeDetector);
|
|
22
|
+
this.register(windowDetector);
|
|
23
|
+
this.register(postMessageDetector);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Register a custom transport detector
|
|
28
|
+
* @param detector The detector to register
|
|
29
|
+
*/
|
|
30
|
+
register(detector: TransportDetector): void {
|
|
31
|
+
// Remove existing detector with same name
|
|
32
|
+
this.detectors = this.detectors.filter(d => d.name !== detector.name);
|
|
33
|
+
this.detectors.push(detector);
|
|
34
|
+
// Sort by priority (descending)
|
|
35
|
+
this.detectors.sort((a, b) => b.priority - a.priority);
|
|
36
|
+
// Clear cache when detectors change
|
|
37
|
+
this.cachedTransport = null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Unregister a transport detector by name
|
|
42
|
+
* @param name Name of the detector to remove
|
|
43
|
+
*/
|
|
44
|
+
unregister(name: string): void {
|
|
45
|
+
this.detectors = this.detectors.filter(d => d.name !== name);
|
|
46
|
+
this.cachedTransport = null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Get all registered detectors
|
|
51
|
+
*/
|
|
52
|
+
getDetectors(): readonly TransportDetector[] {
|
|
53
|
+
return this.detectors;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Detect and return the appropriate transport for the current environment
|
|
58
|
+
* Results are cached for performance
|
|
59
|
+
* @param forceRedetect Force re-detection (ignores cache)
|
|
60
|
+
*/
|
|
61
|
+
detect(forceRedetect = false): Transport {
|
|
62
|
+
if (this.cachedTransport && !forceRedetect) {
|
|
63
|
+
return this.cachedTransport;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
for (const detector of this.detectors) {
|
|
67
|
+
try {
|
|
68
|
+
if (detector.detect()) {
|
|
69
|
+
this.cachedTransport = detector.createTransport();
|
|
70
|
+
return this.cachedTransport;
|
|
71
|
+
}
|
|
72
|
+
} catch (error) {
|
|
73
|
+
console.warn(`[TransportRegistry] Detector "${detector.name}" threw an error:`, error);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// No transport detected, return NullTransport
|
|
78
|
+
this.cachedTransport = new NullTransport();
|
|
79
|
+
return this.cachedTransport;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Clear the cached transport (useful for testing)
|
|
84
|
+
*/
|
|
85
|
+
clearCache(): void {
|
|
86
|
+
this.cachedTransport = null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Reset registry to default state (built-in detectors only)
|
|
91
|
+
*/
|
|
92
|
+
reset(): void {
|
|
93
|
+
this.detectors = [];
|
|
94
|
+
this.cachedTransport = null;
|
|
95
|
+
this.register(cefSharpDetector);
|
|
96
|
+
this.register(reactNativeDetector);
|
|
97
|
+
this.register(iframeDetector);
|
|
98
|
+
this.register(windowDetector);
|
|
99
|
+
this.register(postMessageDetector);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Global transport registry instance
|
|
105
|
+
*/
|
|
106
|
+
export const TransportRegistry = new TransportRegistryClass();
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Convenience function to detect transport
|
|
110
|
+
*/
|
|
111
|
+
export const detectTransport = (forceRedetect = false): Transport => {
|
|
112
|
+
return TransportRegistry.detect(forceRedetect);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Convenience function to register a custom detector
|
|
117
|
+
*/
|
|
118
|
+
export const registerTransport = (detector: TransportDetector): void => {
|
|
119
|
+
TransportRegistry.register(detector);
|
|
120
|
+
};
|
package/src/cefsharp.ts
ADDED
package/src/iframe.ts
ADDED
package/src/index.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Types
|
|
2
|
+
export type {
|
|
3
|
+
Transport,
|
|
4
|
+
TransportListener,
|
|
5
|
+
TransportUnsubscribe,
|
|
6
|
+
TransportFactory,
|
|
7
|
+
TransportDetector,
|
|
8
|
+
} from './types';
|
|
9
|
+
|
|
10
|
+
// Base class
|
|
11
|
+
export { BaseTransport } from './BaseTransport';
|
|
12
|
+
|
|
13
|
+
// Transports
|
|
14
|
+
export { CefSharpTransport, cefSharpDetector } from './transports/CefSharpTransport';
|
|
15
|
+
export { ReactNativeTransport, reactNativeDetector } from './transports/ReactNativeTransport';
|
|
16
|
+
export { IframeTransport, iframeDetector } from './transports/IframeTransport';
|
|
17
|
+
export { WindowTransport, windowDetector } from './transports/WindowTransport';
|
|
18
|
+
export { PostMessageTransport, postMessageDetector } from './transports/PostMessageTransport';
|
|
19
|
+
export { NullTransport } from './transports/NullTransport';
|
|
20
|
+
|
|
21
|
+
// Registry and auto-detection
|
|
22
|
+
export {
|
|
23
|
+
TransportRegistry,
|
|
24
|
+
detectTransport,
|
|
25
|
+
registerTransport,
|
|
26
|
+
} from './TransportRegistry';
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { CefSharpTransport, cefSharpDetector } from './CefSharpTransport';
|
|
3
|
+
|
|
4
|
+
interface CefSharpWindow extends Window {
|
|
5
|
+
CefSharp?: {
|
|
6
|
+
PostMessage: (message: string) => void;
|
|
7
|
+
BindObjectAsync: (...args: unknown[]) => Promise<void>;
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const testWindow = window as CefSharpWindow;
|
|
12
|
+
|
|
13
|
+
describe('CefSharpTransport', () => {
|
|
14
|
+
let transport: CefSharpTransport;
|
|
15
|
+
let originalCefSharp: typeof testWindow.CefSharp;
|
|
16
|
+
let consoleWarnSpy: ReturnType<typeof vi.spyOn>;
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
transport = new CefSharpTransport();
|
|
20
|
+
originalCefSharp = testWindow.CefSharp;
|
|
21
|
+
consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
testWindow.CefSharp = originalCefSharp;
|
|
26
|
+
consoleWarnSpy.mockRestore();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should have name property equal to "cefsharp"', () => {
|
|
30
|
+
expect(transport.name).toBe('cefsharp');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should return false when CefSharp not present', () => {
|
|
34
|
+
testWindow.CefSharp = undefined;
|
|
35
|
+
expect(transport.isAvailable()).toBe(false);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('should return false when CefSharp.PostMessage not a function', () => {
|
|
39
|
+
testWindow.CefSharp = {} as any;
|
|
40
|
+
expect(transport.isAvailable()).toBe(false);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should return true when CefSharp.PostMessage exists', () => {
|
|
44
|
+
testWindow.CefSharp = {
|
|
45
|
+
PostMessage: vi.fn(),
|
|
46
|
+
BindObjectAsync: vi.fn(),
|
|
47
|
+
};
|
|
48
|
+
expect(transport.isAvailable()).toBe(true);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should call CefSharp.PostMessage with message when sending', () => {
|
|
52
|
+
const mockPostMessage = vi.fn();
|
|
53
|
+
testWindow.CefSharp = {
|
|
54
|
+
PostMessage: mockPostMessage,
|
|
55
|
+
BindObjectAsync: vi.fn(),
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
transport.send('test message');
|
|
59
|
+
|
|
60
|
+
expect(mockPostMessage).toHaveBeenCalledWith('test message');
|
|
61
|
+
expect(mockPostMessage).toHaveBeenCalledTimes(1);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should warn when CefSharp not available during send', () => {
|
|
65
|
+
testWindow.CefSharp = undefined;
|
|
66
|
+
|
|
67
|
+
transport.send('test message');
|
|
68
|
+
|
|
69
|
+
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
|
70
|
+
'[CefSharpTransport] CefSharp.PostMessage is not available'
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should warn when CefSharp.PostMessage not available during send', () => {
|
|
75
|
+
testWindow.CefSharp = {} as any;
|
|
76
|
+
|
|
77
|
+
transport.send('test message');
|
|
78
|
+
|
|
79
|
+
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
|
80
|
+
'[CefSharpTransport] CefSharp.PostMessage is not available'
|
|
81
|
+
);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should add message event listener when subscribing', () => {
|
|
85
|
+
const addEventListenerSpy = vi.spyOn(window, 'addEventListener');
|
|
86
|
+
const listener = vi.fn();
|
|
87
|
+
|
|
88
|
+
transport.subscribe(listener);
|
|
89
|
+
|
|
90
|
+
expect(addEventListenerSpy).toHaveBeenCalledWith('message', expect.any(Function));
|
|
91
|
+
|
|
92
|
+
addEventListenerSpy.mockRestore();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should call listener with string data from message event', () => {
|
|
96
|
+
const listener = vi.fn();
|
|
97
|
+
transport.subscribe(listener);
|
|
98
|
+
|
|
99
|
+
const event = new MessageEvent('message', { data: 'test data' });
|
|
100
|
+
window.dispatchEvent(event);
|
|
101
|
+
|
|
102
|
+
expect(listener).toHaveBeenCalledWith('test data');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should not call listener with non-string data', () => {
|
|
106
|
+
const listener = vi.fn();
|
|
107
|
+
transport.subscribe(listener);
|
|
108
|
+
|
|
109
|
+
const event1 = new MessageEvent('message', { data: 123 });
|
|
110
|
+
const event2 = new MessageEvent('message', { data: { foo: 'bar' } });
|
|
111
|
+
const event3 = new MessageEvent('message', { data: null });
|
|
112
|
+
|
|
113
|
+
window.dispatchEvent(event1);
|
|
114
|
+
window.dispatchEvent(event2);
|
|
115
|
+
window.dispatchEvent(event3);
|
|
116
|
+
|
|
117
|
+
expect(listener).not.toHaveBeenCalled();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should remove event listener when cleanup function is called', () => {
|
|
121
|
+
const removeEventListenerSpy = vi.spyOn(window, 'removeEventListener');
|
|
122
|
+
const listener = vi.fn();
|
|
123
|
+
|
|
124
|
+
const unsubscribe = transport.subscribe(listener);
|
|
125
|
+
unsubscribe();
|
|
126
|
+
|
|
127
|
+
expect(removeEventListenerSpy).toHaveBeenCalledWith('message', expect.any(Function));
|
|
128
|
+
|
|
129
|
+
removeEventListenerSpy.mockRestore();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('should not receive messages after unsubscribing', () => {
|
|
133
|
+
const listener = vi.fn();
|
|
134
|
+
const unsubscribe = transport.subscribe(listener);
|
|
135
|
+
|
|
136
|
+
const event1 = new MessageEvent('message', { data: 'before unsubscribe' });
|
|
137
|
+
window.dispatchEvent(event1);
|
|
138
|
+
|
|
139
|
+
expect(listener).toHaveBeenCalledTimes(1);
|
|
140
|
+
|
|
141
|
+
unsubscribe();
|
|
142
|
+
|
|
143
|
+
const event2 = new MessageEvent('message', { data: 'after unsubscribe' });
|
|
144
|
+
window.dispatchEvent(event2);
|
|
145
|
+
|
|
146
|
+
expect(listener).toHaveBeenCalledTimes(1); // Still 1, not 2
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe('cefSharpDetector', () => {
|
|
151
|
+
let originalCefSharp: typeof testWindow.CefSharp;
|
|
152
|
+
|
|
153
|
+
beforeEach(() => {
|
|
154
|
+
originalCefSharp = testWindow.CefSharp;
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
afterEach(() => {
|
|
158
|
+
testWindow.CefSharp = originalCefSharp;
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('should have name property equal to "cefsharp"', () => {
|
|
162
|
+
expect(cefSharpDetector.name).toBe('cefsharp');
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('should have priority of 100', () => {
|
|
166
|
+
expect(cefSharpDetector.priority).toBe(100);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('should detect() return false when CefSharp not present', () => {
|
|
170
|
+
testWindow.CefSharp = undefined;
|
|
171
|
+
expect(cefSharpDetector.detect()).toBe(false);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('should detect() return true when CefSharp.PostMessage exists', () => {
|
|
175
|
+
testWindow.CefSharp = {
|
|
176
|
+
PostMessage: vi.fn(),
|
|
177
|
+
BindObjectAsync: vi.fn(),
|
|
178
|
+
};
|
|
179
|
+
expect(cefSharpDetector.detect()).toBe(true);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('should createTransport() return CefSharpTransport instance', () => {
|
|
183
|
+
const transport = cefSharpDetector.createTransport();
|
|
184
|
+
expect(transport).toBeInstanceOf(CefSharpTransport);
|
|
185
|
+
expect(transport.name).toBe('cefsharp');
|
|
186
|
+
});
|
|
187
|
+
});
|