Fix bug causing archived devices trying to connect

Reviewed By: nikoant

Differential Revision: D28064540

fbshipit-source-id: 43f05c4348a33e9633751bb9f69cd8d17ddd13c4
This commit is contained in:
Michel Weststrate
2021-04-29 12:12:00 -07:00
committed by Facebook GitHub Bot
parent 8a3ba8615d
commit 699343a9ca
7 changed files with 325 additions and 18 deletions

View File

@@ -11,7 +11,6 @@ import {PluginDefinition, FlipperPlugin, FlipperDevicePlugin} from './plugin';
import BaseDevice, {OS} from './devices/BaseDevice'; import BaseDevice, {OS} from './devices/BaseDevice';
import {Logger} from './fb-interfaces/Logger'; import {Logger} from './fb-interfaces/Logger';
import {Store} from './reducers/index'; import {Store} from './reducers/index';
import {setPluginState} from './reducers/pluginStates';
import {Payload, ConnectionStatus} from 'rsocket-types'; import {Payload, ConnectionStatus} from 'rsocket-types';
import {Flowable, Single} from 'rsocket-flowable'; import {Flowable, Single} from 'rsocket-flowable';
import {performance} from 'perf_hooks'; import {performance} from 'perf_hooks';
@@ -150,7 +149,7 @@ export default class Client extends EventEmitter {
device: BaseDevice, device: BaseDevice,
) { ) {
super(); super();
this.connected.set(true); this.connected.set(!!conn);
this.plugins = plugins ? plugins : []; this.plugins = plugins ? plugins : [];
this.backgroundPlugins = []; this.backgroundPlugins = [];
this.connection = conn; this.connection = conn;
@@ -696,8 +695,10 @@ export default class Client extends EventEmitter {
initPlugin(pluginId: string) { initPlugin(pluginId: string) {
this.activePlugins.add(pluginId); this.activePlugins.add(pluginId);
this.rawSend('init', {plugin: pluginId}); if (this.connected.get()) {
this.sandyPluginStates.get(pluginId)?.connect(); this.rawSend('init', {plugin: pluginId});
this.sandyPluginStates.get(pluginId)?.connect();
}
} }
deinitPlugin(pluginId: string) { deinitPlugin(pluginId: string) {

View File

@@ -1015,3 +1015,262 @@ test('Sandy plugins support isPluginSupported + selectPlugin', async () => {
`); `);
expect(renders).toBe(2); expect(renders).toBe(2);
}); });
test('PluginContainer can render Sandy plugins for archived devices', async () => {
let renders = 0;
function MySandyPlugin() {
renders++;
const sandyApi = usePlugin(plugin);
const count = useValue(sandyApi.count);
expect(Object.keys(sandyApi)).toEqual([
'connectedStub',
'disconnectedStub',
'activatedStub',
'deactivatedStub',
'count',
]);
expect(() => {
// eslint-disable-next-line
usePlugin(function bla() {
return {};
});
}).toThrowError(/didn't match the type of the requested plugin/);
return <div>Hello from Sandy{count}</div>;
}
type Events = {
inc: {delta: number};
};
const plugin = (client: PluginClient<Events>) => {
expect(client.connected.get()).toBeFalsy();
expect(client.isConnected).toBeFalsy();
expect(client.device.isConnected).toBeFalsy();
expect(client.device.isArchived).toBeTruthy();
const count = createState(0);
const connectedStub = jest.fn();
const disconnectedStub = jest.fn();
const activatedStub = jest.fn();
const deactivatedStub = jest.fn();
client.onConnect(connectedStub);
client.onDisconnect(disconnectedStub);
client.onActivate(activatedStub);
client.onDeactivate(deactivatedStub);
client.onMessage('inc', ({delta}) => {
count.set(count.get() + delta);
});
return {
connectedStub,
disconnectedStub,
activatedStub,
deactivatedStub,
count,
};
};
const definition = new _SandyPluginDefinition(
TestUtils.createMockPluginDetails(),
{
plugin,
Component: MySandyPlugin,
},
);
const {
renderer,
act,
client,
store,
} = await renderMockFlipperWithPlugin(definition, {archivedDevice: true});
expect(client.rawSend).not.toBeCalled();
expect(renderer.baseElement).toMatchInlineSnapshot(`
<body>
<div>
<div
class="css-w6yhx2-View-FlexBox-FlexColumn"
>
<div>
Hello from Sandy
0
</div>
</div>
<div
class="css-724x97-View-FlexBox-FlexRow"
id="detailsSidebar"
/>
</div>
</body>
`);
expect(renders).toBe(1);
// make sure the plugin gets activated, but not connected!
const pluginInstance: ReturnType<
typeof plugin
> = client.sandyPluginStates.get(definition.id)!.instanceApi;
expect(pluginInstance.connectedStub).toBeCalledTimes(0);
expect(pluginInstance.disconnectedStub).toBeCalledTimes(0);
expect(pluginInstance.activatedStub).toBeCalledTimes(1);
expect(pluginInstance.deactivatedStub).toBeCalledTimes(0);
// select non existing plugin
act(() => {
store.dispatch(
selectPlugin({
selectedPlugin: 'Logs',
deepLinkPayload: null,
}),
);
});
expect(client.rawSend).not.toBeCalled();
expect(renderer.baseElement).toMatchInlineSnapshot(`
<body>
<div />
</body>
`);
expect(pluginInstance.connectedStub).toBeCalledTimes(0);
expect(pluginInstance.disconnectedStub).toBeCalledTimes(0);
expect(pluginInstance.activatedStub).toBeCalledTimes(1);
expect(pluginInstance.deactivatedStub).toBeCalledTimes(1);
// go back
act(() => {
store.dispatch(
selectPlugin({
selectedPlugin: definition.id,
deepLinkPayload: null,
}),
);
});
// Might be needed, but seems to work reliable without: await sleep(1000);
expect(renderer.baseElement).toMatchInlineSnapshot(`
<body>
<div>
<div
class="css-w6yhx2-View-FlexBox-FlexColumn"
>
<div>
Hello from Sandy
0
</div>
</div>
<div
class="css-724x97-View-FlexBox-FlexRow"
id="detailsSidebar"
/>
</div>
</body>
`);
expect(pluginInstance.count.get()).toBe(0);
expect(pluginInstance.connectedStub).toBeCalledTimes(0);
expect(pluginInstance.disconnectedStub).toBeCalledTimes(0);
expect(pluginInstance.activatedStub).toBeCalledTimes(2);
expect(pluginInstance.deactivatedStub).toBeCalledTimes(1);
expect(client.rawSend).not.toBeCalled();
});
test('PluginContainer triggers correct lifecycles for background plugin', async () => {
function MySandyPlugin() {
return <div>Hello from Sandy</div>;
}
const plugin = (client: PluginClient) => {
expect(client.connected.get()).toBeFalsy();
expect(client.isConnected).toBeFalsy();
expect(client.device.isConnected).toBeFalsy();
expect(client.device.isArchived).toBeTruthy();
const connectedStub = jest.fn();
const disconnectedStub = jest.fn();
const activatedStub = jest.fn();
const deactivatedStub = jest.fn();
client.onConnect(connectedStub);
client.onDisconnect(disconnectedStub);
client.onActivate(activatedStub);
client.onDeactivate(deactivatedStub);
return {connectedStub, disconnectedStub, activatedStub, deactivatedStub};
};
const definition = new _SandyPluginDefinition(
TestUtils.createMockPluginDetails(),
{
plugin,
Component: MySandyPlugin,
},
);
const {act, client, store} = await renderMockFlipperWithPlugin(definition, {
archivedDevice: true,
onSend(_method) {
throw new Error('not to be called');
},
});
expect(client.rawSend).not.toBeCalled();
// make sure the plugin gets connected
const pluginInstance: ReturnType<
typeof plugin
> = client.sandyPluginStates.get(definition.id)!.instanceApi;
expect(pluginInstance.connectedStub).toBeCalledTimes(0);
expect(pluginInstance.disconnectedStub).toBeCalledTimes(0);
expect(pluginInstance.activatedStub).toBeCalledTimes(1);
expect(pluginInstance.deactivatedStub).toBeCalledTimes(0);
// select non existing plugin
act(() => {
store.dispatch(
selectPlugin({
selectedPlugin: 'Logs',
deepLinkPayload: null,
}),
);
});
// bg plugin!
expect(client.rawSend).not.toBeCalled();
expect(pluginInstance.connectedStub).toBeCalledTimes(0);
expect(pluginInstance.disconnectedStub).toBeCalledTimes(0);
expect(pluginInstance.activatedStub).toBeCalledTimes(1);
expect(pluginInstance.deactivatedStub).toBeCalledTimes(1);
// go back
act(() => {
store.dispatch(
selectPlugin({
selectedPlugin: definition.id,
deepLinkPayload: null,
}),
);
});
expect(pluginInstance.connectedStub).toBeCalledTimes(0);
expect(pluginInstance.disconnectedStub).toBeCalledTimes(0);
expect(pluginInstance.activatedStub).toBeCalledTimes(2);
expect(pluginInstance.deactivatedStub).toBeCalledTimes(1);
expect(client.rawSend).not.toBeCalled();
// select something else
act(() => {
store.dispatch(
selectPlugin({
selectedPlugin: 'Logs',
deepLinkPayload: null,
}),
);
});
// select new plugin
act(() => {
store.dispatch(
selectPlugin({
selectedPlugin: definition.id,
deepLinkPayload: null,
}),
);
});
expect(pluginInstance.connectedStub).toBeCalledTimes(0);
expect(pluginInstance.disconnectedStub).toBeCalledTimes(0);
expect(pluginInstance.activatedStub).toBeCalledTimes(3);
expect(pluginInstance.deactivatedStub).toBeCalledTimes(2);
expect(client.rawSend).not.toBeCalled();
});

View File

@@ -20,7 +20,7 @@ export default class ArchivedDevice extends BaseDevice {
deviceType: DeviceType; deviceType: DeviceType;
title: string; title: string;
os: OS; os: OS;
screenshotHandle: string | null; screenshotHandle?: string | null;
source?: string; source?: string;
supportRequestDetails?: SupportFormRequestDetailsState; supportRequestDetails?: SupportFormRequestDetailsState;
}) { }) {
@@ -29,7 +29,7 @@ export default class ArchivedDevice extends BaseDevice {
this.connected.set(false); this.connected.set(false);
this.source = options.source || ''; this.source = options.source || '';
this.supportRequestDetails = options.supportRequestDetails; this.supportRequestDetails = options.supportRequestDetails;
this.archivedScreenshotHandle = options.screenshotHandle; this.archivedScreenshotHandle = options.screenshotHandle ?? null;
} }
archivedScreenshotHandle: string | null; archivedScreenshotHandle: string | null;

