From f8ff6dc3933e42a8c68e63a71bf57c178ae100e1 Mon Sep 17 00:00:00 2001 From: Michel Weststrate Date: Tue, 4 Aug 2020 07:05:57 -0700 Subject: [PATCH] added deeplink support to sandy device plugins Summary: Make sure device plugins can be deeplinked as well. (note that the duplication between `Plugin` and `DevicePlugin` is cleaned up again in D22727089, first wanted to make it work and tested, then clean) DeepLink no longer have to be strings, per popular requests, as that makes direct linking between plugins easier (online links from the outside world have to arrive as strings) Reviewed By: jknoxville, nikoant Differential Revision: D22727091 fbshipit-source-id: 523c90b1e1fbf3700fdb4f62699dd57070cbc980 --- desktop/app/src/PluginContainer.tsx | 2 +- .../src/__tests__/PluginContainer.node.tsx | 141 ++++++++++++++++++ desktop/app/src/devices/BaseDevice.tsx | 5 +- .../src/__tests__/test-utils.node.tsx | 23 +++ .../src/plugin/DevicePlugin.tsx | 22 ++- .../src/test-utils/test-utils.tsx | 7 + 6 files changed, 195 insertions(+), 5 deletions(-) diff --git a/desktop/app/src/PluginContainer.tsx b/desktop/app/src/PluginContainer.tsx index 1014275a1..23b1c8334 100644 --- a/desktop/app/src/PluginContainer.tsx +++ b/desktop/app/src/PluginContainer.tsx @@ -180,7 +180,7 @@ class PluginContainer extends PureComponent { this.processMessageQueue(); // make sure deeplinks are propagated const {deepLinkPayload, target, activePlugin} = this.props; - if (deepLinkPayload && target instanceof Client && activePlugin) { + if (deepLinkPayload && activePlugin && target) { target.sandyPluginStates .get(activePlugin.id) ?.triggerDeepLink(deepLinkPayload); diff --git a/desktop/app/src/__tests__/PluginContainer.node.tsx b/desktop/app/src/__tests__/PluginContainer.node.tsx index 28d7d4596..d09b0646a 100644 --- a/desktop/app/src/__tests__/PluginContainer.node.tsx +++ b/desktop/app/src/__tests__/PluginContainer.node.tsx @@ -505,3 +505,144 @@ test('PluginContainer can render Sandy device plugins', async () => { expect(pluginInstance.activatedStub).toBeCalledTimes(2); expect(pluginInstance.deactivatedStub).toBeCalledTimes(1); }); + +test('PluginContainer + Sandy device plugin supports deeplink', async () => { + const linksSeen: any[] = []; + + const devicePlugin = (client: DevicePluginClient) => { + const linkState = createState(''); + client.onDeepLink((link) => { + linksSeen.push(link); + linkState.set(String(link)); + }); + return { + linkState, + }; + }; + + const definition = new SandyPluginDefinition( + TestUtils.createMockPluginDetails(), + { + devicePlugin, + supportsDevice: () => true, + Component() { + const instance = usePlugin(devicePlugin); + const linkState = useValue(instance.linkState); + return

hello {linkState || 'world'}

; + }, + }, + ); + const {renderer, act, store} = await renderMockFlipperWithPlugin(definition); + + const theUniverse = { + thisIs: 'theUniverse', + toString() { + return JSON.stringify({...this}); + }, + }; + + expect(linksSeen).toEqual([]); + expect(renderer.baseElement).toMatchInlineSnapshot(` + +
+
+

+ hello + world +

+
+
+
+ + `); + + act(() => { + store.dispatch( + selectPlugin({ + selectedPlugin: definition.id, + deepLinkPayload: theUniverse, + selectedApp: null, + }), + ); + }); + + expect(linksSeen).toEqual([theUniverse]); + expect(renderer.baseElement).toMatchInlineSnapshot(` + +
+
+

+ hello + {"thisIs":"theUniverse"} +

+
+
+
+ + `); + + // Sending same link doesn't trigger again + act(() => { + store.dispatch( + selectPlugin({ + selectedPlugin: definition.id, + deepLinkPayload: theUniverse, + selectedApp: null, + }), + ); + }); + expect(linksSeen).toEqual([theUniverse]); + + // ...nor does a random other store update that does trigger a plugin container render + act(() => { + store.dispatch( + updateSettings({ + ...store.getState().settingsState, + }), + ); + }); + expect(linksSeen).toEqual([theUniverse]); + + // Different link does trigger again + act(() => { + store.dispatch( + selectPlugin({ + selectedPlugin: definition.id, + deepLinkPayload: 'london!', + selectedApp: null, + }), + ); + }); + expect(linksSeen).toEqual([theUniverse, 'london!']); + + // and same link does trigger if something else was selected in the mean time + act(() => { + store.dispatch( + selectPlugin({ + selectedPlugin: 'Logs', + deepLinkPayload: 'london!', + selectedApp: null, + }), + ); + }); + act(() => { + store.dispatch( + selectPlugin({ + selectedPlugin: definition.id, + deepLinkPayload: 'london!', + selectedApp: null, + }), + ); + }); + expect(linksSeen).toEqual([theUniverse, 'london!', 'london!']); +}); diff --git a/desktop/app/src/devices/BaseDevice.tsx b/desktop/app/src/devices/BaseDevice.tsx index 42ac4f503..423e1221b 100644 --- a/desktop/app/src/devices/BaseDevice.tsx +++ b/desktop/app/src/devices/BaseDevice.tsx @@ -71,7 +71,10 @@ export default class BaseDevice { // sorted list of supported device plugins devicePlugins: string[] = []; - sandyPluginStates = new Map(); + sandyPluginStates: Map = new Map< + string, + SandyDevicePluginInstance + >(); supportsOS(os: OS) { return os.toLowerCase() === this.os.toLowerCase(); diff --git a/desktop/flipper-plugin/src/__tests__/test-utils.node.tsx b/desktop/flipper-plugin/src/__tests__/test-utils.node.tsx index bc97dee51..018dc056f 100644 --- a/desktop/flipper-plugin/src/__tests__/test-utils.node.tsx +++ b/desktop/flipper-plugin/src/__tests__/test-utils.node.tsx @@ -11,6 +11,7 @@ import * as TestUtils from '../test-utils/test-utils'; import * as testPlugin from './TestPlugin'; import {createState} from '../state/atom'; import {FlipperClient} from '../plugin/Plugin'; +import {DevicePluginClient} from '../plugin/DevicePlugin'; test('it can start a plugin and lifecycle events', () => { const {instance, ...p} = TestUtils.startPlugin(testPlugin); @@ -215,3 +216,25 @@ test('plugins can receive deeplinks', async () => { plugin.triggerDeepLink('test'); expect(plugin.instance.field1.get()).toBe('test'); }); + +test('device plugins can receive deeplinks', async () => { + const plugin = TestUtils.startDevicePlugin({ + devicePlugin(client: DevicePluginClient) { + client.onDeepLink((deepLink) => { + if (typeof deepLink === 'string') { + field1.set(deepLink); + } + }); + const field1 = createState('', {persist: 'test'}); + return {field1}; + }, + supportsDevice: () => true, + Component() { + return null; + }, + }); + + expect(plugin.instance.field1.get()).toBe(''); + plugin.triggerDeepLink('test'); + expect(plugin.instance.field1.get()).toBe('test'); +}); diff --git a/desktop/flipper-plugin/src/plugin/DevicePlugin.tsx b/desktop/flipper-plugin/src/plugin/DevicePlugin.tsx index 930b9f086..7315ace1a 100644 --- a/desktop/flipper-plugin/src/plugin/DevicePlugin.tsx +++ b/desktop/flipper-plugin/src/plugin/DevicePlugin.tsx @@ -61,7 +61,10 @@ export interface DevicePluginClient { */ onDeactivate(cb: () => void): void; - // TODO: support onDeeplink! + /** + * Triggered when this plugin is opened through a deeplink + */ + onDeepLink(cb: (deepLink: unknown) => void): void; } export interface RealFlipperDevice { @@ -91,6 +94,8 @@ export class SandyDevicePluginInstance { initialStates?: Record; // all the atoms that should be serialized when making an export / import rootStates: Record> = {}; + // last seen deeplink + lastDeeplink?: any; constructor( realDevice: RealFlipperDevice, @@ -120,6 +125,9 @@ export class SandyDevicePluginInstance { onDeactivate: (cb) => { this.events.on('deactivate', cb); }, + onDeepLink: (callback) => { + this.events.on('deeplink', callback); + }, }; setCurrentPluginInstance(this); this.initialStates = initialStates; @@ -143,8 +151,8 @@ export class SandyDevicePluginInstance { } deactivate() { - this.assertNotDestroyed(); - if (this.activated) { + if (!this.destroyed && this.activated) { + this.lastDeeplink = undefined; this.activated = false; this.events.emit('deactivate'); } @@ -161,6 +169,14 @@ export class SandyDevicePluginInstance { return '[SandyDevicePluginInstance]'; } + triggerDeepLink(deepLink: unknown) { + this.assertNotDestroyed(); + if (deepLink !== this.lastDeeplink) { + this.lastDeeplink = deepLink; + this.events.emit('deeplink', deepLink); + } + } + exportState() { return Object.fromEntries( Object.entries(this.rootStates).map(([key, atom]) => [key, atom.get()]), diff --git a/desktop/flipper-plugin/src/test-utils/test-utils.tsx b/desktop/flipper-plugin/src/test-utils/test-utils.tsx index 47256ece2..0d17ed1bd 100644 --- a/desktop/flipper-plugin/src/test-utils/test-utils.tsx +++ b/desktop/flipper-plugin/src/test-utils/test-utils.tsx @@ -138,6 +138,10 @@ interface StartDevicePluginResult { * Emulates sending a log message arriving from the device */ sendLogEntry(logEntry: DeviceLogEntry): void; + /** + * Emulates triggering a deeplik + */ + triggerDeepLink(deeplink: unknown): void; /** * Grabs the current (exportable) state */ @@ -278,6 +282,9 @@ export function startDevicePlugin( }); }, exportState: () => pluginInstance.exportState(), + triggerDeepLink: (deepLink: unknown) => { + pluginInstance.triggerDeepLink(deepLink); + }, }; // @ts-ignore res._backingInstance = pluginInstance;