Make sure Sandy Device Plugins can be unit testsed

Summary: Add unit tests to verify that the unit test utilities for for Sandy device plugins work as expected. Fixed a bug revealed by that and cleaned up some TODO's

Reviewed By: jknoxville, passy, nikoant

Differential Revision: D22693928

fbshipit-source-id: 93162db19d826d0cd7f642cef1447fd756261ac8
This commit is contained in:
Michel Weststrate
2020-08-04 07:05:57 -07:00
committed by Facebook GitHub Bot
parent 91ed4e31c0
commit 1e956e1bf5
5 changed files with 249 additions and 24 deletions

View File

@@ -265,6 +265,7 @@ test('requirePlugin loads valid Sandy plugin', () => {
title: 'Sample', title: 'Sample',
version: '1.0.0', version: '1.0.0',
}); });
expect(plugin.isDevicePlugin).toBe(false);
expect(typeof plugin.module.Component).toBe('function'); expect(typeof plugin.module.Component).toBe('function');
expect(plugin.module.Component.displayName).toBe('FlipperPlugin(Sample)'); expect(plugin.module.Component.displayName).toBe('FlipperPlugin(Sample)');
expect(typeof plugin.asPluginModule().plugin).toBe('function'); expect(typeof plugin.asPluginModule().plugin).toBe('function');
@@ -286,3 +287,38 @@ test('requirePlugin errors on invalid Sandy plugin', () => {
`"Flipper plugin 'Sample' should export named function called 'plugin'"`, `"Flipper plugin 'Sample' should export named function called 'plugin'"`,
); );
}); });
test('requirePlugin loads valid Sandy Device plugin', () => {
const name = 'pluginID';
const requireFn = requirePlugin([], {}, require);
const plugin = requireFn({
...samplePluginDetails,
name,
entry: path.join(
__dirname,
'../../../../flipper-plugin/src/__tests__/DeviceTestPlugin',
),
version: '1.0.0',
flipperSDKVersion: '0.0.0',
}) as SandyPluginDefinition;
expect(plugin).not.toBeNull();
// @ts-ignore
expect(plugin).toBeInstanceOf(SandyPluginDefinition);
expect(plugin.id).toBe('Sample');
expect(plugin.details).toMatchObject({
flipperSDKVersion: '0.0.0',
id: 'Sample',
isDefault: false,
main: 'dist/bundle.js',
name: 'pluginID',
source: 'src/index.js',
specVersion: 2,
title: 'Sample',
version: '1.0.0',
});
expect(plugin.isDevicePlugin).toBe(true);
expect(typeof plugin.module.Component).toBe('function');
expect(plugin.module.Component.displayName).toBe('FlipperPlugin(Sample)');
expect(typeof plugin.asDevicePluginModule().devicePlugin).toBe('function');
expect(typeof plugin.asDevicePluginModule().supportsDevice).toBe('function');
});

View File