View File

@@ -11,7 +11,7 @@ import {createStore} from 'redux';
import BaseDevice from '../devices/BaseDevice'; import BaseDevice from '../devices/BaseDevice';
import {rootReducer} from '../store'; import {rootReducer} from '../store';
import {Store} from '../reducers/index'; import {Store} from '../reducers/index';
import Client, {ClientQuery} from '../Client'; import Client, {ClientQuery, FlipperClientConnection} from '../Client';
import {buildClientId} from '../utils/clientUtils'; 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';
@@ -20,6 +20,7 @@ import {getInstance} from '../fb-stubs/Logger';
import {initializeFlipperLibImplementation} from '../utils/flipperLibImplementation'; import {initializeFlipperLibImplementation} from '../utils/flipperLibImplementation';
import pluginManager from '../dispatcher/pluginManager'; import pluginManager from '../dispatcher/pluginManager';
import {PluginDetails} from 'flipper-plugin-lib'; import {PluginDetails} from 'flipper-plugin-lib';
import ArchivedDevice from '../devices/ArchivedDevice';
export interface AppOptions { export interface AppOptions {
plugins?: PluginDefinition[]; plugins?: PluginDefinition[];
@@ -37,6 +38,7 @@ export interface ClientOptions {
export interface DeviceOptions { export interface DeviceOptions {
serial?: string; serial?: string;
isSupportedByPlugin?: (p: PluginDetails) => boolean; isSupportedByPlugin?: (p: PluginDetails) => boolean;
archived?: boolean;
} }
export default class MockFlipper { export default class MockFlipper {
@@ -110,13 +112,17 @@ export default class MockFlipper {
public createDevice({ public createDevice({
serial, serial,
isSupportedByPlugin, isSupportedByPlugin,
archived,
}: DeviceOptions = {}): BaseDevice { }: DeviceOptions = {}): BaseDevice {
const device = new BaseDevice( const s = serial ?? `serial_${++this._deviceCounter}`;
serial ?? `serial_${++this._deviceCounter}`, const device = archived
'physical', ? new ArchivedDevice({
'MockAndroidDevice', serial: s,
'Android', deviceType: 'emulator',
); title: 'archived device',
os: 'Android',
})
: new BaseDevice(s, 'physical', 'MockAndroidDevice', 'Android');
device.supportsPlugin = !isSupportedByPlugin device.supportsPlugin = !isSupportedByPlugin
? () => true ? () => true
: isSupportedByPlugin; : isSupportedByPlugin;
@@ -172,13 +178,12 @@ export default class MockFlipper {
const client = new Client( const client = new Client(
id, id,
query, query,
null, // create a stub connection to avoid this plugin to be archived? device.isArchived ? null : createStubConnection(),
this._logger, this._logger,
this._store, this._store,
supportedPlugins, supportedPlugins,
device, device,
); );
// yikes // yikes
client.device = { client.device = {
then() { then() {
@@ -212,7 +217,9 @@ export default class MockFlipper {
}; };
client.rawSend = jest.fn(); client.rawSend = jest.fn();
await client.init(); if (!device.isArchived) {
await client.init();
}
// As convenience, by default we select the new client, star the plugin, and select it // As convenience, by default we select the new client, star the plugin, and select it
if (!skipRegister) { if (!skipRegister) {
@@ -227,3 +234,33 @@ export default class MockFlipper {
return client; return client;
} }
} }
function createStubConnection():
| FlipperClientConnection<any, any>
| null
| undefined {
return {
close() {
throw new Error('Should not be called in test');
},
fireAndForget() {
throw new Error('Should not be called in test');
},
requestResponse() {
throw new Error('Should not be called in test');
},
connectionStatus() {
return {
subscribe() {},
lift() {
throw new Error('Should not be called in test');
},
map() {
throw new Error('Should not be called in test');
},
take() {
throw new Error('Should not be called in test');
},
};
},
};
}

View File

@@ -62,6 +62,7 @@ type MockOptions = Partial<{
asBackgroundPlugin?: true; asBackgroundPlugin?: true;
supportedPlugins?: string[]; supportedPlugins?: string[];
device?: BaseDevice; device?: BaseDevice;
archivedDevice?: boolean;
}>; }>;
function isPluginEnabled( function isPluginEnabled(
@@ -90,7 +91,8 @@ export async function createMockFlipperWithPlugin(
const logger = mockFlipper.logger; const logger = mockFlipper.logger;
const store = mockFlipper.store; const store = mockFlipper.store;
const createDevice = (serial: string) => mockFlipper.createDevice({serial}); const createDevice = (serial: string, archived?: boolean) =>
mockFlipper.createDevice({serial, archived});
const createClient = async ( const createClient = async (
device: BaseDevice, device: BaseDevice,
name: string, name: string,
@@ -131,7 +133,7 @@ export async function createMockFlipperWithPlugin(
const device = options?.device const device = options?.device
? mockFlipper.loadDevice(options?.device) ? mockFlipper.loadDevice(options?.device)
: createDevice('serial'); : createDevice('serial', options?.archivedDevice);
const client = await createClient(device, 'TestApp'); const client = await createClient(device, 'TestApp');
store.dispatch(selectDevice(device)); store.dispatch(selectDevice(device));

View File

@@ -211,10 +211,16 @@ export function startPlugin<Module extends FlipperPluginModule<any>>(
}, },
connected: createState(true), connected: createState(true),
initPlugin() { initPlugin() {
if (options?.isArchived) {
return;
}
this.connected.set(true); this.connected.set(true);
pluginInstance.connect(); pluginInstance.connect();
}, },
deinitPlugin() { deinitPlugin() {
if (options?.isArchived) {
return;
}
this.connected.set(false); this.connected.set(false);
pluginInstance.disconnect(); pluginInstance.disconnect();
}, },

View File

@@ -111,6 +111,7 @@ This handler is untyped, and onMessage should be favored over using onUnhandledM
Usage: `client.onActivate(callback: () => void)` Usage: `client.onActivate(callback: () => void)`
Called when the plugin is selected by the user and mounted into the Flipper Desktop UI. See also the closely related `onConnect` event. Called when the plugin is selected by the user and mounted into the Flipper Desktop UI. See also the closely related `onConnect` event.
Unlike `onConnect`, `onActivate` will trigger as well for archived / imported devices.
#### `onDeactivate` #### `onDeactivate`
@@ -126,6 +127,7 @@ Usage: `client.onConnect(callback: () => void)`
Triggered once the connection with the plugin on the client is established, and for example [`send`](#send) can be called safely. Triggered once the connection with the plugin on the client is established, and for example [`send`](#send) can be called safely.
Typically, this happens when the plugin is activated (opened) in the Flipper Desktop. Typically, this happens when the plugin is activated (opened) in the Flipper Desktop.
However, for [background plugins](create-plugin#background-plugins), this happens immediately after the plugin has been instantiated. However, for [background plugins](create-plugin#background-plugins), this happens immediately after the plugin has been instantiated.
For archived / imported devices, this lifecycle is never triggered.
#### `onDisconnect` #### `onDisconnect`