Command processing (2/n): testing
Summary: *Stack summary*: this stack refactors plugin management actions to perform them in a dispatcher rather than in the root reducer (store.tsx) as all of these actions has side effects. To do that, we store requested plugin management actions (install/update/uninstall, star/unstar) in a queue which is then handled by pluginManager dispatcher. This dispatcher then dispatches all required state updates. *Diff summary*: refactored Flipper mocking helpers to allow testing of plugin commands, and wrote some tests for pluginManager. Reviewed By: mweststrate Differential Revision: D26450344 fbshipit-source-id: 0e8414517cc1ad353781dffd7ffb4a5f9a815d38
This commit is contained in:
committed by
Facebook GitHub Bot
parent
8efdde08c4
commit
24aed8fd45
64
desktop/app/src/test-utils/DeviceTestPlugin.tsx
Normal file
64
desktop/app/src/test-utils/DeviceTestPlugin.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* 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 * as React from 'react';
|
||||
import {
|
||||
DevicePluginClient,
|
||||
Device,
|
||||
usePlugin,
|
||||
createState,
|
||||
useValue,
|
||||
} from 'flipper-plugin';
|
||||
|
||||
export function supportsDevice(_device: Device) {
|
||||
return true;
|
||||
}
|
||||
|
||||
export function devicePlugin(client: DevicePluginClient) {
|
||||
const logStub = jest.fn();
|
||||
const activateStub = jest.fn();
|
||||
const deactivateStub = jest.fn();
|
||||
const destroyStub = jest.fn();
|
||||
const state = createState(
|
||||
{
|
||||
count: 0,
|
||||
},
|
||||
{
|
||||
persist: 'counter',
|
||||
},
|
||||
);
|
||||
|
||||
client.device.onLogEntry((entry) => {
|
||||
state.update((d) => {
|
||||
d.count++;
|
||||
});
|
||||
logStub(entry);
|
||||
});
|
||||
client.onActivate(activateStub);
|
||||
client.onDeactivate(deactivateStub);
|
||||
client.onDestroy(destroyStub);
|
||||
|
||||
return {
|
||||
logStub,
|
||||
activateStub,
|
||||
deactivateStub,
|
||||
destroyStub,
|
||||
state,
|
||||
};
|
||||
}
|
||||
|
||||
export function Component() {
|
||||
const api = usePlugin(devicePlugin);
|
||||
const count = useValue(api.state).count;
|
||||
|
||||
// @ts-expect-error
|
||||
api.bla;
|
||||
|
||||
return <h1>Hi from test plugin {count}</h1>;
|
||||
}
|
||||
213
desktop/app/src/test-utils/MockFlipper.tsx
Normal file
213
desktop/app/src/test-utils/MockFlipper.tsx
Normal file
@@ -0,0 +1,213 @@
|
||||
/**
|
||||
* 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 {createStore} from 'redux';
|
||||
import BaseDevice from '../devices/BaseDevice';
|
||||
import {rootReducer} from '../store';
|
||||
import {Store} from '../reducers/index';
|
||||
import Client, {ClientQuery} from '../Client';
|
||||
import {buildClientId} from '../utils/clientUtils';
|
||||
import {Logger} from '../fb-interfaces/Logger';
|
||||
import {PluginDefinition} from '../plugin';
|
||||
import {registerPlugins} from '../reducers/plugins';
|
||||
import {getInstance} from '../fb-stubs/Logger';
|
||||
import {initializeFlipperLibImplementation} from '../utils/flipperLibImplementation';
|
||||
import pluginManager from '../dispatcher/pluginManager';
|
||||
|
||||
export interface AppOptions {
|
||||
plugins?: PluginDefinition[];
|
||||
}
|
||||
|
||||
export interface ClientOptions {
|
||||
name?: string;
|
||||
supportedPlugins?: string[];
|
||||
backgroundPlugins?: string[];
|
||||
onSend?: (pluginId: string, method: string, params?: object) => any;
|
||||
query?: ClientQuery;
|
||||
skipRegister?: boolean;
|
||||
}
|
||||
|
||||
export interface DeviceOptions {
|
||||
serial?: string;
|
||||
}
|
||||
|
||||
export default class MockFlipper {
|
||||
private unsubscribePluginManager!: () => Promise<void>;
|
||||
private _store!: Store;
|
||||
private _logger!: Logger;
|
||||
private _devices: BaseDevice[] = [];
|
||||
private _clients: Client[] = [];
|
||||
private _deviceCounter: number = 0;
|
||||
private _clientCounter: number = 0;
|
||||
|
||||
public get store(): Store {
|
||||
return this._store;
|
||||
}
|
||||
|
||||
public get logger(): Logger {
|
||||
return this._logger;
|
||||
}
|
||||
|
||||
public get devices(): ReadonlyArray<BaseDevice> {
|
||||
return this._devices;
|
||||
}
|
||||
|
||||
public get clients(): ReadonlyArray<Client> {
|
||||
return this._clients;
|
||||
}
|
||||
|
||||
public get dispatch() {
|
||||
return this._store.dispatch;
|
||||
}
|
||||
|
||||
public getState() {
|
||||
return this._store.getState();
|
||||
}
|
||||
|
||||
public async init({plugins}: AppOptions = {}) {
|
||||
this._store = createStore(rootReducer);
|
||||
this._logger = getInstance();
|
||||
this.unsubscribePluginManager = pluginManager(this._store, this._logger, {
|
||||
runSideEffectsSynchronously: true,
|
||||
});
|
||||
initializeFlipperLibImplementation(this._store, this._logger);
|
||||
this._store.dispatch(registerPlugins(plugins ?? []));
|
||||
}
|
||||
|
||||
public async initWithDeviceAndClient(
|
||||
{
|
||||
appOptions = {},
|
||||
deviceOptions = {},
|
||||
clientOptions = {},
|
||||
}: {
|
||||
appOptions?: AppOptions;
|
||||
deviceOptions?: DeviceOptions;
|
||||
clientOptions?: ClientOptions;
|
||||
} = {appOptions: {}, deviceOptions: {}, clientOptions: {}},
|
||||
): Promise<{flipper: MockFlipper; device: BaseDevice; client: Client}> {
|
||||
await this.init(appOptions);
|
||||
const device = this.createDevice(deviceOptions);
|
||||
const client = await this.createClient(device, clientOptions);
|
||||
return {
|
||||
flipper: this,
|
||||
device,
|
||||
client,
|
||||
};
|
||||
}
|
||||
|
||||
public async destroy() {
|
||||
this.unsubscribePluginManager && this.unsubscribePluginManager();
|
||||
}
|
||||
|
||||
public createDevice({serial}: DeviceOptions = {}): BaseDevice {
|
||||
const device = new BaseDevice(
|
||||
serial ?? `serial_${++this._deviceCounter}`,
|
||||
'physical',
|
||||
'MockAndroidDevice',
|
||||
'Android',
|
||||
);
|
||||
this._store.dispatch({
|
||||
type: 'REGISTER_DEVICE',
|
||||
payload: device,
|
||||
});
|
||||
device.loadDevicePlugins(this._store.getState().plugins.devicePlugins);
|
||||
this._devices.push(device);
|
||||
return device;
|
||||
}
|
||||
|
||||
public async createClient(
|
||||
device: BaseDevice,
|
||||
{
|
||||
name,
|
||||
supportedPlugins,
|
||||
backgroundPlugins,
|
||||
onSend,
|
||||
skipRegister,
|
||||
query,
|
||||
}: ClientOptions = {},
|
||||
): Promise<Client> {
|
||||
if (!this._devices.includes(device)) {
|
||||
throw new Error('The provided device does not exist');
|
||||
}
|
||||
query = query ?? {
|
||||
app: name ?? `serial_${++this._clientCounter}`,
|
||||
os: 'Android',
|
||||
device: device.title,
|
||||
device_id: device.serial,
|
||||
sdk_version: 4,
|
||||
};
|
||||
const id = buildClientId({
|
||||
app: query.app,
|
||||
os: query.os,
|
||||
device: query.device,
|
||||
device_id: query.device_id,
|
||||
});
|
||||
supportedPlugins =
|
||||
supportedPlugins ??
|
||||
[...this._store.getState().plugins.clientPlugins.values()].map(
|
||||
(p) => p.id,
|
||||
);
|
||||
const client = new Client(
|
||||
id,
|
||||
query,
|
||||
null, // create a stub connection to avoid this plugin to be archived?
|
||||
this._logger,
|
||||
this._store,
|
||||
supportedPlugins,
|
||||
device,
|
||||
);
|
||||
|
||||
// yikes
|
||||
client.device = {
|
||||
then() {
|
||||
return device;
|
||||
},
|
||||
} as any;
|
||||
client.rawCall = async (
|
||||
method: string,
|
||||
_fromPlugin: boolean,
|
||||
params: any,
|
||||
): Promise<any> => {
|
||||
const intercepted = onSend?.(method, params);
|
||||
if (intercepted !== undefined) {
|
||||
return intercepted;
|
||||
}
|
||||
switch (method) {
|
||||
case 'getPlugins':
|
||||
// assuming this plugin supports all plugins for now
|
||||
return {
|
||||
plugins: supportedPlugins,
|
||||
};
|
||||
case 'getBackgroundPlugins':
|
||||
return {
|
||||
plugins: backgroundPlugins ?? [],
|
||||
};
|
||||
default:
|
||||
throw new Error(
|
||||
`Test client doesn't support rawCall method '${method}'`,
|
||||
);
|
||||
}
|
||||
};
|
||||
client.rawSend = jest.fn();
|
||||
|
||||
await client.init();
|
||||
|
||||
// As convenience, by default we select the new client, star the plugin, and select it
|
||||
if (!skipRegister) {
|
||||
this._store.dispatch({
|
||||
type: 'NEW_CLIENT',
|
||||
payload: client,
|
||||
});
|
||||
}
|
||||
|
||||
this._clients.push(client);
|
||||
|
||||
return client;
|
||||
}
|
||||
}
|
||||
98
desktop/app/src/test-utils/TestPlugin.tsx
Normal file
98
desktop/app/src/test-utils/TestPlugin.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* 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 * as React from 'react';
|
||||
import {PluginClient, usePlugin, createState, useValue} from 'flipper-plugin';
|
||||
|
||||
type Events = {
|
||||
inc: {
|
||||
delta: number;
|
||||
};
|
||||
};
|
||||
|
||||
type Methods = {
|
||||
currentState(params: {since: number}): Promise<number>;
|
||||
};
|
||||
|
||||
export function plugin(client: PluginClient<Events, Methods>) {
|
||||
const connectStub = jest.fn();
|
||||
const disconnectStub = jest.fn();
|
||||
const activateStub = jest.fn();
|
||||
const deactivateStub = jest.fn();
|
||||
const destroyStub = jest.fn();
|
||||
const state = createState(
|
||||
{
|
||||
count: 0,
|
||||
},
|
||||
{
|
||||
persist: 'counter',
|
||||
},
|
||||
);
|
||||
const unhandledMessages = createState<any[]>([]);
|
||||
|
||||
client.onConnect(connectStub);
|
||||
client.onDisconnect(disconnectStub);
|
||||
client.onActivate(activateStub);
|
||||
client.onDeactivate(deactivateStub);
|
||||
client.onDestroy(destroyStub);
|
||||
client.onMessage('inc', ({delta}) => {
|
||||
state.update((draft) => {
|
||||
draft.count += delta;
|
||||
});
|
||||
});
|
||||
client.onUnhandledMessage((event, params) => {
|
||||
unhandledMessages.update((draft) => {
|
||||
draft.push({event, params});
|
||||
});
|
||||
});
|
||||
|
||||
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'});
|
||||
// @ts-expect-error
|
||||
client.onMessage('stuff', (_params) => {
|
||||
// noop
|
||||
});
|
||||
client.onMessage('inc', (params) => {
|
||||
// @ts-expect-error
|
||||
params.bla;
|
||||
});
|
||||
}
|
||||
|
||||
async function getCurrentState() {
|
||||
return client.send('currentState', {since: 0});
|
||||
}
|
||||
|
||||
expect(client.device).not.toBeNull();
|
||||
|
||||
return {
|
||||
activateStub,
|
||||
deactivateStub,
|
||||
connectStub,
|
||||
destroyStub,
|
||||
disconnectStub,
|
||||
getCurrentState,
|
||||
state,
|
||||
unhandledMessages,
|
||||
appId: client.appId,
|
||||
appName: client.appName,
|
||||
};
|
||||
}
|
||||
|
||||
export function Component() {
|
||||
const api = usePlugin(plugin);
|
||||
const count = useValue(api.state).count;
|
||||
|
||||
// @ts-expect-error
|
||||
api.bla;
|
||||
|
||||
return <h1>Hi from test plugin {count}</h1>;
|
||||
}
|
||||
@@ -8,7 +8,6 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {createStore} from 'redux';
|
||||
import {Provider} from 'react-redux';
|
||||
import {
|
||||
render,
|
||||
@@ -25,18 +24,14 @@ import {
|
||||
} from '../reducers/connections';
|
||||
import BaseDevice from '../devices/BaseDevice';
|
||||
|
||||
import {rootReducer} from '../store';
|
||||
import {Store} from '../reducers/index';
|
||||
import Client, {ClientQuery} from '../Client';
|
||||
|
||||
import {buildClientId} from '../utils/clientUtils';
|
||||
import {Logger} from '../fb-interfaces/Logger';
|
||||
import {PluginDefinition} from '../plugin';
|
||||
import {registerPlugins} from '../reducers/plugins';
|
||||
import PluginContainer from '../PluginContainer';
|
||||
import {getPluginKey, isDevicePluginDefinition} from '../utils/pluginUtils';
|
||||
import {getInstance} from '../fb-stubs/Logger';
|
||||
import {initializeFlipperLibImplementation} from '../utils/flipperLibImplementation';
|
||||
import MockFlipper from './MockFlipper';
|
||||
|
||||
export type MockFlipperResult = {
|
||||
client: Client;
|
||||
@@ -64,94 +59,35 @@ type MockOptions = Partial<{
|
||||
additionalPlugins?: PluginDefinition[];
|
||||
dontEnableAdditionalPlugins?: true;
|
||||
asBackgroundPlugin?: true;
|
||||
supportedPlugins?: string[];
|
||||
}>;
|
||||
|
||||
export async function createMockFlipperWithPlugin(
|
||||
pluginClazz: PluginDefinition,
|
||||
options?: MockOptions,
|
||||
): Promise<MockFlipperResult> {
|
||||
const store = createStore(rootReducer);
|
||||
const logger = getInstance();
|
||||
initializeFlipperLibImplementation(store, logger);
|
||||
store.dispatch(
|
||||
registerPlugins([pluginClazz, ...(options?.additionalPlugins ?? [])]),
|
||||
);
|
||||
const mockFlipper = new MockFlipper();
|
||||
await mockFlipper.init({
|
||||
plugins: [pluginClazz, ...(options?.additionalPlugins ?? [])],
|
||||
});
|
||||
const logger = mockFlipper.logger;
|
||||
const store = mockFlipper.store;
|
||||
|
||||
function createDevice(serial: string): BaseDevice {
|
||||
const device = new BaseDevice(
|
||||
serial,
|
||||
'physical',
|
||||
'MockAndroidDevice',
|
||||
'Android',
|
||||
);
|
||||
store.dispatch({
|
||||
type: 'REGISTER_DEVICE',
|
||||
payload: device,
|
||||
});
|
||||
device.loadDevicePlugins(store.getState().plugins.devicePlugins);
|
||||
return device;
|
||||
}
|
||||
|
||||
async function createClient(
|
||||
const createDevice = (serial: string) => mockFlipper.createDevice({serial});
|
||||
const createClient = async (
|
||||
device: BaseDevice,
|
||||
name: string,
|
||||
query?: ClientQuery,
|
||||
skipRegister?: boolean,
|
||||
): Promise<Client> {
|
||||
query = query ?? {
|
||||
app: name,
|
||||
os: 'Android',
|
||||
device: device.title,
|
||||
device_id: device.serial,
|
||||
sdk_version: 4,
|
||||
};
|
||||
const id = buildClientId({
|
||||
app: query.app,
|
||||
os: query.os,
|
||||
device: query.device,
|
||||
device_id: query.device_id,
|
||||
});
|
||||
|
||||
const client = new Client(
|
||||
id,
|
||||
) => {
|
||||
const client = await mockFlipper.createClient(device, {
|
||||
name,
|
||||
query,
|
||||
null, // create a stub connection to avoid this plugin to be archived?
|
||||
logger,
|
||||
store,
|
||||
[
|
||||
...(isDevicePluginDefinition(pluginClazz) ? [] : [pluginClazz.id]),
|
||||
...(options?.dontEnableAdditionalPlugins
|
||||
? []
|
||||
: options?.additionalPlugins?.map((p) => p.id) ?? []),
|
||||
],
|
||||
device,
|
||||
);
|
||||
|
||||
client.rawCall = async (
|
||||
method: string,
|
||||
_fromPlugin: boolean,
|
||||
params: any,
|
||||
): Promise<any> => {
|
||||
const intercepted = options?.onSend?.(method, params);
|
||||
if (intercepted !== undefined) {
|
||||
return intercepted;
|
||||
}
|
||||
switch (method) {
|
||||
case 'getPlugins':
|
||||
// assuming this plugin supports all plugins for now
|
||||
return {
|
||||
plugins: [...store.getState().plugins.clientPlugins.keys()],
|
||||
};
|
||||
case 'getBackgroundPlugins':
|
||||
return {plugins: options?.asBackgroundPlugin ? [pluginClazz.id] : []};
|
||||
default:
|
||||
throw new Error(
|
||||
`Test client doesn't support rawCall method '${method}'`,
|
||||
);
|
||||
}
|
||||
};
|
||||
client.rawSend = jest.fn();
|
||||
|
||||
skipRegister,
|
||||
onSend: options?.onSend,
|
||||
supportedPlugins: options?.supportedPlugins,
|
||||
backgroundPlugins: options?.asBackgroundPlugin ? [pluginClazz.id] : [],
|
||||
});
|
||||
// enable the plugin
|
||||
if (
|
||||
!isDevicePluginDefinition(pluginClazz) &&
|
||||
@@ -180,17 +116,8 @@ export async function createMockFlipperWithPlugin(
|
||||
}
|
||||
});
|
||||
}
|
||||
await client.init();
|
||||
|
||||
// As convenience, by default we select the new client, star the plugin, and select it
|
||||
if (!skipRegister) {
|
||||
store.dispatch({
|
||||
type: 'NEW_CLIENT',
|
||||
payload: client,
|
||||
});
|
||||
}
|
||||
return client;
|
||||
}
|
||||
};
|
||||
|
||||
const device = createDevice('serial');
|
||||
const client = await createClient(device, 'TestApp');
|
||||
|
||||
Reference in New Issue
Block a user