Introduce isPluginAvailable and selectPlugin

Summary:
Introduced API to replace the deprecated `selectPlugin` in Sandy.

The API can be used to navigate from `device plugin -> device plugin`, or` client plugin -> device / client plugin`

Introduced `isPluginAvailable` as well, so that the user interaction an be fine tuned in case the plugin is not disabled.

Reviewed By: jknoxville

Differential Revision: D25422370

fbshipit-source-id: c6c603f1c68e6291280b3d0883e474448754ded1
This commit is contained in:
Michel Weststrate
2020-12-09 14:34:46 -08:00
committed by Facebook GitHub Bot
parent 02a56da3f5
commit 52862f6083
9 changed files with 266 additions and 15 deletions

View File

@@ -876,3 +876,128 @@ test('PluginContainer + Sandy device plugin supports deeplink', async () => {
});
expect(linksSeen).toEqual([theUniverse, 'london!', 'london!']);
});
test('Sandy plugins support isPluginSupported + selectPlugin', async () => {
let renders = 0;
const linksSeen: any[] = [];
function MySandyPlugin() {
renders++;
return <h1>Plugin1</h1>;
}
const plugin = (client: PluginClient) => {
const activatedStub = jest.fn();
const deactivatedStub = jest.fn();
client.onDeepLink((link) => {
linksSeen.push(link);
});
client.onActivate(activatedStub);
client.onDeactivate(deactivatedStub);
return {
activatedStub,
deactivatedStub,
isPluginAvailable: client.isPluginAvailable,
selectPlugin: client.selectPlugin,
};
};
const definition = new _SandyPluginDefinition(
TestUtils.createMockPluginDetails({id: 'base'}),
{
plugin,
Component: MySandyPlugin,
},
);
const definition2 = new _SandyPluginDefinition(
TestUtils.createMockPluginDetails({id: 'other'}),
{
plugin() {
return {};
},
Component() {
return <h1>Plugin2</h1>;
},
},
);
const definition3 = new _SandyPluginDefinition(
TestUtils.createMockPluginDetails({id: 'device'}),
{
supportsDevice() {
return true;
},
devicePlugin() {
return {};
},
Component() {
return <h1>Plugin3</h1>;
},
},
);
const {renderer, client, store} = await renderMockFlipperWithPlugin(
definition,
{
additionalPlugins: [definition2, definition3],
},
);
expect(renderer.baseElement.querySelector('h1')).toMatchInlineSnapshot(`
<h1>
Plugin1
</h1>
`);
expect(renders).toBe(1);
const pluginInstance: ReturnType<typeof plugin> = client.sandyPluginStates.get(
definition.id,
)!.instanceApi;
expect(pluginInstance.isPluginAvailable(definition.id)).toBeTruthy();
expect(pluginInstance.isPluginAvailable('nonsense')).toBeFalsy();
expect(pluginInstance.isPluginAvailable(definition2.id)).toBeFalsy(); // not enabled yet
expect(pluginInstance.isPluginAvailable(definition3.id)).toBeTruthy();
expect(pluginInstance.activatedStub).toBeCalledTimes(1);
expect(pluginInstance.deactivatedStub).toBeCalledTimes(0);
expect(linksSeen).toEqual([]);
// open a device plugin
pluginInstance.selectPlugin(definition3.id);
expect(store.getState().connections.selectedPlugin).toBe(definition3.id);
expect(renderer.baseElement.querySelector('h1')).toMatchInlineSnapshot(`
<h1>
Plugin3
</h1>
`);
expect(pluginInstance.deactivatedStub).toBeCalledTimes(1);
// go back by opening own plugin again (funny, but why not)
pluginInstance.selectPlugin(definition.id, 'data');
expect(store.getState().connections.selectedPlugin).toBe(definition.id);
expect(pluginInstance.activatedStub).toBeCalledTimes(2);
expect(renderer.baseElement.querySelector('h1')).toMatchInlineSnapshot(`
<h1>
Plugin1
</h1>
`);
expect(linksSeen).toEqual(['data']);
// try to go to plugin 2, fails (not starred, so no-op)
pluginInstance.selectPlugin(definition2.id);
expect(store.getState().connections.selectedPlugin).toBe(definition.id);
// star plugin 2 and navigate to plugin 2
store.dispatch(
starPlugin({
plugin: definition2,
selectedApp: client.query.app,
}),
);
pluginInstance.selectPlugin(definition2.id);
expect(store.getState().connections.selectedPlugin).toBe(definition2.id);
expect(pluginInstance.deactivatedStub).toBeCalledTimes(2);
expect(renderer.baseElement.querySelector('h1')).toMatchInlineSnapshot(`
<h1>
Plugin2
</h1>
`);
expect(renders).toBe(2);
});

