@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.
Files changed (55) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +368 -0
  3. package/dist/BaseTransport-CxzIr1Ds.d.mts +80 -0
  4. package/dist/BaseTransport-CxzIr1Ds.d.ts +80 -0
  5. package/dist/cefsharp.d.mts +37 -0
  6. package/dist/cefsharp.d.ts +37 -0
  7. package/dist/cefsharp.js +65 -0
  8. package/dist/cefsharp.js.map +1 -0
  9. package/dist/cefsharp.mjs +62 -0
  10. package/dist/cefsharp.mjs.map +1 -0
  11. package/dist/iframe.d.mts +35 -0
  12. package/dist/iframe.d.ts +35 -0
  13. package/dist/iframe.js +75 -0
  14. package/dist/iframe.js.map +1 -0
  15. package/dist/iframe.mjs +72 -0
  16. package/dist/iframe.mjs.map +1 -0
  17. package/dist/index.d.mts +85 -0
  18. package/dist/index.d.ts +85 -0
  19. package/dist/index.js +374 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/index.mjs +358 -0
  22. package/dist/index.mjs.map +1 -0
  23. package/dist/react-native.d.mts +36 -0
  24. package/dist/react-native.d.ts +36 -0
  25. package/dist/react-native.js +65 -0
  26. package/dist/react-native.js.map +1 -0
  27. package/dist/react-native.mjs +62 -0
  28. package/dist/react-native.mjs.map +1 -0
  29. package/dist/window.d.mts +36 -0
  30. package/dist/window.d.ts +36 -0
  31. package/dist/window.js +79 -0
  32. package/dist/window.js.map +1 -0
  33. package/dist/window.mjs +76 -0
  34. package/dist/window.mjs.map +1 -0
  35. package/package.json +97 -0
  36. package/src/BaseTransport.test.ts +60 -0
  37. package/src/BaseTransport.ts +27 -0
  38. package/src/TransportRegistry.test.ts +345 -0
  39. package/src/TransportRegistry.ts +120 -0
  40. package/src/cefsharp.ts +3 -0
  41. package/src/iframe.ts +3 -0
  42. package/src/index.ts +26 -0
  43. package/src/react-native.ts +3 -0
  44. package/src/transports/CefSharpTransport.test.ts +187 -0
  45. package/src/transports/CefSharpTransport.ts +73 -0
  46. package/src/transports/IframeTransport.test.ts +212 -0
  47. package/src/transports/IframeTransport.ts +79 -0
  48. package/src/transports/NullTransport.test.ts +64 -0
  49. package/src/transports/NullTransport.ts +27 -0
  50. package/src/transports/PostMessageTransport.ts +50 -0
  51. package/src/transports/ReactNativeTransport.test.ts +196 -0
  52. package/src/transports/ReactNativeTransport.ts +73 -0
  53. package/src/transports/WindowTransport.ts +84 -0
  54. package/src/types.ts +69 -0
  55. 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
+ };
@@ -0,0 +1,3 @@
1
+ // CefSharp transport - standalone entry point
2
+ export { CefSharpTransport, cefSharpDetector } from './transports/CefSharpTransport';
3
+ export type { Transport, TransportListener, TransportUnsubscribe } from './types';
package/src/iframe.ts ADDED
@@ -0,0 +1,3 @@
1
+ // Iframe transport - standalone entry point
2
+ export { IframeTransport, iframeDetector } from './transports/IframeTransport';
3
+ export type { Transport, TransportListener, TransportUnsubscribe } from './types';
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,3 @@
1
+ // React Native transport - standalone entry point
2
+ export { ReactNativeTransport, reactNativeDetector } from './transports/ReactNativeTransport';
3
+ export type { Transport, TransportListener, TransportUnsubscribe } from './types';
@@ -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
+ });