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)));
|
||||
}
|
||||
|
||||
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.
|
||||
if (window.requestIdleCallback) {
|
||||
window.requestIdleCallback(() => {
|
||||
refreshInstalledPlugins(store);
|
||||
});
|
||||
}
|
||||
|
||||
sideEffect(
|
||||
const unsubscribeHandlePluginCommands = sideEffect(
|
||||
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,
|
||||
(queue, store) => {
|
||||
for (const command of queue) {
|
||||
@@ -59,6 +73,9 @@ export default (store: Store, _logger: Logger) => {
|
||||
store.dispatch(pluginCommandsProcessed(queue.length));
|
||||
},
|
||||
);
|
||||
return async () => {
|
||||
unsubscribeHandlePluginCommands();
|
||||
};
|
||||
};
|
||||
|
||||
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 () => {
|
||||
const {client, store} = await createMockFlipperWithPlugin(TestPlugin);
|
||||
const {client, store} = await createMockFlipperWithPlugin(TestPlugin, {
|
||||
supportedPlugins: ['Navigation'],
|
||||
});
|
||||
|
||||
const Plugin2 = new _SandyPluginDefinition(
|
||||
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 {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 ?? [])]),
|
||||
);
|
||||
|
||||
function createDevice(serial: string): BaseDevice {
|
||||
const device = new BaseDevice(
|
||||
serial,
|
||||
'physical',
|
||||
'MockAndroidDevice',
|
||||
'Android',
|
||||
);
|
||||
store.dispatch({
|
||||
type: 'REGISTER_DEVICE',
|
||||
payload: device,
|
||||
const mockFlipper = new MockFlipper();
|
||||
await mockFlipper.init({
|
||||
plugins: [pluginClazz, ...(options?.additionalPlugins ?? [])],
|
||||
});
|
||||
device.loadDevicePlugins(store.getState().plugins.devicePlugins);
|
||||
return device;
|
||||
}
|
||||
const logger = mockFlipper.logger;
|
||||
const store = mockFlipper.store;
|
||||
|
||||
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');
|
||||
|
||||
@@ -28,7 +28,13 @@ export function sideEffect<
|
||||
State = Store extends ReduxStore<infer S, any> ? S : never
|
||||
>(
|
||||
store: Store,
|
||||
options: {name: string; throttleMs: number; fireImmediately?: boolean},
|
||||
options: {
|
||||
name: string;
|
||||
throttleMs: number;
|
||||
fireImmediately?: boolean;
|
||||
noTimeBudgetWarns?: boolean;
|
||||
runSynchronously?: boolean;
|
||||
},
|
||||
selector: (state: State) => V,
|
||||
effect: (selectedState: V, store: Store) => void,
|
||||
): () => void {
|
||||
@@ -52,7 +58,11 @@ export function sideEffect<
|
||||
}
|
||||
lastRun = performance.now();
|
||||
const duration = lastRun - start;
|
||||
if (duration > 15 && duration > options.throttleMs / 10) {
|
||||
if (
|
||||
!options.noTimeBudgetWarns &&
|
||||
duration > 15 &&
|
||||
duration > options.throttleMs / 10
|
||||
) {
|
||||
console.warn(
|
||||
`Side effect '${options.name}' took ${Math.round(
|
||||
duration,
|
||||
@@ -75,6 +85,9 @@ export function sideEffect<
|
||||
return; // no new value, no need to schedule
|
||||
}
|
||||
scheduled = true;
|
||||
if (options.runSynchronously) {
|
||||
run();
|
||||
} else {
|
||||
timeout = setTimeout(
|
||||
run,
|
||||
// Run ASAP (but async) or, if we recently did run, delay until at least 'throttle' time has expired
|
||||
@@ -82,6 +95,7 @@ export function sideEffect<
|
||||
? 1
|
||||
: Math.max(1, lastRun + options.throttleMs - performance.now()),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
if (options.fireImmediately) {
|
||||
|
||||
Reference in New Issue
Block a user