Allow plugins to send messages

Summary: Sandy plugins can now send messages to plugins. The methods and params are strongly typed in implementation and unit tests, based on the <Methods> generic of FlipperClient.

Reviewed By: nikoant

Differential Revision: D22256972

fbshipit-source-id: 549523a402949b3eb6bb4b4ca160dedb5c5e722d
This commit is contained in:
Michel Weststrate
2020-07-01 08:58:40 -07:00
committed by Facebook GitHub Bot
parent 8b2d8498e6
commit ec85dd5b01
9 changed files with 161 additions and 10 deletions

View File

@@ -17,7 +17,7 @@ type Events = {
};
type Methods = {
currentState(): Promise<number>;
currentState(params: {since: number}): Promise<number>;
};
export function plugin(client: FlipperClient<Events, Methods>) {
@@ -32,10 +32,22 @@ export function plugin(client: FlipperClient<Events, Methods>) {
client.onDisconnect(disconnectStub);
client.onDestroy(destroyStub);
function _unused_JustTypeChecks() {
// @ts-expect-error Argument of type '"bla"' is not assignable
client.send('bla', {});
// @ts-expect-error Argument of type '{ stuff: string; }' is not assignable to parameter of type
client.send('currentState', {stuff: 'nope'});
}
async function getCurrentState() {
return client.send('currentState', {since: 0});
}
return {
connectStub,
destroyStub,
disconnectStub,
getCurrentState,
};
}

View File

@@ -8,14 +8,15 @@
*/
import * as TestUtils from '../test-utils/test-utils';
import * as testPlugin from './TestPlugin';
test('it can start a plugin and lifecycle events', () => {
const {instance, ...p} = TestUtils.startPlugin(testPlugin);
// TODO T69105011 @ts-expect-error
// p.bla;
// @ts-expect-error
p.bla;
// @ts-expect-error
instance.bla;
// startPlugin starts connected
expect(instance.connectStub).toBeCalledTimes(1);
@@ -59,3 +60,36 @@ test('it can render a plugin', () => {
`);
// TODO: test sending updates T68683442
});
test('a plugin can send messages', async () => {
const {instance, onSend} = TestUtils.startPlugin(testPlugin);
// By default send is stubbed
expect(await instance.getCurrentState()).toBeUndefined();
expect(onSend).toHaveBeenCalledWith('currentState', {since: 0});
// @ts-expect-error
onSend('bla');
// ... But we can intercept!
onSend.mockImplementationOnce(async (method, params) => {
expect(method).toEqual('currentState');
expect(params).toEqual({since: 0});
return 3;
});
expect(await instance.getCurrentState()).toEqual(3);
});
test('a plugin cannot send messages after being disconnected', async () => {
const {instance, disconnect} = TestUtils.startPlugin(testPlugin);
disconnect();
let threw = false;
try {
await instance.getCurrentState();
} catch (e) {
threw = true; // for some weird reason expect(async () => instance.getCurrentState()).toThrow(...) doesn't work today...
expect(e).toMatchInlineSnapshot(`[Error: Plugin is not connected]`);
}
expect(threw).toBeTruthy();
});

View File

@@ -39,6 +39,14 @@ export interface FlipperClient<
* - when the plugin is disabled
*/
onDisconnect(cb: () => void): void;
/**
* Send a message to the connected client
*/
send<Method extends keyof Methods>(
method: Method,
params: Parameters<Methods[Method]>[0],
): ReturnType<Methods[Method]>;
}
/**
@@ -49,6 +57,12 @@ export interface RealFlipperClient {
isBackgroundPlugin(pluginId: string): boolean;
initPlugin(pluginId: string): void;
deinitPlugin(pluginId: string): void;
call(
api: string,
method: string,
fromPlugin: boolean,
params?: Object,
): Promise<Object>;
}
export type FlipperPluginFactory<
@@ -88,6 +102,15 @@ export class SandyPluginInstance {
onDisconnect: (cb) => {
this.events.on('disconnect', cb);
},
send: async (method, params) => {
this.assertConnected();
return await realClient.call(
this.definition.id,
method as any,
true,
params as any,
);
},
};
this.instanceApi = definition.module.plugin(this.client);
}
@@ -143,4 +166,11 @@ export class SandyPluginInstance {
throw new Error('Plugin has been destroyed already');
}
}
private assertConnected() {
this.assertNotDestroyed();
if (!this.connected) {
throw new Error('Plugin is not connected');
}
}
}

View File

@@ -15,7 +15,11 @@ import {
} from '@testing-library/react';
import {PluginDetails} from 'flipper-plugin-lib';
import {RealFlipperClient, SandyPluginInstance} from '../plugin/Plugin';
import {
RealFlipperClient,
SandyPluginInstance,
FlipperClient,
} from '../plugin/Plugin';
import {
SandyPluginDefinition,
FlipperPluginModule,
@@ -29,6 +33,15 @@ interface StartPluginOptions {
// TODO: support initial state T68683449 (and type correctly)
}
type ExtractClientType<Module extends FlipperPluginModule<any>> = Parameters<
Module['plugin']
>[0];
type ExtractMethodsType<
Module extends FlipperPluginModule<any>
> = ExtractClientType<Module> extends FlipperClient<any, infer Methods>
? Methods
: never;
interface StartPluginResult<Module extends FlipperPluginModule<any>> {
/**
* the instantiated plugin for this test
@@ -50,6 +63,16 @@ interface StartPluginResult<Module extends FlipperPluginModule<any>> {
* 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.
* Use send.mockImplementation(function) to intercept the calls.
*/
onSend: jest.MockedFunction<
<Method extends keyof ExtractMethodsType<Module>>(
method: Method,
params: Parameters<ExtractMethodsType<Module>[Method]>[0],
) => ReturnType<ExtractMethodsType<Module>[Method]>
>;
}
export function startPlugin<Module extends FlipperPluginModule<any>>(
@@ -61,6 +84,7 @@ export function startPlugin<Module extends FlipperPluginModule<any>>(
module,
);
const sendStub = jest.fn();
const fakeFlipper: RealFlipperClient = {
isBackgroundPlugin(_pluginId: string) {
// we only reason about non-background plugins,
@@ -69,6 +93,14 @@ export function startPlugin<Module extends FlipperPluginModule<any>>(
},
initPlugin(_pluginId: string) {},
deinitPlugin(_pluginId: string) {},
call(
api: string,
method: string,
fromPlugin: boolean,
params?: Object,
): Promise<Object> {
return sendStub(method, params);
},
};
const pluginInstance = new SandyPluginInstance(fakeFlipper, definition);
@@ -81,6 +113,7 @@ export function startPlugin<Module extends FlipperPluginModule<any>>(
connect: () => pluginInstance.connect(),
disconnect: () => pluginInstance.disconnect(),
destroy: () => pluginInstance.destroy(),
onSend: sendStub,
// @ts-ignore
_backingInstance: pluginInstance,
};