/** * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @format */ import {createMockFlipperWithPlugin} from '../../test-utils/createMockFlipperWithPlugin'; import {Store, Client} from '../../'; import {selectPlugin, starPlugin} from '../../reducers/connections'; import {registerPlugins} from '../../reducers/plugins'; import { _SandyPluginDefinition, _SandyPluginInstance, PluginClient, TestUtils, } from 'flipper-plugin'; const pluginDetails = TestUtils.createMockPluginDetails(); let initialized = false; beforeEach(() => { initialized = false; }); function plugin(client: PluginClient) { const connectStub = jest.fn(); const disconnectStub = jest.fn(); const destroyStub = jest.fn(); const messages: any[] = []; client.onConnect(connectStub); client.onDisconnect(disconnectStub); client.onDestroy(destroyStub); client.onMessage('message', (msg) => { messages.push(msg); }); initialized = true; return { connectStub, disconnectStub, destroyStub, send: client.send, messages, }; } const TestPlugin = new _SandyPluginDefinition(pluginDetails, { plugin: jest.fn().mockImplementation(plugin) as typeof plugin, Component: jest.fn().mockImplementation(() => null), }); type PluginApi = ReturnType; function starTestPlugin(store: Store, client: Client) { store.dispatch( starPlugin({ plugin: TestPlugin, selectedApp: client.query.app, }), ); } function selectTestPlugin(store: Store, client: Client) { store.dispatch( selectPlugin({ selectedPlugin: TestPlugin.id, selectedApp: client.query.app, deepLinkPayload: null, selectedDevice: store.getState().connections.selectedDevice!, }), ); } test('it should initialize starred sandy plugins', async () => { const {client, store} = await createMockFlipperWithPlugin(TestPlugin); // already started, so initialized immediately expect(initialized).toBe(true); expect(client.sandyPluginStates.get(TestPlugin.id)).toBeInstanceOf( _SandyPluginInstance, ); const instanceApi: PluginApi = client.sandyPluginStates.get(TestPlugin.id)! .instanceApi; expect(instanceApi.connectStub).toBeCalledTimes(0); selectTestPlugin(store, client); // without rendering, non-bg plugins won't connect automatically, // so this isn't the best test, but PluginContainer tests do test that part of the lifecycle client.initPlugin(TestPlugin.id); expect(instanceApi.connectStub).toBeCalledTimes(1); client.deinitPlugin(TestPlugin.id); expect(instanceApi.disconnectStub).toBeCalledTimes(1); expect(instanceApi.destroyStub).toBeCalledTimes(0); }); test('it should cleanup a plugin if disabled', async () => { const {client, store} = await createMockFlipperWithPlugin(TestPlugin); expect(TestPlugin.asPluginModule().plugin).toBeCalledTimes(1); const pluginInstance: PluginApi = client.sandyPluginStates.get(TestPlugin.id)! .instanceApi; expect(pluginInstance.destroyStub).toHaveBeenCalledTimes(0); client.initPlugin(TestPlugin.id); expect(pluginInstance.connectStub).toHaveBeenCalledTimes(1); // unstar starTestPlugin(store, client); expect(client.sandyPluginStates.has(TestPlugin.id)).toBeFalsy(); expect(pluginInstance.disconnectStub).toHaveBeenCalledTimes(1); expect(pluginInstance.destroyStub).toHaveBeenCalledTimes(1); }); test('it should NOT cleanup if client is removed', async () => { const {client} = await createMockFlipperWithPlugin(TestPlugin); const pluginInstance = client.sandyPluginStates.get(TestPlugin.id)!; expect(pluginInstance.instanceApi.destroyStub).toHaveBeenCalledTimes(0); pluginInstance.connect(); expect(client.connected.get()).toBe(true); client.disconnect(); expect(client.connected.get()).toBe(false); expect(client.sandyPluginStates.has(TestPlugin.id)).toBeTruthy(); expect( (pluginInstance.instanceApi as PluginApi).disconnectStub, ).toHaveBeenCalledTimes(1); expect( (pluginInstance.instanceApi as PluginApi).destroyStub, ).toHaveBeenCalledTimes(0); }); test('it should cleanup if client is destroyed', async () => { const {client} = await createMockFlipperWithPlugin(TestPlugin); const pluginInstance = client.sandyPluginStates.get(TestPlugin.id)!; expect(pluginInstance.instanceApi.destroyStub).toHaveBeenCalledTimes(0); client.destroy(); expect(client.sandyPluginStates.has(TestPlugin.id)).toBeFalsy(); expect( (pluginInstance.instanceApi as PluginApi).destroyStub, ).toHaveBeenCalledTimes(1); }); test('it should not initialize a sandy plugin if not enabled', async () => { const {client, store} = await createMockFlipperWithPlugin(TestPlugin); const Plugin2 = new _SandyPluginDefinition( TestUtils.createMockPluginDetails({ name: 'Plugin2', id: 'Plugin2', }), { plugin: jest.fn().mockImplementation(plugin), Component() { return null; }, }, ); const pluginState1 = client.sandyPluginStates.get(TestPlugin.id); expect(pluginState1).toBeInstanceOf(_SandyPluginInstance); store.dispatch(registerPlugins([Plugin2])); await client.refreshPlugins(); // not yet enabled, so not yet started expect(client.sandyPluginStates.get(Plugin2.id)).toBeUndefined(); expect(Plugin2.asPluginModule().plugin).toBeCalledTimes(0); store.dispatch( starPlugin({ plugin: Plugin2, selectedApp: client.query.app, }), ); expect(client.sandyPluginStates.get(Plugin2.id)).toBeInstanceOf( _SandyPluginInstance, ); const instance = client.sandyPluginStates.get(Plugin2.id)! .instanceApi as PluginApi; expect(client.sandyPluginStates.get(TestPlugin.id)).toBe(pluginState1); // not reinitialized expect(TestPlugin.asPluginModule().plugin).toBeCalledTimes(1); expect(Plugin2.asPluginModule().plugin).toBeCalledTimes(1); expect(instance.destroyStub).toHaveBeenCalledTimes(0); // disable plugin again store.dispatch( starPlugin({ plugin: Plugin2, selectedApp: client.query.app, }), ); expect(client.sandyPluginStates.get(Plugin2.id)).toBeUndefined(); expect(instance.connectStub).toHaveBeenCalledTimes(0); // disconnect wasn't called because connect was never called expect(instance.disconnectStub).toHaveBeenCalledTimes(0); expect(instance.destroyStub).toHaveBeenCalledTimes(1); }); test('it trigger hooks for background plugins', async () => { const {client} = await createMockFlipperWithPlugin(TestPlugin, { asBackgroundPlugin: true, }); const pluginInstance: PluginApi = client.sandyPluginStates.get(TestPlugin.id)! .instanceApi; expect(client.isBackgroundPlugin(TestPlugin.id)).toBeTruthy(); expect(pluginInstance.destroyStub).toHaveBeenCalledTimes(0); expect(pluginInstance.connectStub).toHaveBeenCalledTimes(1); expect(pluginInstance.disconnectStub).toHaveBeenCalledTimes(0); client.destroy(); expect(client.sandyPluginStates.has(TestPlugin.id)).toBeFalsy(); expect(pluginInstance.destroyStub).toHaveBeenCalledTimes(1); expect(pluginInstance.connectStub).toHaveBeenCalledTimes(1); expect(pluginInstance.disconnectStub).toHaveBeenCalledTimes(1); }); test('it can send messages from sandy clients', async () => { let testMethodCalledWith: any = undefined; const {client} = await createMockFlipperWithPlugin(TestPlugin, { onSend(method, params) { if (method === 'execute') { testMethodCalledWith = params; return {}; } }, }); const pluginInstance: PluginApi = client.sandyPluginStates.get(TestPlugin.id)! .instanceApi; // without rendering, non-bg plugins won't connect automatically, client.initPlugin(TestPlugin.id); await pluginInstance.send('test', {test: 3}); expect(testMethodCalledWith).toMatchInlineSnapshot(` Object { "api": "TestPlugin", "method": "test", "params": Object { "test": 3, }, } `); }); test('it should initialize "Navigation" plugin if not enabled', async () => { const {client, store} = await createMockFlipperWithPlugin(TestPlugin, { supportedPlugins: ['Navigation'], }); const Plugin2 = new _SandyPluginDefinition( TestUtils.createMockPluginDetails({ name: 'Plugin2', id: 'Navigation', }), { plugin: jest.fn().mockImplementation(plugin), Component() { return null; }, }, ); const pluginState1 = client.sandyPluginStates.get(TestPlugin.id); expect(pluginState1).toBeInstanceOf(_SandyPluginInstance); store.dispatch(registerPlugins([Plugin2])); await client.refreshPlugins(); // not enabled, but Navigation is an exception, so we still get an instance const origInstance = client.sandyPluginStates.get(Plugin2.id); expect(origInstance).toBeDefined(); expect(Plugin2.asPluginModule().plugin).toBeCalledTimes(1); store.dispatch( starPlugin({ plugin: Plugin2, selectedApp: client.query.app, }), ); expect(client.sandyPluginStates.get(Plugin2.id)).toBe(origInstance); const instance = client.sandyPluginStates.get(Plugin2.id)! .instanceApi as PluginApi; expect(Plugin2.asPluginModule().plugin).toBeCalledTimes(1); expect(instance.destroyStub).toHaveBeenCalledTimes(0); // disable plugin again store.dispatch( starPlugin({ plugin: Plugin2, selectedApp: client.query.app, }), ); // stil enabled expect(client.sandyPluginStates.get(Plugin2.id)).toBe(origInstance); expect(instance.connectStub).toHaveBeenCalledTimes(0); // disconnect wasn't called because connect was never called expect(instance.disconnectStub).toHaveBeenCalledTimes(0); expect(instance.destroyStub).toHaveBeenCalledTimes(0); // closing does stop the plugin! client.destroy(); expect(instance.destroyStub).toHaveBeenCalledTimes(1); expect(client.sandyPluginStates.get(Plugin2.id)).toBeUndefined(); });