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
108
desktop/app/src/dispatcher/__tests__/pluginManager.node.tsx
Normal file
108
desktop/app/src/dispatcher/__tests__/pluginManager.node.tsx
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
|
||||||
|
jest.mock('../plugins');
|
||||||
|
jest.mock('../../utils/electronModuleCache');
|
||||||
|
import {loadPlugin} from '../../reducers/pluginManager';
|
||||||
|
import {requirePlugin} from '../plugins';
|
||||||
|
import {mocked} from 'ts-jest/utils';
|
||||||
|
import {TestUtils} from 'flipper-plugin';
|
||||||
|
import * as TestPlugin from '../../test-utils/TestPlugin';
|
||||||
|
import {_SandyPluginDefinition as SandyPluginDefinition} from 'flipper-plugin';
|
||||||
|
import MockFlipper from '../../test-utils/MockFlipper';
|
||||||
|
|
||||||
|
const pluginDetails1 = TestUtils.createMockPluginDetails({
|
||||||
|
id: 'plugin1',
|
||||||
|
version: '0.0.1',
|
||||||
|
});
|
||||||
|
const pluginDefinition1 = new SandyPluginDefinition(pluginDetails1, TestPlugin);
|
||||||
|
|
||||||
|
const pluginDetails1V2 = TestUtils.createMockPluginDetails({
|
||||||
|
id: 'plugin1',
|
||||||
|
version: '0.0.2',
|
||||||
|
});
|
||||||
|
const pluginDefinition1V2 = new SandyPluginDefinition(
|
||||||
|
pluginDetails1V2,
|
||||||
|
TestPlugin,
|
||||||
|
);
|
||||||
|
|
||||||
|
const pluginDetails2 = TestUtils.createMockPluginDetails({id: 'plugin2'});
|
||||||
|
const pluginDefinition2 = new SandyPluginDefinition(pluginDetails2, TestPlugin);
|
||||||
|
|
||||||
|
const mockedRequirePlugin = mocked(requirePlugin);
|
||||||
|
|
||||||
|
let mockFlipper: MockFlipper;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
mockedRequirePlugin.mockImplementation(
|
||||||
|
(details) =>
|
||||||
|
(details === pluginDetails1
|
||||||
|
? pluginDefinition1
|
||||||
|
: details === pluginDetails2
|
||||||
|
? pluginDefinition2
|
||||||
|
: details === pluginDetails1V2
|
||||||
|
? pluginDefinition1V2
|
||||||
|
: undefined)!,
|
||||||
|
);
|
||||||
|
mockFlipper = new MockFlipper();
|
||||||
|
await mockFlipper.initWithDeviceAndClient({
|
||||||
|
clientOptions: {supportedPlugins: ['plugin1', 'plugin2']},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
mockedRequirePlugin.mockReset();
|
||||||
|
await mockFlipper.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('load plugin when no other version loaded', async () => {
|
||||||
|
mockFlipper.dispatch(
|
||||||
|
loadPlugin({plugin: pluginDetails1, enable: false, notifyIfFailed: false}),
|
||||||
|
);
|
||||||
|
expect(mockFlipper.getState().plugins.clientPlugins.get('plugin1')).toBe(
|
||||||
|
pluginDefinition1,
|
||||||
|
);
|
||||||
|
expect(mockFlipper.getState().plugins.loadedPlugins.get('plugin1')).toBe(
|
||||||
|
pluginDetails1,
|
||||||
|
);
|
||||||
|
expect(mockFlipper.clients[0].sandyPluginStates.has('plugin1')).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('load plugin when other version loaded', async () => {
|
||||||
|
mockFlipper.dispatch(
|
||||||
|
loadPlugin({plugin: pluginDetails1, enable: false, notifyIfFailed: false}),
|
||||||
|
);
|
||||||
|
mockFlipper.dispatch(
|
||||||
|
loadPlugin({
|
||||||
|
plugin: pluginDetails1V2,
|
||||||
|
enable: false,
|
||||||
|
notifyIfFailed: false,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(mockFlipper.getState().plugins.clientPlugins.get('plugin1')).toBe(
|
||||||
|
pluginDefinition1V2,
|
||||||
|
);
|
||||||
|
expect(mockFlipper.getState().plugins.loadedPlugins.get('plugin1')).toBe(
|
||||||
|
pluginDetails1V2,
|
||||||
|
);
|
||||||
|
expect(mockFlipper.clients[0].sandyPluginStates.has('plugin1')).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('load and enable Sandy plugin', async () => {
|
||||||
|
mockFlipper.dispatch(
|
||||||
|
loadPlugin({plugin: pluginDetails1, enable: true, notifyIfFailed: false}),
|
||||||
|
);
|
||||||
|
expect(mockFlipper.getState().plugins.clientPlugins.get('plugin1')).toBe(
|
||||||
|
pluginDefinition1,
|
||||||
|
);
|
||||||
|
expect(mockFlipper.getState().plugins.loadedPlugins.get('plugin1')).toBe(
|
||||||
|
pluginDetails1,
|
||||||
|
);
|
||||||
|
expect(mockFlipper.clients[0].sandyPluginStates.has('plugin1')).toBeTruthy();
|
||||||
|
});
|
||||||
@@ -35,15 +35,29 @@ function refreshInstalledPlugins(store: Store) {
|
|||||||
.then((plugins) => store.dispatch(registerInstalledPlugins(plugins)));
|
.then((plugins) => store.dispatch(registerInstalledPlugins(plugins)));
|
||||||
}
|
}
|
||||||
|
|
||||||
export default (store: Store, _logger: Logger) => {
|
export default (
|
||||||
|
store: Store,
|
||||||
|
_logger: Logger,
|
||||||
|
{runSideEffectsSynchronously}: {runSideEffectsSynchronously: boolean} = {
|
||||||
|
runSideEffectsSynchronously: false,
|
||||||
|
},
|
||||||
|
) => {
|
||||||
// This needn't happen immediately and is (light) I/O work.
|
// This needn't happen immediately and is (light) I/O work.
|
||||||
window.requestIdleCallback(() => {
|
if (window.requestIdleCallback) {
|
||||||
refreshInstalledPlugins(store);
|
window.requestIdleCallback(() => {
|
||||||
});
|
refreshInstalledPlugins(store);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
sideEffect(
|
const unsubscribeHandlePluginCommands = sideEffect(
|
||||||
store,
|
store,
|
||||||
{name: 'handlePluginActivation', throttleMs: 1000, fireImmediately: true},
|
{
|
||||||
|
name: 'handlePluginCommands',
|
||||||
|
throttleMs: 0,
|
||||||
|
fireImmediately: true,
|
||||||
|
runSynchronously: runSideEffectsSynchronously, // Used to simplify writing tests, if "true" passed, the all side effects will be called synchronously and immediately after changes
|
||||||
|
noTimeBudgetWarns: true, // These side effects are critical, so we're doing them with zero throttling and want to avoid unnecessary warns
|
||||||
|
},
|
||||||
(state) => state.pluginManager.pluginCommandsQueue,
|
(state) => state.pluginManager.pluginCommandsQueue,
|
||||||
(queue, store) => {
|
(queue, store) => {
|
||||||
for (const command of queue) {
|
for (const command of queue) {
|
||||||
@@ -59,6 +73,9 @@ export default (store: Store, _logger: Logger) => {
|
|||||||
store.dispatch(pluginCommandsProcessed(queue.length));
|
store.dispatch(pluginCommandsProcessed(queue.length));
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
return async () => {
|
||||||
|
unsubscribeHandlePluginCommands();
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
function loadPlugin(store: Store, payload: LoadPluginActionPayload) {
|
function loadPlugin(store: Store, payload: LoadPluginActionPayload) {
|
||||||
|
|||||||
@@ -250,7 +250,9 @@ test('it can send messages from sandy clients', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('it should initialize "Navigation" plugin if not enabled', async () => {
|
test('it should initialize "Navigation" plugin if not enabled', async () => {
|
||||||
const {client, store} = await createMockFlipperWithPlugin(TestPlugin);
|
const {client, store} = await createMockFlipperWithPlugin(TestPlugin, {
|
||||||
|
supportedPlugins: ['Navigation'],
|
||||||
|
});
|
||||||
|
|
||||||
const Plugin2 = new _SandyPluginDefinition(
|
const Plugin2 = new _SandyPluginDefinition(
|
||||||
TestUtils.createMockPluginDetails({
|
TestUtils.createMockPluginDetails({
|
||||||
|
|||||||
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 React from 'react';
|
||||||
import {createStore} from 'redux';
|
|
||||||
import {Provider} from 'react-redux';
|
import {Provider} from 'react-redux';
|
||||||
import {
|
import {
|
||||||
render,
|
render,
|
||||||
@@ -25,18 +24,14 @@ import {
|
|||||||
} from '../reducers/connections';
|
} from '../reducers/connections';
|
||||||
import BaseDevice from '../devices/BaseDevice';
|
import BaseDevice from '../devices/BaseDevice';
|
||||||
|
|
||||||
import {rootReducer} from '../store';
|
|
||||||
import {Store} from '../reducers/index';
|
import {Store} from '../reducers/index';
|
||||||
import Client, {ClientQuery} from '../Client';
|
import Client, {ClientQuery} from '../Client';
|
||||||
|
|
||||||
import {buildClientId} from '../utils/clientUtils';
|
|
||||||
import {Logger} from '../fb-interfaces/Logger';
|
import {Logger} from '../fb-interfaces/Logger';
|
||||||
import {PluginDefinition} from '../plugin';
|
import {PluginDefinition} from '../plugin';
|
||||||
import {registerPlugins} from '../reducers/plugins';
|
|
||||||
import PluginContainer from '../PluginContainer';
|
import PluginContainer from '../PluginContainer';
|
||||||
import {getPluginKey, isDevicePluginDefinition} from '../utils/pluginUtils';
|
import {getPluginKey, isDevicePluginDefinition} from '../utils/pluginUtils';
|
||||||
import {getInstance} from '../fb-stubs/Logger';
|
import MockFlipper from './MockFlipper';
|
||||||
import {initializeFlipperLibImplementation} from '../utils/flipperLibImplementation';
|
|
||||||
|
|
||||||
export type MockFlipperResult = {
|
export type MockFlipperResult = {
|
||||||
client: Client;
|
client: Client;
|
||||||
@@ -64,94 +59,35 @@ type MockOptions = Partial<{
|
|||||||
additionalPlugins?: PluginDefinition[];
|
additionalPlugins?: PluginDefinition[];
|
||||||
dontEnableAdditionalPlugins?: true;
|
dontEnableAdditionalPlugins?: true;
|
||||||
asBackgroundPlugin?: true;
|
asBackgroundPlugin?: true;
|
||||||
|
supportedPlugins?: string[];
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export async function createMockFlipperWithPlugin(
|
export async function createMockFlipperWithPlugin(
|
||||||
pluginClazz: PluginDefinition,
|
pluginClazz: PluginDefinition,
|
||||||
options?: MockOptions,
|
options?: MockOptions,
|
||||||
): Promise<MockFlipperResult> {
|
): Promise<MockFlipperResult> {
|
||||||
const store = createStore(rootReducer);
|
const mockFlipper = new MockFlipper();
|
||||||
const logger = getInstance();
|
await mockFlipper.init({
|
||||||
initializeFlipperLibImplementation(store, logger);
|
plugins: [pluginClazz, ...(options?.additionalPlugins ?? [])],
|
||||||
store.dispatch(
|
});
|
||||||
registerPlugins([pluginClazz, ...(options?.additionalPlugins ?? [])]),
|
const logger = mockFlipper.logger;
|
||||||
);
|
const store = mockFlipper.store;
|
||||||
|
|
||||||
function createDevice(serial: string): BaseDevice {
|
const createDevice = (serial: string) => mockFlipper.createDevice({serial});
|
||||||
const device = new BaseDevice(
|
const createClient = async (
|
||||||
serial,
|
|
||||||
'physical',
|
|
||||||
'MockAndroidDevice',
|
|
||||||
'Android',
|
|
||||||
);
|
|
||||||
store.dispatch({
|
|
||||||
type: 'REGISTER_DEVICE',
|
|
||||||
payload: device,
|
|
||||||
});
|
|
||||||
device.loadDevicePlugins(store.getState().plugins.devicePlugins);
|
|
||||||
return device;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createClient(
|
|
||||||
device: BaseDevice,
|
device: BaseDevice,
|
||||||
name: string,
|
name: string,
|
||||||
query?: ClientQuery,
|
query?: ClientQuery,
|
||||||
skipRegister?: boolean,
|
skipRegister?: boolean,
|
||||||
): Promise<Client> {
|
) => {
|
||||||
query = query ?? {
|
const client = await mockFlipper.createClient(device, {
|
||||||
app: name,
|
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,
|
|
||||||
query,
|
query,
|
||||||
null, // create a stub connection to avoid this plugin to be archived?
|
skipRegister,
|
||||||
logger,
|
onSend: options?.onSend,
|
||||||
store,
|
supportedPlugins: options?.supportedPlugins,
|
||||||
[
|
backgroundPlugins: options?.asBackgroundPlugin ? [pluginClazz.id] : [],
|
||||||
...(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();
|
|
||||||
|
|
||||||
// enable the plugin
|
// enable the plugin
|
||||||
if (
|
if (
|
||||||
!isDevicePluginDefinition(pluginClazz) &&
|
!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;
|
return client;
|
||||||
}
|
};
|
||||||
|
|
||||||
const device = createDevice('serial');
|
const device = createDevice('serial');
|
||||||
const client = await createClient(device, 'TestApp');
|
const client = await createClient(device, 'TestApp');
|
||||||
|
|||||||
@@ -28,7 +28,13 @@ export function sideEffect<
|
|||||||
State = Store extends ReduxStore<infer S, any> ? S : never
|
State = Store extends ReduxStore<infer S, any> ? S : never
|
||||||
>(
|
>(
|
||||||
store: Store,
|
store: Store,
|
||||||
options: {name: string; throttleMs: number; fireImmediately?: boolean},
|
options: {
|
||||||
|
name: string;
|
||||||
|
throttleMs: number;
|
||||||
|
fireImmediately?: boolean;
|
||||||
|
noTimeBudgetWarns?: boolean;
|
||||||
|
runSynchronously?: boolean;
|
||||||
|
},
|
||||||
selector: (state: State) => V,
|
selector: (state: State) => V,
|
||||||
effect: (selectedState: V, store: Store) => void,
|
effect: (selectedState: V, store: Store) => void,
|
||||||
): () => void {
|
): () => void {
|
||||||
@@ -52,7 +58,11 @@ export function sideEffect<
|
|||||||
}
|
}
|
||||||
lastRun = performance.now();
|
lastRun = performance.now();
|
||||||
const duration = lastRun - start;
|
const duration = lastRun - start;
|
||||||
if (duration > 15 && duration > options.throttleMs / 10) {
|
if (
|
||||||
|
!options.noTimeBudgetWarns &&
|
||||||
|
duration > 15 &&
|
||||||
|
duration > options.throttleMs / 10
|
||||||
|
) {
|
||||||
console.warn(
|
console.warn(
|
||||||
`Side effect '${options.name}' took ${Math.round(
|
`Side effect '${options.name}' took ${Math.round(
|
||||||
duration,
|
duration,
|
||||||
@@ -75,13 +85,17 @@ export function sideEffect<
|
|||||||
return; // no new value, no need to schedule
|
return; // no new value, no need to schedule
|
||||||
}
|
}
|
||||||
scheduled = true;
|
scheduled = true;
|
||||||
timeout = setTimeout(
|
if (options.runSynchronously) {
|
||||||
run,
|
run();
|
||||||
// Run ASAP (but async) or, if we recently did run, delay until at least 'throttle' time has expired
|
} else {
|
||||||
lastRun === -1
|
timeout = setTimeout(
|
||||||
? 1
|
run,
|
||||||
: Math.max(1, lastRun + options.throttleMs - performance.now()),
|
// Run ASAP (but async) or, if we recently did run, delay until at least 'throttle' time has expired
|
||||||
);
|
lastRun === -1
|
||||||
|
? 1
|
||||||
|
: Math.max(1, lastRun + options.throttleMs - performance.now()),
|
||||||
|
);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (options.fireImmediately) {
|
if (options.fireImmediately) {
|
||||||
|
|||||||
Reference in New Issue
Block a user