Cleanup and some code reuse between Device- and normal Plugins

Summary: Introducing a base abstract class (blegh) to share some life cycle management between Device- and normal plugins. Cleaned up the test utils a bit as well + some old TODO's that now have been taken care of

Reviewed By: nikoant

Differential Revision: D22727089

fbshipit-source-id: 507816f1bfdbc6e7e71d4bd365b881b6710ca917
This commit is contained in:
Michel Weststrate
2020-08-04 07:05:57 -07:00
committed by Facebook GitHub Bot
parent b9c9e89b53
commit 642261c0d0
13 changed files with 275 additions and 353 deletions

View File

@@ -15,7 +15,7 @@ import {
SandyDevicePluginInstance, SandyDevicePluginInstance,
SandyPluginDefinition, SandyPluginDefinition,
} from 'flipper-plugin'; } from 'flipper-plugin';
import {DevicePluginMap} from '../plugin'; import {DevicePluginMap, FlipperDevicePlugin} from '../plugin';
export type DeviceShell = { export type DeviceShell = {
stdout: stream.Readable; stdout: stream.Readable;
@@ -179,18 +179,22 @@ export default class BaseDevice {
const plugins = Array.from(devicePlugins.values()); const plugins = Array.from(devicePlugins.values());
plugins.sort(sortPluginsByName); plugins.sort(sortPluginsByName);
for (const plugin of plugins) { for (const plugin of plugins) {
if (plugin instanceof SandyPluginDefinition) { this.loadDevicePlugin(plugin);
if (plugin.asDevicePluginModule().supportsDevice(this as any)) { }
this.devicePlugins.push(plugin.id); }
this.sandyPluginStates.set(
plugin.id, loadDevicePlugin(plugin: typeof FlipperDevicePlugin | SandyPluginDefinition) {
new SandyDevicePluginInstance(this, plugin), if (plugin instanceof SandyPluginDefinition) {
); // TODO: pass initial state if applicable if (plugin.asDevicePluginModule().supportsDevice(this as any)) {
} this.devicePlugins.push(plugin.id);
} else { this.sandyPluginStates.set(
if (plugin.supportsDevice(this)) { plugin.id,
this.devicePlugins.push(plugin.id); new SandyDevicePluginInstance(this, plugin),
} ); // TODO T70582933: pass initial state if applicable
}
} else {
if (plugin.supportsDevice(this)) {
this.devicePlugins.push(plugin.id);
} }
} }
} }

View File

