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:
committed by
Facebook GitHub Bot
parent
8b2d8498e6
commit
ec85dd5b01
@@ -30,7 +30,7 @@ beforeEach(() => {
|
|||||||
initialized = false;
|
initialized = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
function plugin(client: FlipperClient) {
|
function plugin(client: FlipperClient<any, any>) {
|
||||||
const connectStub = jest.fn();
|
const connectStub = jest.fn();
|
||||||
const disconnectStub = jest.fn();
|
const disconnectStub = jest.fn();
|
||||||
const destroyStub = jest.fn();
|
const destroyStub = jest.fn();
|
||||||
@@ -45,6 +45,7 @@ function plugin(client: FlipperClient) {
|
|||||||
connectStub,
|
connectStub,
|
||||||
disconnectStub,
|
disconnectStub,
|
||||||
destroyStub,
|
destroyStub,
|
||||||
|
send: client.send,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const TestPlugin = new SandyPluginDefinition(pluginDetails, {
|
const TestPlugin = new SandyPluginDefinition(pluginDetails, {
|
||||||
@@ -207,4 +208,30 @@ test('it trigger hooks for background plugins', async () => {
|
|||||||
expect(pluginInstance.disconnectStub).toHaveBeenCalledTimes(1);
|
expect(pluginInstance.disconnectStub).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('it can send messages from sandy clients', async () => {
|
||||||
|
let testMethodCalledWith: any = undefined;
|
||||||
|
const {client} = await createMockFlipperWithPlugin(TestPlugin, {
|
||||||
|
onSend(method, params) {
|
||||||
|
if (method === 'execute') {
|
||||||
|
testMethodCalledWith = params;
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const pluginInstance: PluginApi = client.sandyPluginStates.get(TestPlugin.id)!
|
||||||
|
.instanceApi;
|
||||||
|
// without rendering, non-bg plugins won't connect automatically,
|
||||||
|
client.initPlugin(TestPlugin.id);
|
||||||
|
await pluginInstance.send('test', {test: 3});
|
||||||
|
expect(testMethodCalledWith).toMatchInlineSnapshot(`
|
||||||
|
Object {
|
||||||
|
"api": "TestPlugin",
|
||||||
|
"method": "test",
|
||||||
|
"params": Object {
|
||||||
|
"test": 3,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
// TODO: T68683449 state is persisted if a plugin connects and reconnects
|
// TODO: T68683449 state is persisted if a plugin connects and reconnects
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ type MockOptions = Partial<{
|
|||||||
* can be used to intercept outgoing calls. If it returns undefined
|
* can be used to intercept outgoing calls. If it returns undefined
|
||||||
* the base implementation will be used
|
* the base implementation will be used
|
||||||
*/
|
*/
|
||||||
onSend(method: string, params?: object): object | undefined;
|
onSend(pluginId: string, method: string, params?: object): any;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export async function createMockFlipperWithPlugin(
|
export async function createMockFlipperWithPlugin(
|
||||||
@@ -110,7 +110,11 @@ export async function createMockFlipperWithPlugin(
|
|||||||
return device;
|
return device;
|
||||||
},
|
},
|
||||||
} as any;
|
} as any;
|
||||||
client.rawCall = async (method, _fromPlugin, params): Promise<any> => {
|
client.rawCall = async (
|
||||||
|
method: string,
|
||||||
|
_fromPlugin: boolean,
|
||||||
|
params: any,
|
||||||
|
): Promise<any> => {
|
||||||
const intercepted = options?.onSend?.(method, params);
|
const intercepted = options?.onSend?.(method, params);
|
||||||
if (intercepted !== undefined) {
|
if (intercepted !== undefined) {
|
||||||
return intercepted;
|
return intercepted;
|
||||||
@@ -127,7 +131,9 @@ export async function createMockFlipperWithPlugin(
|
|||||||
case 'getBackgroundPlugins':
|
case 'getBackgroundPlugins':
|
||||||
return {plugins: []};
|
return {plugins: []};
|
||||||
default:
|
default:
|
||||||
throw new Error(`Test client doesn't support rawCall to ${method}`);
|
throw new Error(
|
||||||
|
`Test client doesn't support rawCall method '${method}'`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
"@testing-library/react": "^10.4.3"
|
"@testing-library/react": "^10.4.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/jest": "^26.0.3",
|
||||||
"typescript": "^3.9.5"
|
"typescript": "^3.9.5"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ type Events = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type Methods = {
|
type Methods = {
|
||||||
currentState(): Promise<number>;
|
currentState(params: {since: number}): Promise<number>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function plugin(client: FlipperClient<Events, Methods>) {
|
export function plugin(client: FlipperClient<Events, Methods>) {
|
||||||
@@ -32,10 +32,22 @@ export function plugin(client: FlipperClient<Events, Methods>) {
|
|||||||
client.onDisconnect(disconnectStub);
|
client.onDisconnect(disconnectStub);
|
||||||
client.onDestroy(destroyStub);
|
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 {
|
return {
|
||||||
connectStub,
|
connectStub,
|
||||||
destroyStub,
|
destroyStub,
|
||||||
disconnectStub,
|
disconnectStub,
|
||||||
|
getCurrentState,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,14 +8,15 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
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';
|
||||||
|
|
||||||
test('it can start a plugin and lifecycle events', () => {
|
test('it can start a plugin and lifecycle events', () => {
|
||||||
const {instance, ...p} = TestUtils.startPlugin(testPlugin);
|
const {instance, ...p} = TestUtils.startPlugin(testPlugin);
|
||||||
|
|
||||||
// TODO T69105011 @ts-expect-error
|
// @ts-expect-error
|
||||||
// p.bla;
|
p.bla;
|
||||||
|
// @ts-expect-error
|
||||||
|
instance.bla;
|
||||||
|
|
||||||
// startPlugin starts connected
|
// startPlugin starts connected
|
||||||
expect(instance.connectStub).toBeCalledTimes(1);
|
expect(instance.connectStub).toBeCalledTimes(1);
|
||||||
@@ -59,3 +60,36 @@ test('it can render a plugin', () => {
|
|||||||
`);
|
`);
|
||||||
// TODO: test sending updates T68683442
|
// 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();
|
||||||
|
});
|
||||||
|
|||||||
@@ -39,6 +39,14 @@ export interface FlipperClient<
|
|||||||
* - when the plugin is disabled
|
* - when the plugin is disabled
|
||||||
*/
|
*/
|
||||||
onDisconnect(cb: () => void): void;
|
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;
|
isBackgroundPlugin(pluginId: string): boolean;
|
||||||
initPlugin(pluginId: string): void;
|
initPlugin(pluginId: string): void;
|
||||||
deinitPlugin(pluginId: string): void;
|
deinitPlugin(pluginId: string): void;
|
||||||
|
call(
|
||||||
|
api: string,
|
||||||
|
method: string,
|
||||||
|
fromPlugin: boolean,
|
||||||
|
params?: Object,
|
||||||
|
): Promise<Object>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type FlipperPluginFactory<
|
export type FlipperPluginFactory<
|
||||||
@@ -88,6 +102,15 @@ export class SandyPluginInstance {
|
|||||||
onDisconnect: (cb) => {
|
onDisconnect: (cb) => {
|
||||||
this.events.on('disconnect', 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);
|
this.instanceApi = definition.module.plugin(this.client);
|
||||||
}
|
}
|
||||||
@@ -143,4 +166,11 @@ export class SandyPluginInstance {
|
|||||||
throw new Error('Plugin has been destroyed already');
|
throw new Error('Plugin has been destroyed already');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private assertConnected() {
|
||||||
|
this.assertNotDestroyed();
|
||||||
|
if (!this.connected) {
|
||||||
|
throw new Error('Plugin is not connected');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,11 @@ import {
|
|||||||
} from '@testing-library/react';
|
} from '@testing-library/react';
|
||||||
import {PluginDetails} from 'flipper-plugin-lib';
|
import {PluginDetails} from 'flipper-plugin-lib';
|
||||||
|
|
||||||
import {RealFlipperClient, SandyPluginInstance} from '../plugin/Plugin';
|
import {
|
||||||
|
RealFlipperClient,
|
||||||
|
SandyPluginInstance,
|
||||||
|
FlipperClient,
|
||||||
|
} from '../plugin/Plugin';
|
||||||
import {
|
import {
|
||||||
SandyPluginDefinition,
|
SandyPluginDefinition,
|
||||||
FlipperPluginModule,
|
FlipperPluginModule,
|
||||||
@@ -29,6 +33,15 @@ interface StartPluginOptions {
|
|||||||
// TODO: support initial state T68683449 (and type correctly)
|
// 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>> {
|
interface StartPluginResult<Module extends FlipperPluginModule<any>> {
|
||||||
/**
|
/**
|
||||||
* the instantiated plugin for this test
|
* 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
|
* Emulates the 'destroy' event. After calling destroy this plugin instance won't be usable anymore
|
||||||
*/
|
*/
|
||||||
destroy(): void;
|
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>>(
|
export function startPlugin<Module extends FlipperPluginModule<any>>(
|
||||||
@@ -61,6 +84,7 @@ export function startPlugin<Module extends FlipperPluginModule<any>>(
|
|||||||
module,
|
module,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const sendStub = jest.fn();
|
||||||
const fakeFlipper: RealFlipperClient = {
|
const fakeFlipper: RealFlipperClient = {
|
||||||
isBackgroundPlugin(_pluginId: string) {
|
isBackgroundPlugin(_pluginId: string) {
|
||||||
// we only reason about non-background plugins,
|
// we only reason about non-background plugins,
|
||||||
@@ -69,6 +93,14 @@ export function startPlugin<Module extends FlipperPluginModule<any>>(
|
|||||||
},
|
},
|
||||||
initPlugin(_pluginId: string) {},
|
initPlugin(_pluginId: string) {},
|
||||||
deinitPlugin(_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);
|
const pluginInstance = new SandyPluginInstance(fakeFlipper, definition);
|
||||||
@@ -81,6 +113,7 @@ export function startPlugin<Module extends FlipperPluginModule<any>>(
|
|||||||
connect: () => pluginInstance.connect(),
|
connect: () => pluginInstance.connect(),
|
||||||
disconnect: () => pluginInstance.disconnect(),
|
disconnect: () => pluginInstance.disconnect(),
|
||||||
destroy: () => pluginInstance.destroy(),
|
destroy: () => pluginInstance.destroy(),
|
||||||
|
onSend: sendStub,
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
_backingInstance: pluginInstance,
|
_backingInstance: pluginInstance,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,5 +6,5 @@
|
|||||||
},
|
},
|
||||||
"references": [{"path": "../plugin-lib"}],
|
"references": [{"path": "../plugin-lib"}],
|
||||||
"include": ["src"],
|
"include": ["src"],
|
||||||
"exclude": ["node_modules", "**/__tests__/*"]
|
"exclude": ["node_modules"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2032,6 +2032,14 @@
|
|||||||
jest-diff "^25.2.1"
|
jest-diff "^25.2.1"
|
||||||
pretty-format "^25.2.1"
|
pretty-format "^25.2.1"
|
||||||
|
|
||||||
|
"@types/jest@^26.0.3":
|
||||||
|
version "26.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/jest/-/jest-26.0.3.tgz#79534e0e94857171c0edc596db0ebe7cb7863251"
|
||||||
|
integrity sha512-v89ga1clpVL/Y1+YI0eIu1VMW+KU7Xl8PhylVtDKVWaSUHBHYPLXMQGBdrpHewaKoTvlXkksbYqPgz8b4cmRZg==
|
||||||
|
dependencies:
|
||||||
|
jest-diff "^25.2.1"
|
||||||
|
pretty-format "^25.2.1"
|
||||||
|
|
||||||
"@types/json-schema@^7.0.3":
|
"@types/json-schema@^7.0.3":
|
||||||
version "7.0.3"
|
version "7.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.3.tgz#bdfd69d61e464dcc81b25159c270d75a73c1a636"
|
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.3.tgz#bdfd69d61e464dcc81b25159c270d75a73c1a636"
|
||||||
|
|||||||
Reference in New Issue
Block a user