View File

@@ -9,9 +9,9 @@
import {produce} from 'immer';
import BaseDevice from '../devices/BaseDevice';
import type BaseDevice from '../devices/BaseDevice';
import MacDevice from '../devices/MacDevice';
import Client from '../Client';
import type Client from '../Client';
import {UninitializedClient} from '../UninitializedClient';
import {isEqual} from 'lodash';
import {performance} from 'perf_hooks';

View File

@@ -16,7 +16,6 @@ import {
act as testingLibAct,
} from '@testing-library/react';
import {queries} from '@testing-library/dom';
import {TestUtils} from 'flipper-plugin';
import {
selectPlugin,
@@ -37,7 +36,7 @@ import {registerPlugins} from '../reducers/plugins';
import PluginContainer from '../PluginContainer';
import {getPluginKey, isDevicePluginDefinition} from '../utils/pluginUtils';
import {getInstance} from '../fb-stubs/Logger';
import {setFlipperLibImplementation} from '../utils/flipperLibImplementation';
import {initializeFlipperLibImplementation} from '../utils/flipperLibImplementation';
export type MockFlipperResult = {
client: Client;
@@ -55,7 +54,8 @@ type MockOptions = Partial<{
* can be used to intercept outgoing calls. If it returns undefined
* the base implementation will be used
*/
onSend(pluginId: string, method: string, params?: object): any;
onSend?: (pluginId: string, method: string, params?: object) => any;
additionalPlugins?: PluginDefinition[];
}>;
export async function createMockFlipperWithPlugin(
@@ -64,9 +64,10 @@ export async function createMockFlipperWithPlugin(
): Promise<MockFlipperResult> {
const store = createStore(rootReducer);
const logger = getInstance();
setFlipperLibImplementation(TestUtils.createMockFlipperLib());
store.dispatch(registerPlugins([pluginClazz]));
initializeFlipperLibImplementation(store, logger);
store.dispatch(
registerPlugins([pluginClazz, ...(options?.additionalPlugins ?? [])]),
);
function createDevice(serial: string): BaseDevice {
const device = new BaseDevice(

View File

@@ -12,18 +12,18 @@ import type {Logger} from '../fb-interfaces/Logger';
import type {Store} from '../reducers';
import createPaste from '../fb-stubs/createPaste';
import GK from '../fb-stubs/GK';
import {getInstance} from '../fb-stubs/Logger';
import type BaseDevice from '../devices/BaseDevice';
let flipperLibInstance: FlipperLib | undefined;
export function initializeFlipperLibImplementation(
_store: Store,
_logger: Logger,
store: Store,
logger: Logger,
) {
// late require to avoid cyclic dependency
const {addSandyPluginEntries} = require('../MenuBar');
flipperLibInstance = {
logger: getInstance(),
logger,
enableMenuEntries(entries) {
addSandyPluginEntries(entries);
},
@@ -31,6 +31,44 @@ export function initializeFlipperLibImplementation(
GK(gatekeeper: string) {
return GK.get(gatekeeper);
},
isPluginAvailable(device, client, pluginId) {
// supported device pluin
if (device.devicePlugins.includes(pluginId)) {
return true;
}
if (client) {
// plugin supported?
if (client.plugins.includes(pluginId)) {
// part of an archived device?
if (device.isArchived) {
return true;
}
// plugin enabled?
if (
store
.getState()
.connections.userStarredPlugins[client.query.app]?.includes(
pluginId,
)
) {
return true;
}
}
}
return false;
},
selectPlugin(device, client, pluginId, deeplink) {
store.dispatch({
type: 'SELECT_PLUGIN',
payload: {
selectedPlugin: pluginId,
selectedDevice: device as BaseDevice,
selectedApp: client ? client.id : null,
deepLinkPayload: deeplink,
time: Date.now(),
},
});
},
};
}