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:
committed by
Facebook GitHub Bot
parent
91ed4e31c0
commit
1e956e1bf5
@@ -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');
|
||||||
|
});
|
||||||
|
|||||||
60
desktop/flipper-plugin/src/__tests__/DeviceTestPlugin.tsx
Normal file
60
desktop/flipper-plugin/src/__tests__/DeviceTestPlugin.tsx
Normal 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>;
|
||||||
|
}
|
||||||
151
desktop/flipper-plugin/src/__tests__/test-utils-device.node.tsx
Normal file
151
desktop/flipper-plugin/src/__tests__/test-utils-device.node.tsx
Normal 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'});
|
||||||
|
});
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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',
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user