UNPKG

@superset-ui/switchboard

Version:

Switchboard is a library to make it easier to communicate across browser windows using the MessageChannel API

338 lines (255 loc) 11.5 kB
(function () {var enterModule = typeof reactHotLoaderGlobal !== 'undefined' ? reactHotLoaderGlobal.enterModule : undefined;enterModule && enterModule(module);})();import _setImmediate from "@babel/runtime-corejs3/core-js-stable/set-immediate";var __signature__ = typeof reactHotLoaderGlobal !== 'undefined' ? reactHotLoaderGlobal.default.signature : function (a) {return a;}; /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import SingletonSwitchboard, { Switchboard } from './switchboard'; // A note on these fakes: // // jsdom doesn't supply a MessageChannel or a MessagePort, // so we have to build our own unless we want to unit test in-browser. // Might want to open a PR in jsdom: https://github.com/jsdom/jsdom/issues/2448 /** Matches the MessagePort api as closely as necessary (it's a small api) */ class FakeMessagePort {constructor() {this. otherPort = void 0;this. isStarted = false;this. queue = [];this. listeners = new Set();this. onmessage = null; // not implemented, requires some kinda proxy thingy to mock correctly this. onmessageerror = null;}dispatchEvent(event) {if (this.isStarted) {this.listeners.forEach((listener) => {try {listener(event);} catch (err) {if (typeof this.onmessageerror === 'function') {this.onmessageerror(err);}}});} else {this.queue.push(event);}return true;}addEventListener(eventType, handler) {this.listeners.add(handler);}removeEventListener(eventType, handler) {this.listeners.delete(handler);}postMessage(data) {this.otherPort.dispatchEvent({ data });}start() {if (this.isStarted) return;this.isStarted = true;this.queue.forEach((event) => {this.dispatchEvent(event);});this.queue = [];}close() {this.isStarted = false;} // @ts-ignore __reactstandin__regenerateByEval(key, code) {// @ts-ignore this[key] = eval(code);}} /** Matches the MessageChannel api as closely as necessary (an even smaller api than MessagePort) */ class FakeMessageChannel { constructor() {this.port1 = void 0;this.port2 = void 0; const port1 = new FakeMessagePort(); const port2 = new FakeMessagePort(); port1.otherPort = port2; port2.otherPort = port1; this.port1 = port1; this.port2 = port2; } // @ts-ignore __reactstandin__regenerateByEval(key, code) {// @ts-ignore this[key] = eval(code);}} describe('comms', () => { let originalConsoleDebug = null; let originalConsoleError = null; beforeAll(() => { Object.defineProperty(global, 'MessageChannel', { value: FakeMessageChannel }); originalConsoleDebug = console.debug; originalConsoleError = console.error; }); beforeEach(() => { console.debug = jest.fn(); // silencio bruno console.error = jest.fn(); }); afterEach(() => { console.debug = originalConsoleDebug; console.error = originalConsoleError; }); it('constructs with defaults', () => { const sb = new Switchboard({ port: new MessageChannel().port1 }); expect(sb).not.toBeNull(); expect(sb).toHaveProperty('name'); expect(sb).toHaveProperty('debugMode'); }); it('singleton', async () => { SingletonSwitchboard.start(); expect(console.error).toHaveBeenCalledWith( '[]', 'Switchboard not initialised' ); SingletonSwitchboard.emit('someEvent', 42); expect(console.error).toHaveBeenCalledWith( '[]', 'Switchboard not initialised' ); await expect(SingletonSwitchboard.get('failing')).rejects.toThrow( 'Switchboard not initialised' ); SingletonSwitchboard.init({ port: new MessageChannel().port1 }); expect(SingletonSwitchboard).toHaveProperty('name'); expect(SingletonSwitchboard).toHaveProperty('debugMode'); SingletonSwitchboard.init({ port: new MessageChannel().port1 }); expect(console.error).toHaveBeenCalledWith( '[switchboard]', 'already initialized' ); }); describe('emit', () => { it('triggers the method', async () => { const channel = new MessageChannel(); const ours = new Switchboard({ port: channel.port1, name: 'ours' }); const theirs = new Switchboard({ port: channel.port2, name: 'theirs' }); const handler = jest.fn(); theirs.defineMethod('someEvent', handler); theirs.start(); ours.emit('someEvent', 42); expect(handler).toHaveBeenCalledWith(42); }); it('handles a missing method', async () => { const channel = new MessageChannel(); const ours = new Switchboard({ port: channel.port1, name: 'ours' }); const theirs = new Switchboard({ port: channel.port2, name: 'theirs' }); theirs.start(); channel.port2.onmessageerror = jest.fn(); ours.emit('fakemethod'); await new Promise(_setImmediate); expect(channel.port2.onmessageerror).not.toHaveBeenCalled(); }); }); describe('get', () => { it('returns the value', async () => { const channel = new MessageChannel(); const ours = new Switchboard({ port: channel.port1, name: 'ours' }); const theirs = new Switchboard({ port: channel.port2, name: 'theirs' }); theirs.defineMethod('theirMethod', ({ x }) => Promise.resolve(x + 42) ); theirs.start(); const value = await ours.get('theirMethod', { x: 1 }); expect(value).toEqual(43); }); it('removes the listener after', async () => { const channel = new MessageChannel(); const ours = new Switchboard({ port: channel.port1, name: 'ours' }); const theirs = new Switchboard({ port: channel.port2, name: 'theirs' }); theirs.defineMethod('theirMethod', () => Promise.resolve(420)); theirs.start(); expect(channel.port1.listeners).toHaveProperty( 'size', 1 ); const promise = ours.get('theirMethod'); expect(channel.port1.listeners).toHaveProperty( 'size', 2 ); await promise; expect(channel.port1.listeners).toHaveProperty( 'size', 1 ); }); it('can handle one way concurrency', async () => { const channel = new MessageChannel(); const ours = new Switchboard({ port: channel.port1, name: 'ours' }); const theirs = new Switchboard({ port: channel.port2, name: 'theirs' }); theirs.defineMethod('theirMethod', () => Promise.resolve(42)); theirs.defineMethod( 'theirMethod2', () => new Promise((resolve) => _setImmediate(() => resolve(420))) ); theirs.start(); const [value1, value2] = await Promise.all([ ours.get('theirMethod'), ours.get('theirMethod2')] ); expect(value1).toEqual(42); expect(value2).toEqual(420); }); it('can handle two way concurrency', async () => { const channel = new MessageChannel(); const ours = new Switchboard({ port: channel.port1, name: 'ours' }); const theirs = new Switchboard({ port: channel.port2, name: 'theirs' }); theirs.defineMethod('theirMethod', () => Promise.resolve(42)); ours.defineMethod( 'ourMethod', () => new Promise((resolve) => _setImmediate(() => resolve(420))) ); theirs.start(); const [value1, value2] = await Promise.all([ ours.get('theirMethod'), theirs.get('ourMethod')] ); expect(value1).toEqual(42); expect(value2).toEqual(420); }); it('handles when the method is not defined', async () => { const channel = new MessageChannel(); const ours = new Switchboard({ port: channel.port1, name: 'ours' }); const theirs = new Switchboard({ port: channel.port2, name: 'theirs' }); theirs.start(); await expect(ours.get('fakemethod')).rejects.toThrow( '[theirs] Method "fakemethod" is not defined' ); }); it('handles when the method throws', async () => { const channel = new MessageChannel(); const ours = new Switchboard({ port: channel.port1, name: 'ours' }); const theirs = new Switchboard({ port: channel.port2, name: 'theirs' }); theirs.defineMethod('failing', () => { throw new Error('i dont feel like writing a clever message here'); }); theirs.start(); console.error = jest.fn(); // will be restored by the afterEach await expect(ours.get('failing')).rejects.toThrow( '[theirs] Method "failing" threw an error' ); }); it('handles receiving an unexpected non-reply, non-error response', async () => { const { port1, port2 } = new MessageChannel(); const ours = new Switchboard({ port: port1, name: 'ours' }); // This test is required for 100% coverage. But there's no way to set up these conditions // within the switchboard interface, so we gotta hack together the ports directly. port2.addEventListener('message', (event) => { const { messageId } = event.data; port1.dispatchEvent({ data: { messageId } }); }); port2.start(); await expect(ours.get('someMethod')).rejects.toThrow( 'Unexpected response message' ); }); }); it('logs in debug mode', async () => { const { port1, port2 } = new MessageChannel(); const ours = new Switchboard({ port: port1, name: 'ours', debug: true }); const theirs = new Switchboard({ port: port2, name: 'theirs', debug: true }); theirs.defineMethod('blah', () => {}); theirs.start(); await ours.emit('blah'); expect(console.debug).toHaveBeenCalledTimes(1); expect(console.debug.mock.calls[0][0]).toBe('[theirs]'); }); it('does not log outside debug mode', async () => { const { port1, port2 } = new MessageChannel(); const ours = new Switchboard({ port: port1, name: 'ours' }); const theirs = new Switchboard({ port: port2, name: 'theirs' }); theirs.defineMethod('blah', () => {}); theirs.start(); await ours.emit('blah'); expect(console.debug).toHaveBeenCalledTimes(0); }); });;(function () {var reactHotLoader = typeof reactHotLoaderGlobal !== 'undefined' ? reactHotLoaderGlobal.default : undefined;if (!reactHotLoader) {return;}reactHotLoader.register(FakeMessagePort, "FakeMessagePort", "/Users/evan_1/GitHub/superset/superset-frontend/packages/superset-ui-switchboard/src/switchboard.test.ts");reactHotLoader.register(FakeMessageChannel, "FakeMessageChannel", "/Users/evan_1/GitHub/superset/superset-frontend/packages/superset-ui-switchboard/src/switchboard.test.ts");})();;(function () {var leaveModule = typeof reactHotLoaderGlobal !== 'undefined' ? reactHotLoaderGlobal.leaveModule : undefined;leaveModule && leaveModule(module);})(); //# sourceMappingURL=switchboard.test.js.map