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

View File

@@ -1015,3 +1015,262 @@ test('Sandy plugins support isPluginSupported + selectPlugin', async () => {
`);
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;
title: string;
os: OS;
screenshotHandle: string | null;
screenshotHandle?: string | null;
source?: string;
supportRequestDetails?: SupportFormRequestDetailsState;
}) {
@@ -29,7 +29,7 @@ export default class ArchivedDevice extends BaseDevice {
this.connected.set(false);
this.source = options.source || '';
this.supportRequestDetails = options.supportRequestDetails;
this.archivedScreenshotHandle = options.screenshotHandle;
this.archivedScreenshotHandle = options.screenshotHandle ?? null;
}
archivedScreenshotHandle: string | null;

View File

@@ -11,7 +11,7 @@ import {createStore} from 'redux';
import BaseDevice from '../devices/BaseDevice';
import {rootReducer} from '../store';
import {Store} from '../reducers/index';
import Client, {ClientQuery} from '../Client';
import Client, {ClientQuery, FlipperClientConnection} from '../Client';
import {buildClientId} from '../utils/clientUtils';
import {Logger} from '../fb-interfaces/Logger';
import {PluginDefinition} from '../plugin';
@@ -20,6 +20,7 @@ import {getInstance} from '../fb-stubs/Logger';
import {initializeFlipperLibImplementation} from '../utils/flipperLibImplementation';
import pluginManager from '../dispatcher/pluginManager';
import {PluginDetails} from 'flipper-plugin-lib';
import ArchivedDevice from '../devices/ArchivedDevice';
export interface AppOptions {
plugins?: PluginDefinition[];
@@ -37,6 +38,7 @@ export interface ClientOptions {
export interface DeviceOptions {
serial?: string;
isSupportedByPlugin?: (p: PluginDetails) => boolean;
archived?: boolean;
}
export default class MockFlipper {
@@ -110,13 +112,17 @@ export default class MockFlipper {
public createDevice({
serial,
isSupportedByPlugin,
archived,
}: DeviceOptions = {}): BaseDevice {
const device = new BaseDevice(
serial ?? `serial_${++this._deviceCounter}`,
'physical',
'MockAndroidDevice',
'Android',
);
const s = serial ?? `serial_${++this._deviceCounter}`;
const device = archived
? new ArchivedDevice({
serial: s,
deviceType: 'emulator',
title: 'archived device',
os: 'Android',
})
: new BaseDevice(s, 'physical', 'MockAndroidDevice', 'Android');
device.supportsPlugin = !isSupportedByPlugin
? () => true
: isSupportedByPlugin;
@@ -172,13 +178,12 @@ export default class MockFlipper {
const client = new Client(
id,
query,
null, // create a stub connection to avoid this plugin to be archived?
device.isArchived ? null : createStubConnection(),
this._logger,
this._store,
supportedPlugins,
device,
);
// yikes
client.device = {
then() {
@@ -212,7 +217,9 @@ export default class MockFlipper {
};
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
if (!skipRegister) {
@@ -227,3 +234,33 @@ export default class MockFlipper {
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;
supportedPlugins?: string[];
device?: BaseDevice;
archivedDevice?: boolean;
}>;
function isPluginEnabled(
@@ -90,7 +91,8 @@ export async function createMockFlipperWithPlugin(
const logger = mockFlipper.logger;
const store = mockFlipper.store;
const createDevice = (serial: string) => mockFlipper.createDevice({serial});
const createDevice = (serial: string, archived?: boolean) =>
mockFlipper.createDevice({serial, archived});
const createClient = async (
device: BaseDevice,
name: string,
@@ -131,7 +133,7 @@ export async function createMockFlipperWithPlugin(
const device = options?.device
? mockFlipper.loadDevice(options?.device)
: createDevice('serial');
: createDevice('serial', options?.archivedDevice);
const client = await createClient(device, 'TestApp');
store.dispatch(selectDevice(device));

View File

@@ -211,10 +211,16 @@ export function startPlugin<Module extends FlipperPluginModule<any>>(
},
connected: createState(true),
initPlugin() {
if (options?.isArchived) {
return;
}
this.connected.set(true);
pluginInstance.connect();
},
deinitPlugin() {
if (options?.isArchived) {
return;
}
this.connected.set(false);
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)`
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`
@@ -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.
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.
For archived / imported devices, this lifecycle is never triggered.
#### `onDisconnect`