@@ -0,0 +1,60 @@
/**
* 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 * as React from 'react';
import {DevicePluginClient, Device} from '../plugin/DevicePlugin';
import {usePlugin} from '../plugin/PluginContext';
import {createState, useValue} from '../state/atom';
export function supportsDevice(_device: Device) {
return true;
}
export function devicePlugin(client: DevicePluginClient) {
const logStub = jest.fn();
const activateStub = jest.fn();
const deactivateStub = jest.fn();
const destroyStub = jest.fn();
const state = createState(
{
count: 0,
},
{
persist: 'counter',
},
);
client.device.onLogEntry((entry) => {
state.update((d) => {
d.count++;
});
logStub(entry);
});
client.onActivate(activateStub);
client.onDeactivate(deactivateStub);
client.onDestroy(destroyStub);
return {
logStub,
activateStub,
deactivateStub,
destroyStub,
state,
};
}
export function Component() {
const api = usePlugin(devicePlugin);
const count = useValue(api.state).count;
// @ts-expect-error
api.bla;
return <h1>Hi from test plugin {count}</h1>;
}

View File

@@ -0,0 +1,151 @@
/**
* 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 * as TestUtils from '../test-utils/test-utils';
import * as testPlugin from './DeviceTestPlugin';
import {createState} from '../state/atom';
const testLogMessage = {
date: new Date(),
message: 'test',
pid: 0,
tid: 0,
tag: 'bla',
type: 'warn',
app: 'TestApp',
} as const;
test('it can start a device plugin and listen to lifecycle events', () => {
const {instance, ...p} = TestUtils.startDevicePlugin(testPlugin);
// @ts-expect-error
p.bla;
// @ts-expect-error
instance.bla;
// startPlugin starts activated
expect(instance.activateStub).toBeCalledTimes(1);
expect(instance.deactivateStub).toBeCalledTimes(0);
expect(instance.destroyStub).toBeCalledTimes(0);
// calling activate is a noop
p.activate();
expect(instance.activateStub).toBeCalledTimes(1);
expect(instance.deactivateStub).toBeCalledTimes(0);
expect(instance.destroyStub).toBeCalledTimes(0);
p.sendLogEntry(testLogMessage);
expect(instance.logStub).toBeCalledWith(testLogMessage);
expect(instance.state.get().count).toBe(1);
expect(instance.activateStub).toBeCalledTimes(1);
expect(instance.deactivateStub).toBeCalledTimes(0);
expect(instance.destroyStub).toBeCalledTimes(0);
p.deactivate();
p.activate();
expect(instance.activateStub).toBeCalledTimes(2);
expect(instance.deactivateStub).toBeCalledTimes(1);
expect(instance.destroyStub).toBeCalledTimes(0);
p.destroy();
expect(instance.activateStub).toBeCalledTimes(2);
expect(instance.deactivateStub).toBeCalledTimes(2);
expect(instance.destroyStub).toBeCalledTimes(1);
// cannot interact with destroyed plugin
expect(() => {
p.activate();
}).toThrowErrorMatchingInlineSnapshot(`"Plugin has been destroyed already"`);
});
test('it can render a device plugin', () => {
const {renderer, instance, sendLogEntry} = TestUtils.renderDevicePlugin(
testPlugin,
);
expect(renderer.baseElement).toMatchInlineSnapshot(`
<body>
<div>
<h1>
Hi from test plugin
0
</h1>
</div>
</body>
`);
sendLogEntry(testLogMessage);
expect(renderer.baseElement).toMatchInlineSnapshot(`
<body>
<div>
<h1>
Hi from test plugin
1
</h1>
</div>
</body>
`);
// @ts-ignore
expect(instance.state.listeners.length).toBe(1);
renderer.unmount();
// @ts-ignore
expect(instance.state.listeners.length).toBe(0);
});
test('device plugins support non-serializable state', async () => {
const {exportState} = TestUtils.startPlugin({
plugin() {
const field1 = createState(true);
const field2 = createState(
{
test: 3,
},
{
persist: 'field2',
},
);
return {
field1,
field2,
};
},
Component() {
return null;
},
});
// states are serialized in creation order
expect(exportState()).toEqual({field2: {test: 3}});
});
test('device plugins support restoring state', async () => {
const {exportState} = TestUtils.startPlugin(
{
plugin() {
const field1 = createState(1, {persist: 'field1'});
const field2 = createState(2);
const field3 = createState(3, {persist: 'field3'});
expect(field1.get()).toBe('a');
expect(field2.get()).toBe(2);
expect(field3.get()).toBe('b');
return {};
},
Component() {
return null;
},
},
{
initialState: {field1: 'a', field3: 'b'},
},
);
expect(exportState()).toEqual({field1: 'a', field3: 'b'});
});

View File

@@ -137,28 +137,9 @@ export class SandyDevicePluginInstance {
this.activated = true; this.activated = true;
this.events.emit('activate'); this.events.emit('activate');
} }
// TODO:
// const pluginId = this.definition.id;
// if (!this.realClient.isBackgroundPlugin(pluginId)) {
// this.realClient.initPlugin(pluginId); // will call connect() if needed
// }
} }
// the plugin is deselected in the UI
deactivate() { deactivate() {
// TODO:
// if (this.destroyed) {
// // this can happen if the plugin is disabled while active in the UI.
// // In that case deinit & destroy is already triggered from the STAR_PLUGIN action
// return;
// }
// const pluginId = this.definition.id;
// if (!this.realClient.isBackgroundPlugin(pluginId)) {
// this.realClient.deinitPlugin(pluginId);
// }
}
disconnect() {
this.assertNotDestroyed(); this.assertNotDestroyed();
if (this.activated) { if (this.activated) {
this.activated = false; this.activated = false;
@@ -168,10 +149,7 @@ export class SandyDevicePluginInstance {
destroy() { destroy() {
this.assertNotDestroyed(); this.assertNotDestroyed();
// TODO: this.deactivate();
// if (this.activated) {
// this.realClient.deinitPlugin(this.definition.id);
// }
this.events.emit('destroy'); this.events.emit('destroy');
this.destroyed = true; this.destroyed = true;
} }

View File

@@ -251,7 +251,7 @@ export function startDevicePlugin<Module extends FlipperDevicePluginModule>(
createMockPluginDetails(), createMockPluginDetails(),
module, module,
); );
if (definition.isDevicePlugin) { if (!definition.isDevicePlugin) {
throw new Error( throw new Error(
'Use `startPlugin` or `renderPlugin` to test non-device plugins', 'Use `startPlugin` or `renderPlugin` to test non-device plugins',
); );