Added unit tests to protect against broken startup sequence

Summary: Add unit tests to test errors thrown from plugins during initialisation, as follow up for D31127969 (a72e46c792). From the 4 tests cases added (first load plugin then device, device then plugin, first load plugin then client, first client then plugin), the first case was indeed failing before the mentioned diff and not registering the device.

Reviewed By: passy

Differential Revision: D31142583

fbshipit-source-id: 8e44c667316192231d1bb5e4d76c5bf1207ba835
This commit is contained in:
Michel Weststrate
2021-09-27 12:15:17 -07:00
committed by Facebook GitHub Bot
parent 12865fd0bc
commit f98526a2c9
4 changed files with 138 additions and 3 deletions

View File

@@ -9,15 +9,23 @@
import reducer from '../connections'; import reducer from '../connections';
import {State, selectPlugin} from '../connections'; import {State, selectPlugin} from '../connections';
import {_setFlipperLibImplementation} from 'flipper-plugin'; import {
import {createMockFlipperLib} from 'flipper-plugin/src/test-utils/test-utils'; _SandyPluginDefinition,
_setFlipperLibImplementation,
TestUtils,
MockedConsole,
} from 'flipper-plugin';
import {TestDevice} from '../../test-utils/TestDevice'; import {TestDevice} from '../../test-utils/TestDevice';
import {createMockFlipperWithPlugin} from '../../test-utils/createMockFlipperWithPlugin';
let mockedConsole: MockedConsole;
beforeEach(() => { beforeEach(() => {
_setFlipperLibImplementation(createMockFlipperLib()); mockedConsole = TestUtils.mockConsole();
_setFlipperLibImplementation(TestUtils.createMockFlipperLib());
}); });
afterEach(() => { afterEach(() => {
mockedConsole.unmock();
_setFlipperLibImplementation(undefined); _setFlipperLibImplementation(undefined);
}); });
@@ -79,3 +87,88 @@ test('selectPlugin sets deepLinkPayload correctly', () => {
); );
expect(state.deepLinkPayload).toBe('myPayload'); expect(state.deepLinkPayload).toBe('myPayload');
}); });
test('can handle plugins that throw at start', async () => {
const TestPlugin = new _SandyPluginDefinition(
TestUtils.createMockPluginDetails(),
{
Component() {
return null;
},
plugin() {
throw new Error('Broken plugin');
},
},
);
const {client, store, createClient, createDevice} =
await createMockFlipperWithPlugin(TestPlugin);
// not initialized
expect(client.sandyPluginStates.get(TestPlugin.id)).toBe(undefined);
expect(store.getState().connections.clients.length).toBe(1);
expect(client.connected.get()).toBe(true);
expect((console.error as any).mock.calls[0]).toMatchInlineSnapshot(`
Array [
"Failed to start plugin 'TestPlugin': ",
[Error: Broken plugin],
]
`);
const device2 = await createDevice({});
const client2 = await createClient(device2, client.query.app);
expect((console.error as any).mock.calls[1]).toMatchInlineSnapshot(`
Array [
"Failed to start plugin 'TestPlugin': ",
[Error: Broken plugin],
]
`);
expect(store.getState().connections.clients.length).toBe(2);
expect(client2.connected.get()).toBe(true);
expect(client2.sandyPluginStates.size).toBe(0);
});
test('can handle device plugins that throw at start', async () => {
const TestPlugin = new _SandyPluginDefinition(
TestUtils.createMockPluginDetails(),
{
Component() {
return null;
},
devicePlugin() {
throw new Error('Broken device plugin');
},
},
);
const {device, store, createDevice} = await createMockFlipperWithPlugin(
TestPlugin,
);
expect(mockedConsole.errorCalls[0]).toMatchInlineSnapshot(`
Array [
"Failed to start device plugin 'TestPlugin': ",
[Error: Broken device plugin],
]
`);
// not initialized
expect(device.sandyPluginStates.get(TestPlugin.id)).toBe(undefined);
expect(store.getState().connections.devices.length).toBe(1);
expect(device.connected.get()).toBe(true);
const device2 = await createDevice({});
expect(store.getState().connections.devices.length).toBe(2);
expect(device2.connected.get()).toBe(true);
expect(mockedConsole.errorCalls[1]).toMatchInlineSnapshot(`
Array [
"Failed to start device plugin 'TestPlugin': ",
[Error: Broken device plugin],
]
`);
expect(device2.sandyPluginStates.size).toBe(0);
});

View File

@@ -102,6 +102,7 @@ test('Correct top level API exposed', () => {
"LogTypes", "LogTypes",
"Logger", "Logger",
"MenuEntry", "MenuEntry",
"MockedConsole",
"NormalizedMenuEntry", "NormalizedMenuEntry",
"Notification", "Notification",
"PluginClient", "PluginClient",

View File

@@ -13,6 +13,7 @@ export const styled = styledImport;
import './plugin/PluginBase'; import './plugin/PluginBase';
import * as TestUtilites from './test-utils/test-utils'; import * as TestUtilites from './test-utils/test-utils';
export {MockedConsole} from './test-utils/test-utils';
export { export {
SandyPluginInstance as _SandyPluginInstance, SandyPluginInstance as _SandyPluginInstance,

View File

@@ -40,6 +40,7 @@ import {FlipperLib} from '../plugin/FlipperLib';
import {stubLogger} from '../utils/Logger'; import {stubLogger} from '../utils/Logger';
import {Idler} from '../utils/Idler'; import {Idler} from '../utils/Idler';
import {createState} from '../state/atom'; import {createState} from '../state/atom';
import baseMockConsole from 'jest-mock-console';
type Renderer = RenderResult<typeof queries>; type Renderer = RenderResult<typeof queries>;
@@ -532,3 +533,42 @@ function createStubIdler(): Idler {
}, },
}; };
} }
/**
* Mockes the current console. Inspect results through e.g.
* console.errorCalls etc.
*
* Or, alternatively, expect(mockedConsole.error).toBeCalledWith...
*
* Don't forgot to call .unmock when done!
*/
export function mockConsole() {
const restoreConsole = baseMockConsole();
// The mocked console methods, make sure they remain available after unmocking
const {log, error, warn} = console as any;
return {
get logCalls(): any[][] {
return log.mock.calls;
},
get errorCalls(): any[][] {
return error.mock.calls;
},
get warnCalls(): any[][] {
return warn.mock.calls;
},
get log(): jest.Mock<any, any> {
return log as any;
},
get warn(): jest.Mock<any, any> {
return warn as any;
},
get error(): jest.Mock<any, any> {
return error as any;
},
unmock() {
restoreConsole();
},
};
}
export type MockedConsole = ReturnType<typeof mockConsole>;