diff --git a/desktop/app/src/PluginContainer.tsx b/desktop/app/src/PluginContainer.tsx index f52683975..1014275a1 100644 --- a/desktop/app/src/PluginContainer.tsx +++ b/desktop/app/src/PluginContainer.tsx @@ -343,18 +343,13 @@ class PluginContainer extends PureComponent { } let pluginElement: null | React.ReactElement; if (isSandyPlugin(activePlugin)) { - if (target instanceof Client) { - // Make sure we throw away the container for different pluginKey! - pluginElement = ( - - ); - } else { - // TODO: target might be a device as well, support that T68738317 - pluginElement = null; - } + // Make sure we throw away the container for different pluginKey! + pluginElement = ( + + ); } else { const props: PluginProps & { key: string; diff --git a/desktop/app/src/__tests__/PluginContainer.node.tsx b/desktop/app/src/__tests__/PluginContainer.node.tsx index 429320622..28d7d4596 100644 --- a/desktop/app/src/__tests__/PluginContainer.node.tsx +++ b/desktop/app/src/__tests__/PluginContainer.node.tsx @@ -17,6 +17,8 @@ import { TestUtils, usePlugin, createState, + DevicePluginClient, + DeviceLogEntry, useValue, } from 'flipper-plugin'; import {selectPlugin, starPlugin} from '../reducers/connections'; @@ -367,3 +369,139 @@ test('PluginContainer + Sandy plugin supports deeplink', async () => { }); expect(linksSeen).toEqual(['universe!', 'london!', 'london!']); }); + +test('PluginContainer can render Sandy device plugins', async () => { + let renders = 0; + + function MySandyPlugin() { + renders++; + const sandyApi = usePlugin(devicePlugin); + expect(Object.keys(sandyApi)).toEqual([ + 'activatedStub', + 'deactivatedStub', + 'lastLogMessage', + ]); + expect(() => { + // eslint-disable-next-line + usePlugin(function bla() { + return {}; + }); + }).toThrowError(/didn't match the type of the requested plugin/); + const lastLogMessage = useValue(sandyApi.lastLogMessage); + return
Hello from Sandy: {lastLogMessage?.message}
; + } + + const devicePlugin = (client: DevicePluginClient) => { + const lastLogMessage = createState(undefined); + const activatedStub = jest.fn(); + const deactivatedStub = jest.fn(); + client.onActivate(activatedStub); + client.onDeactivate(deactivatedStub); + client.device.onLogEntry((e) => { + lastLogMessage.set(e); + }); + return {activatedStub, deactivatedStub, lastLogMessage}; + }; + + const definition = new SandyPluginDefinition( + TestUtils.createMockPluginDetails(), + { + supportsDevice: () => true, + devicePlugin, + Component: MySandyPlugin, + }, + ); + // any cast because this plugin is not enriched with the meta data that the plugin loader + // normally adds. Our further sandy plugin test infra won't need this, but + // for this test we do need to act a s a loaded plugin, to make sure PluginContainer itself can handle it + const {renderer, act, store, device} = await renderMockFlipperWithPlugin( + definition, + ); + + expect(renderer.baseElement).toMatchInlineSnapshot(` + +
+
+
+ Hello from Sandy: +
+
+
+
+ + `); + expect(renders).toBe(1); + + act(() => { + device.addLogEntry({ + date: new Date(), + message: 'helleuh', + pid: 0, + tid: 0, + type: 'info', + tag: 'test', + }); + }); + expect(renders).toBe(2); + + expect(renderer.baseElement).toMatchInlineSnapshot(` + +
+
+
+ Hello from Sandy: + helleuh +
+
+
+
+ + `); + + // make sure the plugin gets connected + const pluginInstance: ReturnType = device.sandyPluginStates.get( + definition.id, + )!.instanceApi; + expect(pluginInstance.activatedStub).toBeCalledTimes(1); + expect(pluginInstance.deactivatedStub).toBeCalledTimes(0); + + // select non existing plugin + act(() => { + store.dispatch( + selectPlugin({ + selectedPlugin: 'Logs', + deepLinkPayload: null, + }), + ); + }); + + expect(renderer.baseElement).toMatchInlineSnapshot(` + +
+ + `); + expect(pluginInstance.activatedStub).toBeCalledTimes(1); + expect(pluginInstance.deactivatedStub).toBeCalledTimes(1); + + // go back + act(() => { + store.dispatch( + selectPlugin({ + selectedPlugin: definition.id, + deepLinkPayload: null, + }), + ); + }); + expect(pluginInstance.activatedStub).toBeCalledTimes(2); + expect(pluginInstance.deactivatedStub).toBeCalledTimes(1); +}); diff --git a/desktop/app/src/test-utils/createMockFlipperWithPlugin.tsx b/desktop/app/src/test-utils/createMockFlipperWithPlugin.tsx index 8ff9cdc9e..3b5594aa1 100644 --- a/desktop/app/src/test-utils/createMockFlipperWithPlugin.tsx +++ b/desktop/app/src/test-utils/createMockFlipperWithPlugin.tsx @@ -216,12 +216,21 @@ export async function renderMockFlipperWithPlugin( function selectTestPlugin(store: Store, client: Client) { store.dispatch( - selectPlugin({ - selectedPlugin: pluginClazz.id, - selectedApp: client.query.app, - deepLinkPayload: null, - selectedDevice: store.getState().connections.selectedDevice!, - }), + selectPlugin( + isDevicePluginDefinition(pluginClazz) + ? { + selectedPlugin: pluginClazz.id, + selectedApp: null, + deepLinkPayload: null, + selectedDevice: store.getState().connections.selectedDevice!, + } + : { + selectedPlugin: pluginClazz.id, + selectedApp: client.query.app, + deepLinkPayload: null, + selectedDevice: store.getState().connections.selectedDevice!, + }, + ), ); } diff --git a/desktop/flipper-plugin/src/plugin/DevicePlugin.tsx b/desktop/flipper-plugin/src/plugin/DevicePlugin.tsx index 0e379e0d3..930b9f086 100644 --- a/desktop/flipper-plugin/src/plugin/DevicePlugin.tsx +++ b/desktop/flipper-plugin/src/plugin/DevicePlugin.tsx @@ -42,6 +42,7 @@ export type DevicePluginPredicate = (device: Device) => boolean; export type DevicePluginFactory = (client: DevicePluginClient) => object; +// TODO: better name? export interface DevicePluginClient { readonly device: Device; @@ -59,6 +60,8 @@ export interface DevicePluginClient { * The counterpart of the `onActivate` handler. */ onDeactivate(cb: () => void): void; + + // TODO: support onDeeplink! } export interface RealFlipperDevice {