Files
flipper/desktop/app/src/reducers/__tests__/sandyplugins.node.tsx
Anton Nikolaev 24aed8fd45 Command processing (2/n): testing
Summary:
*Stack summary*: this stack refactors plugin management actions to perform them in a dispatcher rather than in the root reducer (store.tsx) as all of these actions has side effects. To do that, we store requested plugin management actions (install/update/uninstall, star/unstar) in a queue which is then handled by pluginManager dispatcher. This dispatcher then dispatches all required state updates.

*Diff summary*: refactored Flipper mocking helpers to allow testing of plugin commands, and wrote some tests for pluginManager.

Reviewed By: mweststrate

Differential Revision: D26450344

fbshipit-source-id: 0e8414517cc1ad353781dffd7ffb4a5f9a815d38
2021-02-16 10:50:17 -08:00

312 lines
9.8 KiB
TypeScript

/**
* 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<any, any>) {
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<typeof plugin>;
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();
});