@@ -277,7 +277,6 @@ const requirePluginInternal = (
if (pluginDetails.flipperSDKVersion) { if (pluginDetails.flipperSDKVersion) {
// Sandy plugin // Sandy plugin
// TODO: suppor device Plugins T68738317
return new SandyPluginDefinition(pluginDetails, plugin); return new SandyPluginDefinition(pluginDetails, plugin);
} else { } else {
// classic plugin // classic plugin

View File

@@ -23,7 +23,7 @@ const WelcomeScreen = isHeadless()
import NotificationScreen from '../chrome/NotificationScreen'; import NotificationScreen from '../chrome/NotificationScreen';
import SupportRequestFormV2 from '../fb-stubs/SupportRequestFormV2'; import SupportRequestFormV2 from '../fb-stubs/SupportRequestFormV2';
import SupportRequestDetails from '../fb-stubs/SupportRequestDetails'; import SupportRequestDetails from '../fb-stubs/SupportRequestDetails';
import {getPluginKey} from '../utils/pluginUtils'; import {getPluginKey, isDevicePluginDefinition} from '../utils/pluginUtils';
import {deconstructClientId} from '../utils/clientUtils'; import {deconstructClientId} from '../utils/clientUtils';
import {FlipperDevicePlugin, PluginDefinition, isSandyPlugin} from '../plugin'; import {FlipperDevicePlugin, PluginDefinition, isSandyPlugin} from '../plugin';
import {RegisterPluginAction} from './plugins'; import {RegisterPluginAction} from './plugins';
@@ -393,20 +393,10 @@ export default (state: State = INITAL_STATE, action: Actions): State => {
// plugins are registered after creating the base devices, so update them // plugins are registered after creating the base devices, so update them
const plugins = action.payload; const plugins = action.payload;
plugins.forEach((plugin) => { plugins.forEach((plugin) => {
// TODO: T68738317 support sandy device plugin if (isDevicePluginDefinition(plugin)) {
if (
!isSandyPlugin(plugin) &&
plugin.prototype instanceof FlipperDevicePlugin
) {
// smell: devices are mutable // smell: devices are mutable
state.devices.forEach((device) => { state.devices.forEach((device) => {
// @ts-ignore device.loadDevicePlugin(plugin);
if (plugin.supportsDevice(device)) {
device.devicePlugins = [
...(device.devicePlugins || []),
plugin.id,
];
}
}); });
} }
}); });

View File

@@ -662,7 +662,7 @@ async function getStoreExport(
const newPluginState = metadata.pluginStates; const newPluginState = metadata.pluginStates;
// TODO: support async export like fetchMetaData T68683476 // TODO: support async export like fetchMetaData T68683476
// TODO: support device plugins T68738317 // TODO: support device plugins T70582933
const pluginStates2 = pluginsToProcess const pluginStates2 = pluginsToProcess
? exportSandyPluginStates(pluginsToProcess) ? exportSandyPluginStates(pluginsToProcess)
: {}; : {};

View File

@@ -128,7 +128,6 @@ export function processMessagesLater(
case isSelected && getPendingMessages(store, pluginKey).length === 0: case isSelected && getPendingMessages(store, pluginKey).length === 0:
processMessagesImmediately(store, pluginKey, plugin, messages); processMessagesImmediately(store, pluginKey, plugin, messages);
break; break;
// TODO: support SandyDevicePlugin T68738317
case isSelected: case isSelected:
case plugin instanceof SandyPluginInstance: case plugin instanceof SandyPluginInstance:
case plugin instanceof FlipperDevicePlugin: case plugin instanceof FlipperDevicePlugin:

View File

@@ -8,7 +8,7 @@
*/ */
import * as React from 'react'; import * as React from 'react';
import {FlipperClient} from '../plugin/Plugin'; import {PluginClient} from '../plugin/Plugin';
import {usePlugin} from '../plugin/PluginContext'; import {usePlugin} from '../plugin/PluginContext';
import {createState, useValue} from '../state/atom'; import {createState, useValue} from '../state/atom';
@@ -22,7 +22,7 @@ type Methods = {
currentState(params: {since: number}): Promise<number>; currentState(params: {since: number}): Promise<number>;
}; };
export function plugin(client: FlipperClient<Events, Methods>) { export function plugin(client: PluginClient<Events, Methods>) {
const connectStub = jest.fn(); const connectStub = jest.fn();
const disconnectStub = jest.fn(); const disconnectStub = jest.fn();
const activateStub = jest.fn(); const activateStub = jest.fn();

View File

@@ -10,7 +10,7 @@
import * as TestUtils from '../test-utils/test-utils'; import * as TestUtils from '../test-utils/test-utils';
import * as testPlugin from './TestPlugin'; import * as testPlugin from './TestPlugin';
import {createState} from '../state/atom'; import {createState} from '../state/atom';
import {FlipperClient} from '../plugin/Plugin'; import {PluginClient} from '../plugin/Plugin';
import {DevicePluginClient} from '../plugin/DevicePlugin'; import {DevicePluginClient} from '../plugin/DevicePlugin';
test('it can start a plugin and lifecycle events', () => { test('it can start a plugin and lifecycle events', () => {
@@ -246,7 +246,7 @@ test('plugins cannot use a persist key twice', async () => {
test('plugins can receive deeplinks', async () => { test('plugins can receive deeplinks', async () => {
const plugin = TestUtils.startPlugin({ const plugin = TestUtils.startPlugin({
plugin(client: FlipperClient) { plugin(client: PluginClient) {
client.onDeepLink((deepLink) => { client.onDeepLink((deepLink) => {
if (typeof deepLink === 'string') { if (typeof deepLink === 'string') {
field1.set(deepLink); field1.set(deepLink);

View File

@@ -9,7 +9,10 @@
import * as TestUtilites from './test-utils/test-utils'; import * as TestUtilites from './test-utils/test-utils';
export {SandyPluginInstance, FlipperClient} from './plugin/Plugin'; export {
SandyPluginInstance,
PluginClient as FlipperClient,
} from './plugin/Plugin';
export { export {
Device, Device,
DeviceLogEntry, DeviceLogEntry,

View File

@@ -8,9 +8,7 @@
*/ */
import {SandyPluginDefinition} from './SandyPluginDefinition'; import {SandyPluginDefinition} from './SandyPluginDefinition';
import {EventEmitter} from 'events'; import {BasePluginInstance, BasePluginClient} from './PluginBase';
import {Atom} from '../state/atom';
import {setCurrentPluginInstance} from './Plugin';
export type DeviceLogListener = (entry: DeviceLogEntry) => void; export type DeviceLogListener = (entry: DeviceLogEntry) => void;
@@ -42,31 +40,13 @@ export type DevicePluginPredicate = (device: Device) => boolean;
export type DevicePluginFactory = (client: DevicePluginClient) => object; export type DevicePluginFactory = (client: DevicePluginClient) => object;
// TODO: better name? export interface DevicePluginClient extends BasePluginClient {
export interface DevicePluginClient {
readonly device: Device; readonly device: Device;
/**
* the onDestroy event is fired whenever a device is unloaded from Flipper, or a plugin is disabled.
*/
onDestroy(cb: () => void): void;
/**
* the onActivate event is fired whenever the plugin is actived in the UI
*/
onActivate(cb: () => void): void;
/**
* The counterpart of the `onActivate` handler.
*/
onDeactivate(cb: () => void): void;
/**
* Triggered when this plugin is opened through a deeplink
*/
onDeepLink(cb: (deepLink: unknown) => void): void;
} }
/**
* Wrapper interface around BaseDevice in Flipper
*/
export interface RealFlipperDevice { export interface RealFlipperDevice {
isArchived: boolean; isArchived: boolean;
addLogListener(callback: DeviceLogListener): Symbol; addLogListener(callback: DeviceLogListener): Symbol;
@@ -74,35 +54,20 @@ export interface RealFlipperDevice {
addLogEntry(entry: DeviceLogEntry): void; addLogEntry(entry: DeviceLogEntry): void;
} }
export class SandyDevicePluginInstance { export class SandyDevicePluginInstance extends BasePluginInstance {
static is(thing: any): thing is SandyDevicePluginInstance { static is(thing: any): thing is SandyDevicePluginInstance {
return thing instanceof SandyDevicePluginInstance; return thing instanceof SandyDevicePluginInstance;
} }
/** client that is bound to this instance */ /** client that is bound to this instance */
client: DevicePluginClient; client: DevicePluginClient;
/** the original plugin definition */
definition: SandyPluginDefinition;
/** the plugin instance api as used inside components and such */
instanceApi: any;
activated = false;
destroyed = false;
events = new EventEmitter();
// temporarily field that is used during deserialization
initialStates?: Record<string, any>;
// all the atoms that should be serialized when making an export / import
rootStates: Record<string, Atom<any>> = {};
// last seen deeplink
lastDeeplink?: any;
constructor( constructor(
realDevice: RealFlipperDevice, realDevice: RealFlipperDevice,
definition: SandyPluginDefinition, definition: SandyPluginDefinition,
initialStates?: Record<string, any>, initialStates?: Record<string, any>,
) { ) {
this.definition = definition; super(definition, initialStates);
const device: Device = { const device: Device = {
get isArchived() { get isArchived() {
return realDevice.isArchived; return realDevice.isArchived;
@@ -115,84 +80,15 @@ export class SandyDevicePluginInstance {
}, },
}; };
this.client = { this.client = {
...this.createBasePluginClient(),
device, device,
onDestroy: (cb) => {
this.events.on('destroy', cb);
},
onActivate: (cb) => {
this.events.on('activate', cb);
},
onDeactivate: (cb) => {
this.events.on('deactivate', cb);
},
onDeepLink: (callback) => {
this.events.on('deeplink', callback);
},
}; };
setCurrentPluginInstance(this); this.initializePlugin(() =>
this.initialStates = initialStates; definition.asDevicePluginModule().devicePlugin(this.client),
try { );
this.instanceApi = definition
.asDevicePluginModule()
.devicePlugin(this.client);
} finally {
this.initialStates = undefined;
setCurrentPluginInstance(undefined);
}
}
// the plugin is selected in the UI
activate() {
this.assertNotDestroyed();
if (!this.activated) {
this.activated = true;
this.events.emit('activate');
}
}
deactivate() {
if (this.destroyed) {
return;
}
if (this.activated) {
this.lastDeeplink = undefined;
this.activated = false;
this.events.emit('deactivate');
}
}
destroy() {
this.assertNotDestroyed();
this.deactivate();
this.events.emit('destroy');
this.destroyed = true;
} }
toJSON() { toJSON() {
return '[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()]),
);
}
isPersistable(): boolean {
return Object.keys(this.rootStates).length > 0;
}
private assertNotDestroyed() {
if (this.destroyed) {
throw new Error('Plugin has been destroyed already');
}
}
} }

View File

@@ -8,9 +8,7 @@
*/ */
import {SandyPluginDefinition} from './SandyPluginDefinition'; import {SandyPluginDefinition} from './SandyPluginDefinition';
import {EventEmitter} from 'events'; import {BasePluginInstance, BasePluginClient} from './PluginBase';
import {Atom} from '../state/atom';
import {SandyDevicePluginInstance} from './DevicePlugin';
type EventsContract = Record<string, any>; type EventsContract = Record<string, any>;
type MethodsContract = Record<string, (params: any) => Promise<any>>; type MethodsContract = Record<string, (params: any) => Promise<any>>;
@@ -23,25 +21,10 @@ type Message = {
/** /**
* API available to a plugin factory * API available to a plugin factory
*/ */
export interface FlipperClient< export interface PluginClient<
Events extends EventsContract = {}, Events extends EventsContract = {},
Methods extends MethodsContract = {} Methods extends MethodsContract = {}
> { > extends BasePluginClient {
/**
* the onDestroy event is fired whenever a client is unloaded from Flipper, or a plugin is disabled.
*/
onDestroy(cb: () => void): void;
/**
* the onActivate event is fired whenever the plugin is actived in the UI
*/
onActivate(cb: () => void): void;
/**
* The counterpart of the `onActivate` handler.
*/
onDeactivate(cb: () => void): void;
/** /**
* the onConnect event is fired whenever the plugin is connected to it's counter part on the device. * the onConnect event is fired whenever the plugin is connected to it's counter part on the device.
* For most plugins this event is fired if the user selects the plugin, * For most plugins this event is fired if the user selects the plugin,
@@ -57,11 +40,6 @@ export interface FlipperClient<
*/ */
onDisconnect(cb: () => void): void; onDisconnect(cb: () => void): void;
/**
* Triggered when this plugin is opened through a deeplink
*/
onDeepLink(cb: (deepLink: unknown) => void): void;
/** /**
* Send a message to the connected client * Send a message to the connected client
*/ */
@@ -101,26 +79,11 @@ export interface RealFlipperClient {
export type PluginFactory< export type PluginFactory<
Events extends EventsContract, Events extends EventsContract,
Methods extends MethodsContract Methods extends MethodsContract
> = (client: FlipperClient<Events, Methods>) => object; > = (client: PluginClient<Events, Methods>) => object;
export type FlipperPluginComponent = React.FC<{}>; export type FlipperPluginComponent = React.FC<{}>;
let currentPluginInstance: export class SandyPluginInstance extends BasePluginInstance {
| SandyPluginInstance
| SandyDevicePluginInstance
| undefined = undefined;
export function setCurrentPluginInstance(
instance: typeof currentPluginInstance,
) {
currentPluginInstance = instance;
}
export function getCurrentPluginInstance(): typeof currentPluginInstance {
return currentPluginInstance;
}
export class SandyPluginInstance {
static is(thing: any): thing is SandyPluginInstance { static is(thing: any): thing is SandyPluginInstance {
return thing instanceof SandyPluginInstance; return thing instanceof SandyPluginInstance;
} }
@@ -128,41 +91,20 @@ export class SandyPluginInstance {
/** base client provided by Flipper */ /** base client provided by Flipper */
realClient: RealFlipperClient; realClient: RealFlipperClient;
/** client that is bound to this instance */ /** client that is bound to this instance */
client: FlipperClient<any, any>; client: PluginClient<any, any>;
/** the original plugin definition */ /** connection alive? */
definition: SandyPluginDefinition;
/** the plugin instance api as used inside components and such */
instanceApi: any;
activated = false;
connected = false; connected = false;
destroyed = false;
events = new EventEmitter();
// temporarily field that is used during deserialization
initialStates?: Record<string, any>;
// all the atoms that should be serialized when making an export / import
rootStates: Record<string, Atom<any>> = {};
// last seen deeplink
lastDeeplink?: any;
constructor( constructor(
realClient: RealFlipperClient, realClient: RealFlipperClient,
definition: SandyPluginDefinition, definition: SandyPluginDefinition,
initialStates?: Record<string, any>, initialStates?: Record<string, any>,
) { ) {
super(definition, initialStates);
this.realClient = realClient; this.realClient = realClient;
this.definition = definition; this.definition = definition;
this.client = { this.client = {
onDestroy: (cb) => { ...this.createBasePluginClient(),
this.events.on('destroy', cb);
},
onActivate: (cb) => {
this.events.on('activate', cb);
},
onDeactivate: (cb) => {
this.events.on('deactivate', cb);
},
onConnect: (cb) => { onConnect: (cb) => {
this.events.on('connect', cb); this.events.on('connect', cb);
}, },
@@ -181,45 +123,24 @@ export class SandyPluginInstance {
onMessage: (event, callback) => { onMessage: (event, callback) => {
this.events.on('event-' + event, callback); this.events.on('event-' + event, callback);
}, },
onDeepLink: (callback) => {
this.events.on('deeplink', callback);
},
}; };
setCurrentPluginInstance(this); this.initializePlugin(() =>
this.initialStates = initialStates; definition.asPluginModule().plugin(this.client),
try { );
this.instanceApi = definition.asPluginModule().plugin(this.client);
} finally {
this.initialStates = undefined;
setCurrentPluginInstance(undefined);
}
} }
// the plugin is selected in the UI // the plugin is selected in the UI
activate() { activate() {
this.assertNotDestroyed(); super.activate();
if (!this.activated) { const pluginId = this.definition.id;
this.activated = true; if (!this.connected && !this.realClient.isBackgroundPlugin(pluginId)) {
this.events.emit('activate'); this.realClient.initPlugin(pluginId); // will call connect() if needed
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 // the plugin is deselected in the UI
deactivate() { deactivate() {
if (this.destroyed) { super.deactivate();
// 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;
}
if (this.activated) {
this.lastDeeplink = undefined;
this.activated = false;
this.events.emit('deactivate');
}
const pluginId = this.definition.id; const pluginId = this.definition.id;
if (this.connected && !this.realClient.isBackgroundPlugin(pluginId)) { if (this.connected && !this.realClient.isBackgroundPlugin(pluginId)) {
this.realClient.deinitPlugin(pluginId); this.realClient.deinitPlugin(pluginId);
@@ -243,13 +164,10 @@ export class SandyPluginInstance {
} }
destroy() { destroy() {
this.assertNotDestroyed();
this.deactivate();
if (this.connected) { if (this.connected) {
this.realClient.deinitPlugin(this.definition.id); this.realClient.deinitPlugin(this.definition.id);
} }
this.events.emit('destroy'); super.destroy();
this.destroyed = true;
} }
receiveMessages(messages: Message[]) { receiveMessages(messages: Message[]) {
@@ -262,30 +180,6 @@ export class SandyPluginInstance {
return '[SandyPluginInstance]'; return '[SandyPluginInstance]';
} }
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()]),
);
}
isPersistable(): boolean {
return Object.keys(this.rootStates).length > 0;
}
private assertNotDestroyed() {
if (this.destroyed) {
throw new Error('Plugin has been destroyed already');
}
}
private assertConnected() { private assertConnected() {
this.assertNotDestroyed(); this.assertNotDestroyed();
if (!this.connected) { if (!this.connected) {

View File

@@ -0,0 +1,153 @@
/**
* 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 {SandyPluginDefinition} from './SandyPluginDefinition';
import {EventEmitter} from 'events';
import {Atom} from '../state/atom';
export interface BasePluginClient {
/**
* the onDestroy event is fired whenever a device is unloaded from Flipper, or a plugin is disabled.
*/
onDestroy(cb: () => void): void;
/**
* the onActivate event is fired whenever the plugin is actived in the UI
*/
onActivate(cb: () => void): void;
/**
* The counterpart of the `onActivate` handler.
*/
onDeactivate(cb: () => void): void;
/**
* Triggered when this plugin is opened through a deeplink
*/
onDeepLink(cb: (deepLink: unknown) => void): void;
}
let currentPluginInstance: BasePluginInstance | undefined = undefined;
export function setCurrentPluginInstance(
instance: typeof currentPluginInstance,
) {
currentPluginInstance = instance;
}
export function getCurrentPluginInstance(): typeof currentPluginInstance {
return currentPluginInstance;
}
export abstract class BasePluginInstance {
/** the original plugin definition */
definition: SandyPluginDefinition;
/** the plugin instance api as used inside components and such */
instanceApi: any;
activated = false;
destroyed = false;
events = new EventEmitter();
// temporarily field that is used during deserialization
initialStates?: Record<string, any>;
// all the atoms that should be serialized when making an export / import
rootStates: Record<string, Atom<any>> = {};
// last seen deeplink
lastDeeplink?: any;
constructor(
definition: SandyPluginDefinition,
initialStates?: Record<string, any>,
) {
this.definition = definition;
this.initialStates = initialStates;
}
protected initializePlugin(factory: () => any) {
// To be called from constructory
setCurrentPluginInstance(this);
try {
this.instanceApi = factory();
} finally {
this.initialStates = undefined;
setCurrentPluginInstance(undefined);
}
}
protected createBasePluginClient(): BasePluginClient {
return {
onActivate: (cb) => {
this.events.on('activate', cb);
},
onDeactivate: (cb) => {
this.events.on('deactivate', cb);
},
onDeepLink: (callback) => {
this.events.on('deeplink', callback);
},
onDestroy: (cb) => {
this.events.on('destroy', cb);
},
};
}
// the plugin is selected in the UI
activate() {
this.assertNotDestroyed();
if (!this.activated) {
this.activated = true;
this.events.emit('activate');
}
}
deactivate() {
if (this.destroyed) {
return;
}
if (this.activated) {
this.lastDeeplink = undefined;
this.activated = false;
this.events.emit('deactivate');
}
}
destroy() {
this.assertNotDestroyed();
this.deactivate();
this.events.emit('destroy');
this.destroyed = true;
}
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()]),
);
}
isPersistable(): boolean {
return Object.keys(this.rootStates).length > 0;
}
protected assertNotDestroyed() {
if (this.destroyed) {
throw new Error('Plugin has been destroyed already');
}
}
abstract toJSON(): string;
}

View File

@@ -9,7 +9,7 @@
import {produce} from 'immer'; import {produce} from 'immer';
import {useState, useEffect} from 'react'; import {useState, useEffect} from 'react';
import {getCurrentPluginInstance} from '../plugin/Plugin'; import {getCurrentPluginInstance} from '../plugin/PluginBase';
export type Atom<T> = { export type Atom<T> = {
get(): T; get(): T;

View File

@@ -19,7 +19,7 @@ import {PluginDetails} from 'flipper-plugin-lib';
import { import {
RealFlipperClient, RealFlipperClient,
SandyPluginInstance, SandyPluginInstance,
FlipperClient, PluginClient,
} from '../plugin/Plugin'; } from '../plugin/Plugin';
import { import {
SandyPluginDefinition, SandyPluginDefinition,
@@ -34,6 +34,7 @@ import {
RealFlipperDevice, RealFlipperDevice,
DeviceLogListener, DeviceLogListener,
} from '../plugin/DevicePlugin'; } from '../plugin/DevicePlugin';
import {BasePluginInstance} from '../plugin/PluginBase';
type Renderer = RenderResult<typeof queries>; type Renderer = RenderResult<typeof queries>;
@@ -49,17 +50,44 @@ type ExtractClientType<Module extends FlipperPluginModule<any>> = Parameters<
type ExtractMethodsType< type ExtractMethodsType<
Module extends FlipperPluginModule<any> Module extends FlipperPluginModule<any>
> = ExtractClientType<Module> extends FlipperClient<any, infer Methods> > = ExtractClientType<Module> extends PluginClient<any, infer Methods>
? Methods ? Methods
: never; : never;
type ExtractEventsType< type ExtractEventsType<
Module extends FlipperPluginModule<any> Module extends FlipperPluginModule<any>
> = ExtractClientType<Module> extends FlipperClient<infer Events, any> > = ExtractClientType<Module> extends PluginClient<infer Events, any>
? Events ? Events
: never; : never;
interface StartPluginResult<Module extends FlipperPluginModule<any>> { interface BasePluginResult {
/**
* Emulates the 'onActivate' event
*/
activate(): void;
/**
* Emulates the 'onActivate' event (when the user opens the plugin in the UI).
* Will also trigger the `onConnect` event for non-background plugins
*/
deactivate(): void;
/**
* Emulates the 'destroy' event. After calling destroy this plugin instance won't be usable anymore
*/
destroy(): void;
/**
* Emulate triggering a deeplink
*/
triggerDeepLink(deeplink: unknown): void;
/**
* Grab all the persistable state
*/
exportState(): any;
}
interface StartPluginResult<Module extends FlipperPluginModule<any>>
extends BasePluginResult {
/** /**
* the instantiated plugin for this test * the instantiated plugin for this test
*/ */
@@ -68,15 +96,6 @@ interface StartPluginResult<Module extends FlipperPluginModule<any>> {
* module, from which any other exposed methods can be accessed during testing * module, from which any other exposed methods can be accessed during testing
*/ */
module: Module; module: Module;
/**
* Emulates the 'onActivate' event (when the user opens the plugin in the UI).
* Will also trigger the `onConnect` event for non-background plugins
*/
activate(): void;
/**
* Emulatese the 'onDeactivate' event
*/
deactivate(): void;
/** /**
* Emulates the 'onConnect' event * Emulates the 'onConnect' event
*/ */
@@ -85,10 +104,6 @@ interface StartPluginResult<Module extends FlipperPluginModule<any>> {
* Emulatese the 'onDisconnect' event * Emulatese the 'onDisconnect' event
*/ */
disconnect(): void; disconnect(): void;
/**
* Emulates the 'destroy' event. After calling destroy this plugin instance won't be usable anymore
*/
destroy(): void;
/** /**
* Jest Stub that is called whenever client.send() is called by the plugin. * Jest Stub that is called whenever client.send() is called by the plugin.
* Use send.mockImplementation(function) to intercept the calls. * Use send.mockImplementation(function) to intercept the calls.
@@ -117,13 +132,10 @@ interface StartPluginResult<Module extends FlipperPluginModule<any>> {
params: any; // afaik we can't type this :-( params: any; // afaik we can't type this :-(
}[], }[],
): void; ): void;
triggerDeepLink(deeplink: unknown): void;
exportState(): any;
} }
interface StartDevicePluginResult<Module extends FlipperDevicePluginModule> { interface StartDevicePluginResult<Module extends FlipperDevicePluginModule>
extends BasePluginResult {
/** /**
* the instantiated plugin for this test * the instantiated plugin for this test
*/ */
@@ -132,30 +144,10 @@ interface StartDevicePluginResult<Module extends FlipperDevicePluginModule> {
* module, from which any other exposed methods can be accessed during testing * module, from which any other exposed methods can be accessed during testing
*/ */
module: Module; module: Module;
/**
* Emulates the 'onActivate' event
*/
activate(): void;
/**
* Emulates the 'onDeactivate' event
*/
deactivate(): void;
/**
* Emulates the 'destroy' event. After calling destroy this plugin instance won't be usable anymore
*/
destroy(): void;
/** /**
* Emulates sending a log message arriving from the device * Emulates sending a log message arriving from the device
*/ */
sendLogEntry(logEntry: DeviceLogEntry): void; sendLogEntry(logEntry: DeviceLogEntry): void;
/**
* Emulates triggering a deeplik
*/
triggerDeepLink(deeplink: unknown): void;
/**
* Grabs the current (exportable) state
*/
exportState(): any;
} }
export function startPlugin<Module extends FlipperPluginModule<any>>( export function startPlugin<Module extends FlipperPluginModule<any>>(
@@ -198,28 +190,13 @@ export function startPlugin<Module extends FlipperPluginModule<any>>(
definition, definition,
options?.initialState, options?.initialState,
); );
if (options?.isBackgroundPlugin) {
pluginInstance.connect(); // otherwise part of activate
}
// we start activated
pluginInstance.activate();
const res: StartPluginResult<Module> = { const res: StartPluginResult<Module> = {
module, ...createBasePluginResult(pluginInstance),
instance: pluginInstance.instanceApi, instance: pluginInstance.instanceApi,
activate() { module,
pluginInstance.activate();
pluginInstance.connect();
},
deactivate() {
pluginInstance.deactivate();
if (!fakeFlipper.isBackgroundPlugin) {
pluginInstance.disconnect();
}
},
connect: () => pluginInstance.connect(), connect: () => pluginInstance.connect(),
disconnect: () => pluginInstance.disconnect(), disconnect: () => pluginInstance.disconnect(),
destroy: () => pluginInstance.destroy(),
onSend: sendStub, onSend: sendStub,
sendEvent: (event, params) => { sendEvent: (event, params) => {
res.sendEvents([ res.sendEvents([
@@ -234,13 +211,13 @@ export function startPlugin<Module extends FlipperPluginModule<any>>(
pluginInstance.receiveMessages(messages as any); pluginInstance.receiveMessages(messages as any);
}); });
}, },
exportState: () => pluginInstance.exportState(),
triggerDeepLink: (deepLink: unknown) => {
pluginInstance.triggerDeepLink(deepLink);
},
}; };
// @ts-ignore (res as any)._backingInstance = pluginInstance;
res._backingInstance = pluginInstance; // we start activated
if (options?.isBackgroundPlugin) {
pluginInstance.connect(); // otherwise part of activate
}
pluginInstance.activate();
return res; return res;
} }
@@ -252,8 +229,7 @@ export function renderPlugin<Module extends FlipperPluginModule<any>>(
act: (cb: () => void) => void; act: (cb: () => void) => void;
} { } {
const res = startPlugin(module, options); const res = startPlugin(module, options);
// @ts-ignore hidden api const pluginInstance: SandyPluginInstance = (res as any)._backingInstance;
const pluginInstance: SandyPluginInstance = res._backingInstance;
const renderer = render(<SandyPluginRenderer plugin={pluginInstance} />); const renderer = render(<SandyPluginRenderer plugin={pluginInstance} />);
@@ -288,27 +264,20 @@ export function startDevicePlugin<Module extends FlipperDevicePluginModule>(
definition, definition,
options?.initialState, options?.initialState,
); );
// we start connected
pluginInstance.activate();
const res: StartDevicePluginResult<Module> = { const res: StartDevicePluginResult<Module> = {
...createBasePluginResult(pluginInstance),
module, module,
instance: pluginInstance.instanceApi, instance: pluginInstance.instanceApi,
activate: () => pluginInstance.activate(),
deactivate: () => pluginInstance.deactivate(),
destroy: () => pluginInstance.destroy(),
sendLogEntry: (entry) => { sendLogEntry: (entry) => {
act(() => { act(() => {
testDevice.addLogEntry(entry); testDevice.addLogEntry(entry);
}); });
}, },
exportState: () => pluginInstance.exportState(),
triggerDeepLink: (deepLink: unknown) => {
pluginInstance.triggerDeepLink(deepLink);
},
}; };
// @ts-ignore (res as any)._backingInstance = pluginInstance;
res._backingInstance = pluginInstance; // we start connected
pluginInstance.activate();
return res; return res;
} }
@@ -321,7 +290,8 @@ export function renderDevicePlugin<Module extends FlipperDevicePluginModule>(
} { } {
const res = startDevicePlugin(module, options); const res = startDevicePlugin(module, options);
// @ts-ignore hidden api // @ts-ignore hidden api
const pluginInstance: SandyDevicePluginInstance = res._backingInstance; const pluginInstance: SandyDevicePluginInstance = (res as any)
._backingInstance;
const renderer = render(<SandyPluginRenderer plugin={pluginInstance} />); const renderer = render(<SandyPluginRenderer plugin={pluginInstance} />);
@@ -336,6 +306,20 @@ export function renderDevicePlugin<Module extends FlipperDevicePluginModule>(
}; };
} }
function createBasePluginResult(
pluginInstance: BasePluginInstance,
): BasePluginResult {
return {
activate: () => pluginInstance.activate(),
deactivate: () => pluginInstance.deactivate(),
exportState: () => pluginInstance.exportState(),
triggerDeepLink: (deepLink: unknown) => {
pluginInstance.triggerDeepLink(deepLink);
},
destroy: () => pluginInstance.destroy(),
};
}
export function createMockPluginDetails( export function createMockPluginDetails(
details?: Partial<PluginDetails>, details?: Partial<PluginDetails>,
): PluginDetails { ): PluginDetails {