Remove classic plugin infra

Summary:
This removes all code duplication / old plugin infra that isn't needed anymore when all plugin run on the Sandy plugin infra structure.

The diff is quite large, but the minimal one that passes tests and compiles. Existing tests are preserved by wrapping all remaining tests in `wrapSandy` for classic plugins where needed

Reviewed By: passy

Differential Revision: D29394738

fbshipit-source-id: 1315fabd9f048576aed15ed5f1cb6414d5fdbd40
This commit is contained in:
Michel Weststrate
2021-06-30 10:40:50 -07:00
committed by Facebook GitHub Bot
parent 9d6abd62c6
commit 16154e1343
31 changed files with 564 additions and 2330 deletions

View File

@@ -7,7 +7,7 @@
* @format
*/
import {PluginDefinition, FlipperPlugin, FlipperDevicePlugin} from './plugin';
import {PluginDefinition} from './plugin';
import BaseDevice, {OS} from './devices/BaseDevice';
import {Logger} from './fb-interfaces/Logger';
import {Store} from './reducers/index';
@@ -21,7 +21,6 @@ import invariant from 'invariant';
import {
getPluginKey,
defaultEnabledBackgroundPlugins,
isSandyPlugin,
} from './utils/pluginUtils';
import {processMessagesLater} from './utils/messageQueue';
import {emitBytesReceived} from './dispatcher/tracking';
@@ -118,10 +117,7 @@ export default class Client extends EventEmitter {
messageBuffer: Record<
string /*pluginKey*/,
{
plugin:
| typeof FlipperPlugin
| typeof FlipperDevicePlugin
| _SandyPluginInstance;
plugin: _SandyPluginInstance;
messages: Params[];
}
> = {};
@@ -220,7 +216,7 @@ export default class Client extends EventEmitter {
initFromImport(initialStates: Record<string, Record<string, any>>): this {
this.plugins.forEach((pluginId) => {
const plugin = this.getPlugin(pluginId);
if (isSandyPlugin(plugin)) {
if (plugin) {
// TODO: needs to be wrapped in error tracking T68955280
this.sandyPluginStates.set(
plugin.id,
@@ -253,7 +249,7 @@ export default class Client extends EventEmitter {
) {
// start a plugin on start if it is a SandyPlugin, which is enabled, and doesn't have persisted state yet
if (
isSandyPlugin(plugin) &&
plugin &&
(isEnabled || defaultEnabledBackgroundPlugins.includes(plugin.id)) &&
!this.sandyPluginStates.has(plugin.id)
) {

View File

@@ -7,14 +7,10 @@
* @format
*/
import {
FlipperPlugin,
FlipperDevicePlugin,
Props as PluginProps,
} from './plugin';
import {FlipperPlugin, FlipperDevicePlugin} from './plugin';
import {Logger} from './fb-interfaces/Logger';
import BaseDevice from './devices/BaseDevice';
import {pluginKey as getPluginKey} from './reducers/pluginStates';
import {pluginKey as getPluginKey} from './utils/pluginUtils';
import Client from './Client';
import {
ErrorBoundary,
@@ -31,8 +27,6 @@ import {StaticView, setStaticView} from './reducers/connections';
import {switchPlugin} from './reducers/pluginManager';
import React, {PureComponent} from 'react';
import {connect, ReactReduxContext} from 'react-redux';
import {setPluginState} from './reducers/pluginStates';
import {Settings} from './reducers/settings';
import {selectPlugin} from './reducers/connections';
import {State as Store, MiddlewareAPI} from './reducers/index';
import {activateMenuItems} from './MenuBar';
@@ -40,12 +34,11 @@ import {Message} from './reducers/pluginMessageQueue';
import {IdlerImpl} from './utils/Idler';
import {processMessageQueue} from './utils/messageQueue';
import {Layout} from './ui';
import {theme, TrackingScope, _SandyPluginRenderer} from 'flipper-plugin';
import {theme, _SandyPluginRenderer} from 'flipper-plugin';
import {
ActivePluginListItem,
isDevicePlugin,
isDevicePluginDefinition,
isSandyPlugin,
} from './utils/pluginUtils';
import {ContentContainer} from './sandy-chrome/ContentContainer';
import {Alert, Typography} from 'antd';
@@ -59,13 +52,6 @@ import {getActiveClient, getActivePlugin} from './selectors/connections';
const {Text, Link} = Typography;
const Container = styled(FlexColumn)({
width: 0,
flexGrow: 1,
flexShrink: 1,
backgroundColor: colors.white,
});
export const SidebarContainer = styled(FlexRow)({
backgroundColor: theme.backgroundWash,
height: '100%',
@@ -103,19 +89,14 @@ const ProgressBarBar = styled.div<{progress: number}>(({progress}) => ({
type OwnProps = {
logger: Logger;
isSandy?: boolean;
};
type StateFromProps = {
pluginState: Object;
activePlugin: ActivePluginListItem | null;
target: Client | BaseDevice | null;
pluginKey: string | null;
deepLinkPayload: unknown;
selectedApp: string | null;
isArchivedDevice: boolean;
pendingMessages: Message[] | undefined;
settingsState: Settings;
latestInstalledVersion: InstalledPluginDetails | undefined;
};
@@ -125,7 +106,6 @@ type DispatchFromProps = {
selectedApp?: string | null;
deepLinkPayload: unknown;
}) => any;
setPluginState: (payload: {pluginKey: string; state: any}) => void;
setStaticView: (payload: StaticView) => void;
enablePlugin: typeof switchPlugin;
loadPlugin: typeof loadPlugin;
@@ -228,17 +208,13 @@ class PluginContainer extends PureComponent<Props, State> {
if (
target instanceof Client &&
activePlugin &&
(isSandyPlugin(activePlugin.definition) ||
activePlugin.definition.persistedStateReducer) &&
pluginKey &&
pendingMessages?.length
) {
const start = Date.now();
this.idler = new IdlerImpl();
processMessageQueue(
isSandyPlugin(activePlugin.definition)
? target.sandyPluginStates.get(activePlugin.definition.id)!
: activePlugin.definition,
target.sandyPluginStates.get(activePlugin.definition.id)!,
pluginKey,
this.store,
(progress) => {
@@ -360,18 +336,8 @@ class PluginContainer extends PureComponent<Props, State> {
}
renderPlugin() {
const {
pluginState,
setPluginState,
activePlugin,
pluginKey,
target,
isArchivedDevice,
selectedApp,
settingsState,
isSandy,
latestInstalledVersion,
} = this.props;
const {activePlugin, pluginKey, target, latestInstalledVersion} =
this.props;
if (
!activePlugin ||
!target ||
@@ -381,7 +347,6 @@ class PluginContainer extends PureComponent<Props, State> {
console.warn(`No selected plugin. Rendering empty!`);
return this.renderNoPluginActive();
}
let pluginElement: null | React.ReactElement<any>;
const showUpdateAlert =
latestInstalledVersion &&
activePlugin &&
@@ -392,71 +357,14 @@ class PluginContainer extends PureComponent<Props, State> {
latestInstalledVersion.version,
activePlugin.definition.version,
);
if (isSandyPlugin(activePlugin.definition)) {
// Make sure we throw away the container for different pluginKey!
const instance = target.sandyPluginStates.get(activePlugin.definition.id);
if (!instance) {
// happens if we selected a plugin that is not enabled on a specific app or not supported on a specific device.
return this.renderNoPluginActive();
}
pluginElement = (
<_SandyPluginRenderer key={pluginKey} plugin={instance} />
);
} else {
const props: PluginProps<Object> & {
key: string;
ref: (
ref:
| FlipperPlugin<any, any, any>
| FlipperDevicePlugin<any, any, any>
| null
| undefined,
) => void;
} = {
key: pluginKey,
logger: this.props.logger,
selectedApp,
persistedState: activePlugin.definition.defaultPersistedState
? {
...activePlugin.definition.defaultPersistedState,
...pluginState,
}
: pluginState,
setStaticView: (payload: StaticView) =>
this.props.setStaticView(payload),
setPersistedState: (state) => setPluginState({pluginKey, state}),
target,
deepLinkPayload: this.props.deepLinkPayload,
selectPlugin: (pluginID: string, deepLinkPayload: unknown) => {
const {target} = this.props;
// check if plugin will be available
if (target instanceof Client && target.plugins.has(pluginID)) {
this.props.selectPlugin({
selectedPlugin: pluginID,
deepLinkPayload,
});
return true;
} else if (target instanceof BaseDevice) {
this.props.selectPlugin({
selectedPlugin: pluginID,
deepLinkPayload,
});
return true;
} else {
return false;
}
},
ref: this.refChanged,
isArchivedDevice,
settingsState,
};
pluginElement = (
<TrackingScope scope={'plugin:' + activePlugin.definition.id}>
{React.createElement(activePlugin.definition, props)}
</TrackingScope>
);
}
return isSandy ? (
return (
<Layout.Top>
<div>
{showUpdateAlert && (
@@ -490,23 +398,13 @@ class PluginContainer extends PureComponent<Props, State> {
heading={`Plugin "${
activePlugin.definition.title || 'Unknown'
}" encountered an error during render`}>
<ContentContainer>{pluginElement}</ContentContainer>
<ContentContainer>
<_SandyPluginRenderer key={pluginKey} plugin={instance} />
</ContentContainer>
</ErrorBoundary>
<SidebarContainer id="detailsSidebar" />
</Layout.Right>
</Layout.Top>
) : (
<React.Fragment>
<Container key="plugin">
<ErrorBoundary
heading={`Plugin "${
activePlugin.definition.title || 'Unknown'
}" encountered an error during render`}>
{pluginElement}
</ErrorBoundary>
</Container>
<SidebarContainer id="detailsSidebar" />
</React.Fragment>
);
}
}
@@ -516,11 +414,9 @@ export default connect<StateFromProps, DispatchFromProps, OwnProps, Store>(
let pluginKey: string | null = null;
let target: BaseDevice | Client | null = null;
const {
connections: {selectedDevice, selectedApp, deepLinkPayload},
pluginStates,
connections: {selectedDevice, deepLinkPayload},
plugins: {installedPlugins},
pluginMessageQueue,
settingsState,
} = state;
const selectedClient = getActiveClient(state);
const activePlugin = getActivePlugin(state);
@@ -536,24 +432,17 @@ export default connect<StateFromProps, DispatchFromProps, OwnProps, Store>(
pluginKey = getPluginKey(selectedClient.id, activePlugin.details.id);
}
}
const isArchivedDevice = !selectedDevice
? false
: selectedDevice.isArchived;
const pendingMessages = pluginKey
? pluginMessageQueue[pluginKey]
: undefined;
const s: StateFromProps = {
pluginState: pluginStates[pluginKey as string],
activePlugin,
target,
deepLinkPayload,
pluginKey,
isArchivedDevice,
selectedApp: selectedApp || null,
pendingMessages,
settingsState,
latestInstalledVersion: installedPlugins.get(
activePlugin?.details?.name ?? '',
),
@@ -561,7 +450,6 @@ export default connect<StateFromProps, DispatchFromProps, OwnProps, Store>(
return s;
},
{
setPluginState,
selectPlugin,
setStaticView,
enablePlugin: switchPlugin,

View File

@@ -72,7 +72,14 @@ test('Plugin container can render plugin and receive updates', async () => {
<body>
<div>
<div
class="css-w6yhx2-View-FlexBox-FlexColumn"
class="css-1x2cmzz-SandySplitContainer e1hsqii10"
>
<div />
<div
class="css-1knrt0j-SandySplitContainer e1hsqii10"
>
<div
class="css-1woty6b-Container"
>
<h1>
Hello:
@@ -89,6 +96,8 @@ test('Plugin container can render plugin and receive updates', async () => {
id="detailsSidebar"
/>
</div>
</div>
</div>
</body>
`);
@@ -164,7 +173,14 @@ test('PluginContainer can render Sandy plugins', async () => {
<body>
<div>
<div
class="css-w6yhx2-View-FlexBox-FlexColumn"
class="css-1x2cmzz-SandySplitContainer e1hsqii10"
>
<div />
<div
class="css-1knrt0j-SandySplitContainer e1hsqii10"
>
<div
class="css-1woty6b-Container"
>
<div>
Hello from Sandy
@@ -176,6 +192,8 @@ test('PluginContainer can render Sandy plugins', async () => {
id="detailsSidebar"
/>
</div>
</div>
</div>
</body>
`);
expect(renders).toBe(1);
@@ -195,7 +213,14 @@ test('PluginContainer can render Sandy plugins', async () => {
<body>
<div>
<div
class="css-w6yhx2-View-FlexBox-FlexColumn"
class="css-1x2cmzz-SandySplitContainer e1hsqii10"
>
<div />
<div
class="css-1knrt0j-SandySplitContainer e1hsqii10"
>
<div
class="css-1woty6b-Container"
>
<div>
Hello from Sandy
@@ -207,6 +232,8 @@ test('PluginContainer can render Sandy plugins', async () => {
id="detailsSidebar"
/>
</div>
</div>
</div>
</body>
`);
@@ -262,7 +289,14 @@ test('PluginContainer can render Sandy plugins', async () => {
<body>
<div>
<div
class="css-w6yhx2-View-FlexBox-FlexColumn"
class="css-1x2cmzz-SandySplitContainer e1hsqii10"
>
<div />
<div
class="css-1knrt0j-SandySplitContainer e1hsqii10"
>
<div
class="css-1woty6b-Container"
>
<div>
Hello from Sandy
@@ -274,6 +308,8 @@ test('PluginContainer can render Sandy plugins', async () => {
id="detailsSidebar"
/>
</div>
</div>
</div>
</body>
`);
@@ -501,7 +537,14 @@ test('PluginContainer + Sandy plugin supports deeplink', async () => {
<body>
<div>
<div
class="css-w6yhx2-View-FlexBox-FlexColumn"
class="css-1x2cmzz-SandySplitContainer e1hsqii10"
>
<div />
<div
class="css-1knrt0j-SandySplitContainer e1hsqii10"
>
<div
class="css-1woty6b-Container"
>
<h1>
hello
@@ -513,6 +556,8 @@ test('PluginContainer + Sandy plugin supports deeplink', async () => {
id="detailsSidebar"
/>
</div>
</div>
</div>
</body>
`);
@@ -532,7 +577,14 @@ test('PluginContainer + Sandy plugin supports deeplink', async () => {
<body>
<div>
<div
class="css-w6yhx2-View-FlexBox-FlexColumn"
class="css-1x2cmzz-SandySplitContainer e1hsqii10"
>
<div />
<div
class="css-1knrt0j-SandySplitContainer e1hsqii10"
>
<div
class="css-1woty6b-Container"
>
<h1>
hello
@@ -544,6 +596,8 @@ test('PluginContainer + Sandy plugin supports deeplink', async () => {
id="detailsSidebar"
/>
</div>
</div>
</div>
</body>
`);
@@ -655,7 +709,14 @@ test('PluginContainer can render Sandy device plugins', async () => {
<body>
<div>
<div
class="css-w6yhx2-View-FlexBox-FlexColumn"
class="css-1x2cmzz-SandySplitContainer e1hsqii10"
>
<div />
<div
class="css-1knrt0j-SandySplitContainer e1hsqii10"
>
<div
class="css-1woty6b-Container"
>
<div>
Hello from Sandy:
@@ -666,6 +727,8 @@ test('PluginContainer can render Sandy device plugins', async () => {
id="detailsSidebar"
/>
</div>
</div>
</div>
</body>
`);
expect(renders).toBe(1);
@@ -686,7 +749,14 @@ test('PluginContainer can render Sandy device plugins', async () => {
<body>
<div>
<div
class="css-w6yhx2-View-FlexBox-FlexColumn"
class="css-1x2cmzz-SandySplitContainer e1hsqii10"
>
<div />
<div
class="css-1knrt0j-SandySplitContainer e1hsqii10"
>
<div
class="css-1woty6b-Container"
>
<div>
Hello from Sandy:
@@ -698,6 +768,8 @@ test('PluginContainer can render Sandy device plugins', async () => {
id="detailsSidebar"
/>
</div>
</div>
</div>
</body>
`);
@@ -778,7 +850,14 @@ test('PluginContainer + Sandy device plugin supports deeplink', async () => {
<body>
<div>
<div
class="css-w6yhx2-View-FlexBox-FlexColumn"
class="css-1x2cmzz-SandySplitContainer e1hsqii10"
>
<div />
<div
class="css-1knrt0j-SandySplitContainer e1hsqii10"
>
<div
class="css-1woty6b-Container"
>
<h1>
hello
@@ -790,6 +869,8 @@ test('PluginContainer + Sandy device plugin supports deeplink', async () => {
id="detailsSidebar"
/>
</div>
</div>
</div>
</body>
`);
@@ -809,7 +890,14 @@ test('PluginContainer + Sandy device plugin supports deeplink', async () => {
<body>
<div>
<div
class="css-w6yhx2-View-FlexBox-FlexColumn"
class="css-1x2cmzz-SandySplitContainer e1hsqii10"
>
<div />
<div
class="css-1knrt0j-SandySplitContainer e1hsqii10"
>
<div
class="css-1woty6b-Container"
>
<h1>
hello
@@ -821,6 +909,8 @@ test('PluginContainer + Sandy device plugin supports deeplink', async () => {
id="detailsSidebar"
/>
</div>
</div>
</div>
</body>
`);
@@ -1076,7 +1166,14 @@ test('PluginContainer can render Sandy plugins for archived devices', async () =
<body>
<div>
<div
class="css-w6yhx2-View-FlexBox-FlexColumn"
class="css-1x2cmzz-SandySplitContainer e1hsqii10"
>
<div />
<div
class="css-1knrt0j-SandySplitContainer e1hsqii10"
>
<div
class="css-1woty6b-Container"
>
<div>
Hello from Sandy
@@ -1088,6 +1185,8 @@ test('PluginContainer can render Sandy plugins for archived devices', async () =
id="detailsSidebar"
/>
</div>
</div>
</div>
</body>
`);
expect(renders).toBe(1);
@@ -1136,7 +1235,14 @@ test('PluginContainer can render Sandy plugins for archived devices', async () =
<body>
<div>
<div
class="css-w6yhx2-View-FlexBox-FlexColumn"
class="css-1x2cmzz-SandySplitContainer e1hsqii10"
>
<div />
<div
class="css-1knrt0j-SandySplitContainer e1hsqii10"
>
<div
class="css-1woty6b-Container"
>
<div>
Hello from Sandy
@@ -1148,6 +1254,8 @@ test('PluginContainer can render Sandy plugins for archived devices', async () =
id="detailsSidebar"
/>
</div>
</div>
</div>
</body>
`);

View File

@@ -1,77 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`can create a Fake flipper 1`] = `
Object {
"androidEmulators": Array [],
"clients": Array [
Object {
"id": "TestApp#Android#MockAndroidDevice#serial",
"query": Object {
"app": "TestApp",
"device": "MockAndroidDevice",
"device_id": "serial",
"os": "Android",
"sdk_version": 4,
},
},
],
"deepLinkPayload": null,
"devices": Array [
Object {
"deviceType": "physical",
"os": "Android",
"serial": "serial",
"title": "MockAndroidDevice",
},
],
"enabledDevicePlugins": Set {
"DeviceLogs",
"CrashReporter",
"MobileBuilds",
"Hermesdebuggerrn",
"React",
},
"enabledPlugins": Object {
"TestApp": Array [
"TestPlugin",
],
},
"selectedApp": "TestApp#Android#MockAndroidDevice#serial",
"selectedAppPluginListRevision": 0,
"selectedDevice": Object {
"deviceType": "physical",
"os": "Android",
"serial": "serial",
"title": "MockAndroidDevice",
},
"selectedPlugin": "TestPlugin",
"staticView": null,
"uninitializedClients": Array [],
"userPreferredApp": "TestApp#Android#MockAndroidDevice#serial",
"userPreferredDevice": "MockAndroidDevice",
"userPreferredPlugin": "TestPlugin",
}
`;
exports[`can create a Fake flipper 2`] = `
Object {
"bundledPlugins": Map {},
"clientPlugins": Map {
"TestPlugin" => [Function],
},
"devicePlugins": Map {},
"disabledPlugins": Array [],
"failedPlugins": Array [],
"gatekeepedPlugins": Array [],
"initialised": false,
"installedPlugins": Map {},
"loadedPlugins": Map {},
"marketplacePlugins": Array [],
"selectedPlugins": Array [],
"uninstalledPluginNames": Set {},
}
`;
exports[`can create a Fake flipper with legacy wrapper 1`] = `
Object {
"androidEmulators": Array [],

View File

@@ -40,26 +40,6 @@ class TestPlugin extends FlipperPlugin<any, any, any> {
}
}
test('can create a Fake flipper', async () => {
const {client, device, store, sendMessage} =
await createMockFlipperWithPlugin(TestPlugin, {disableLegacyWrapper: true});
expect(client).toBeTruthy();
expect(device).toBeTruthy();
expect(store).toBeTruthy();
expect(sendMessage).toBeTruthy();
expect(client.plugins.has(TestPlugin.id)).toBe(true);
expect(store.getState().connections).toMatchSnapshot();
expect(store.getState().plugins).toMatchSnapshot();
sendMessage('inc', {});
expect(store.getState().pluginStates).toMatchInlineSnapshot(`
Object {
"TestApp#Android#MockAndroidDevice#serial#TestPlugin": Object {
"count": 1,
},
}
`);
});
const testIdler = new TestIdler();
function testOnStatusMessage() {

View File

@@ -1,139 +0,0 @@
/**
* 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 React from 'react';
import {create, act, ReactTestRenderer} from 'react-test-renderer';
import {Provider} from 'react-redux';
import ExportDataPluginSheet from '../ExportDataPluginSheet';
import {FlipperPlugin, FlipperDevicePlugin} from '../../plugin';
import {getPluginKey} from '../../utils/pluginUtils';
import {createMockFlipperWithPlugin} from '../../test-utils/createMockFlipperWithPlugin';
import {setPluginState} from '../../reducers/pluginStates';
import {getExportablePlugins} from '../../selectors/connections';
class TestPlugin extends FlipperPlugin<any, any, any> {
static details = {
title: 'TestPlugin',
id: 'TestPlugin',
} as any;
}
TestPlugin.title = 'TestPlugin';
TestPlugin.id = 'TestPlugin';
TestPlugin.defaultPersistedState = {msg: 'Test plugin'};
class TestDevicePlugin extends FlipperDevicePlugin<any, any, any> {
static details = {
title: 'TestDevicePlugin',
id: 'TestDevicePlugin',
} as any;
static supportsDevice() {
return true;
}
}
TestDevicePlugin.title = 'TestDevicePlugin';
TestDevicePlugin.id = 'TestDevicePlugin';
TestDevicePlugin.defaultPersistedState = {msg: 'TestDevicePlugin'};
test('SettingsSheet snapshot with nothing enabled', async () => {
let root: ReactTestRenderer;
const {store, togglePlugin, client, device, pluginKey} =
await createMockFlipperWithPlugin(TestPlugin, {
additionalPlugins: [TestDevicePlugin],
});
togglePlugin();
store.dispatch(
setPluginState({
pluginKey,
state: {test: '1'},
}),
);
expect(getExportablePlugins(store.getState())).toEqual([]);
// makes device plugin visible
store.dispatch(
setPluginState({
pluginKey: getPluginKey(undefined, device, 'TestDevicePlugin'),
state: {test: '1'},
}),
);
expect(getExportablePlugins(store.getState())).toEqual([
{
id: 'TestDevicePlugin',
label: 'TestDevicePlugin',
},
]);
await act(async () => {
root = create(
<Provider store={store}>
<ExportDataPluginSheet onHide={() => {}} />
</Provider>,
);
});
expect(root!.toJSON()).toMatchSnapshot();
});
test('SettingsSheet snapshot with one plugin enabled', async () => {
let root: ReactTestRenderer;
const {store, device, pluginKey} = await createMockFlipperWithPlugin(
TestPlugin,
{
additionalPlugins: [TestDevicePlugin],
},
);
// enabled
// in Sandy wrapper, a plugin is either persistable or not, but it doesn't depend on the current state.
// So this plugin will show up, even though its state is still the default
expect(getExportablePlugins(store.getState())).toEqual([
{
id: 'TestPlugin',
label: 'TestPlugin',
},
]);
store.dispatch(
setPluginState({
pluginKey,
state: {test: '1'},
}),
);
store.dispatch(
setPluginState({
pluginKey: getPluginKey(undefined, device, 'TestDevicePlugin'),
state: {test: '1'},
}),
);
expect(getExportablePlugins(store.getState())).toEqual([
{
id: 'TestDevicePlugin',
label: 'TestDevicePlugin',
},
{
id: 'TestPlugin',
label: 'TestPlugin',
},
]);
await act(async () => {
root = create(
<Provider store={store}>
<ExportDataPluginSheet onHide={() => {}} />
</Provider>,
);
});
expect(root!.toJSON()).toMatchSnapshot();
});

View File

@@ -1,252 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SettingsSheet snapshot with nothing enabled 1`] = `
<div
className="css-p0wmbe-View-FlexBox-FlexColumn"
>
<div
className="css-1edwc8r-View-FlexBox-FlexColumn"
>
<div
className="css-18abd42-View-FlexBox-FlexColumn ecr18to0"
>
<span
className="css-meh2qi-Text"
>
Select the plugins for which you want to export the data
</span>
<div
className="css-1houjzq-View-FlexBox-FlexColumn"
>
<div
className="css-18abd42-View-FlexBox-FlexColumn ecr18to0"
>
<div
className="css-auhar3-TooltipContainer e1abcqbd0"
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
<div
className="css-1jrm6r3"
>
<div
className="css-wospjg-View-FlexBox-FlexRow epz0qe20"
style={
Object {
"alignItems": "center",
}
}
>
<span
className="css-xsnw23-Text e19o3fcp0"
>
TestDevicePlugin
</span>
<div
className="css-t4wmtk-View-FlexBox-Spacer e13mj6h80"
/>
<input
checked={false}
className="css-1pxrk7-CheckboxContainer e28aqfo0"
disabled={false}
onChange={[Function]}
type="checkbox"
/>
</div>
</div>
<div
className="css-1p0wwd3-View"
/>
</div>
</div>
</div>
</div>
<div
className="css-1yqvjo0"
>
<div
className="css-wospjg-View-FlexBox-FlexRow epz0qe20"
>
<div
className="css-t4wmtk-View-FlexBox-Spacer e13mj6h80"
/>
<div
className="css-12n892b"
>
<button
className="ant-btn ant-btn-default"
onClick={[Function]}
type="button"
>
<span>
Close
</span>
</button>
</div>
<div
className="css-auhar3-TooltipContainer e1abcqbd0"
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
<button
className="ant-btn ant-btn-primary"
disabled={true}
onClick={[Function]}
type="button"
>
<span>
Submit
</span>
</button>
</div>
</div>
</div>
</div>
</div>
`;
exports[`SettingsSheet snapshot with one plugin enabled 1`] = `
<div
className="css-p0wmbe-View-FlexBox-FlexColumn"
>
<div
className="css-1edwc8r-View-FlexBox-FlexColumn"
>
<div
className="css-18abd42-View-FlexBox-FlexColumn ecr18to0"
>
<span
className="css-meh2qi-Text"
>
Select the plugins for which you want to export the data
</span>
<div
className="css-1houjzq-View-FlexBox-FlexColumn"
>
<div
className="css-18abd42-View-FlexBox-FlexColumn ecr18to0"
>
<div
className="css-auhar3-TooltipContainer e1abcqbd0"
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
<div
className="css-1jrm6r3"
>
<div
className="css-wospjg-View-FlexBox-FlexRow epz0qe20"
style={
Object {
"alignItems": "center",
}
}
>
<span
className="css-xsnw23-Text e19o3fcp0"
>
TestDevicePlugin
</span>
<div
className="css-t4wmtk-View-FlexBox-Spacer e13mj6h80"
/>
<input
checked={false}
className="css-1pxrk7-CheckboxContainer e28aqfo0"
disabled={false}
onChange={[Function]}
type="checkbox"
/>
</div>
</div>
<div
className="css-1p0wwd3-View"
/>
</div>
</div>
<div
className="css-18abd42-View-FlexBox-FlexColumn ecr18to0"
>
<div
className="css-auhar3-TooltipContainer e1abcqbd0"
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
<div
className="css-1jrm6r3"
>
<div
className="css-wospjg-View-FlexBox-FlexRow epz0qe20"
style={
Object {
"alignItems": "center",
}
}
>
<span
className="css-xsnw23-Text e19o3fcp0"
>
TestPlugin
</span>
<div
className="css-t4wmtk-View-FlexBox-Spacer e13mj6h80"
/>
<input
checked={false}
className="css-1pxrk7-CheckboxContainer e28aqfo0"
disabled={false}
onChange={[Function]}
type="checkbox"
/>
</div>
</div>
<div
className="css-1p0wwd3-View"
/>
</div>
</div>
</div>
</div>
<div
className="css-1yqvjo0"
>
<div
className="css-wospjg-View-FlexBox-FlexRow epz0qe20"
>
<div
className="css-t4wmtk-View-FlexBox-Spacer e13mj6h80"
/>
<div
className="css-12n892b"
>
<button
className="ant-btn ant-btn-default"
onClick={[Function]}
type="button"
>
<span>
Close
</span>
</button>
</div>
<div
className="css-auhar3-TooltipContainer e1abcqbd0"
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
<button
className="ant-btn ant-btn-primary"
disabled={true}
onClick={[Function]}
type="button"
>
<span>
Submit
</span>
</button>
</div>
</div>
</div>
</div>
</div>
`;

View File

@@ -15,7 +15,7 @@ import {connect} from 'react-redux';
import {Text, ManagedTable, styled, colors, Link, Bordered} from '../../ui';
import StatusIndicator from '../../ui/components/StatusIndicator';
import {State as Store} from '../../reducers';
import {DevicePluginDefinition, ClientPluginDefinition} from '../../plugin';
import {PluginDefinition} from '../../plugin';
const InfoText = styled(Text)({
lineHeight: '130%',
@@ -43,8 +43,8 @@ type StateFromProps = {
failedPlugins: Array<[PluginDetails, string]>;
clients: Array<Client>;
selectedDevice: string | null | undefined;
devicePlugins: DevicePluginDefinition[];
clientPlugins: ClientPluginDefinition[];
devicePlugins: PluginDefinition[];
clientPlugins: PluginDefinition[];
};
type DispatchFromProps = {};

View File

@@ -18,11 +18,7 @@ import {
createState,
_getFlipperLibImplementation,
} from 'flipper-plugin';
import {
DevicePluginDefinition,
DevicePluginMap,
FlipperDevicePlugin,
} from '../plugin';
import {PluginDefinition, DevicePluginMap} from '../plugin';
import {DeviceSpec, OS as PluginOS, PluginDetails} from 'flipper-plugin-lib';
export type DeviceShell = {
@@ -191,9 +187,9 @@ export default class BaseDevice {
return null;
}
supportsPlugin(plugin: DevicePluginDefinition | PluginDetails) {
supportsPlugin(plugin: PluginDefinition | PluginDetails) {
let pluginDetails: PluginDetails;
if (isDevicePluginDefinition(plugin)) {
if (plugin instanceof _SandyPluginDefinition) {
pluginDetails = plugin.details;
if (!pluginDetails.pluginType && !pluginDetails.supportedDevices) {
// TODO T84453692: this branch is to support plugins defined with the legacy approach. Need to remove this branch after some transition period when
@@ -205,7 +201,7 @@ export default class BaseDevice {
false)
);
} else {
return plugin.supportsDevice(this);
return (plugin as any).supportsDevice(this);
}
}
} else {
@@ -240,7 +236,7 @@ export default class BaseDevice {
}
}
loadDevicePlugin(plugin: DevicePluginDefinition, initialState?: any) {
loadDevicePlugin(plugin: PluginDefinition, initialState?: any) {
if (!this.supportsPlugin(plugin)) {
return;
}
@@ -286,12 +282,3 @@ export default class BaseDevice {
this.sandyPluginStates.clear();
}
}
function isDevicePluginDefinition(
definition: any,
): definition is DevicePluginDefinition {
return (
(definition as any).prototype instanceof FlipperDevicePlugin ||
(definition instanceof _SandyPluginDefinition && definition.isDevicePlugin)
);
}

View File

@@ -10,16 +10,13 @@
import {Store} from '../reducers/index';
import {Logger} from '../fb-interfaces/Logger';
import {PluginNotification} from '../reducers/notifications';
import {PluginDefinition} from '../plugin';
import {ipcRenderer, IpcRendererEvent} from 'electron';
import {
setActiveNotifications,
updatePluginBlocklist,
updateCategoryBlocklist,
} from '../reducers/notifications';
import {textContent} from '../utils/index';
import {deconstructPluginKey} from '../utils/clientUtils';
import {getPluginTitle, isSandyPlugin} from '../utils/pluginUtils';
import {getPluginTitle} from '../utils/pluginUtils';
import {sideEffect} from '../utils/sideEffect';
import {openNotification} from '../sandy-chrome/notification/Notification';
@@ -28,7 +25,6 @@ const NOTIFICATION_THROTTLE = 5 * 1000; // in milliseconds
export default (store: Store, logger: Logger) => {
const knownNotifications: Set<string> = new Set();
const knownPluginStates: Map<string, Object> = new Map();
const lastNotificationTime: Map<string, number> = new Map();
ipcRenderer.on(
@@ -78,56 +74,16 @@ export default (store: Store, logger: Logger) => {
sideEffect(
store,
{name: 'notifications', throttleMs: 500},
({notifications, pluginStates, plugins}) => ({
({notifications, plugins}) => ({
notifications,
pluginStates,
devicePlugins: plugins.devicePlugins,
clientPlugins: plugins.clientPlugins,
}),
({notifications, pluginStates, devicePlugins, clientPlugins}, store) => {
({notifications, devicePlugins, clientPlugins}, store) => {
function getPlugin(name: string) {
return devicePlugins.get(name) ?? clientPlugins.get(name);
}
Object.keys(pluginStates).forEach((key) => {
if (knownPluginStates.get(key) !== pluginStates[key]) {
knownPluginStates.set(key, pluginStates[key]);
const plugin = deconstructPluginKey(key);
const pluginName = plugin.pluginName;
const client = plugin.client;
if (!pluginName) {
return;
}
const persistingPlugin: undefined | PluginDefinition =
getPlugin(pluginName);
if (
persistingPlugin &&
!isSandyPlugin(persistingPlugin) &&
persistingPlugin.getActiveNotifications
) {
try {
const notifications = persistingPlugin.getActiveNotifications(
pluginStates[key],
);
store.dispatch(
setActiveNotifications({
notifications,
client,
pluginId: pluginName,
}),
);
} catch (e) {
console.error(
'Failed to compute notifications for plugin ' + pluginName,
e,
);
}
}
}
});
const {activeNotifications, blocklistedPlugins, blocklistedCategories} =
notifications;

View File

@@ -8,7 +8,6 @@
*/
import type {Store} from '../reducers/index';
import {clearPluginState} from '../reducers/pluginStates';
import type {Logger} from '../fb-interfaces/Logger';
import {
LoadPluginActionPayload,
@@ -27,12 +26,7 @@ import {
import {sideEffect} from '../utils/sideEffect';
import {requirePlugin} from './plugins';
import {showErrorNotification} from '../utils/notifications';
import {
ClientPluginDefinition,
DevicePluginDefinition,
FlipperPlugin,
PluginDefinition,
} from '../plugin';
import {PluginDefinition} from '../plugin';
import type Client from '../Client';
import {unloadModule} from '../utils/electronModuleCache';
import {
@@ -148,7 +142,6 @@ function uninstallPlugin(store: Store, {plugin}: UninstallPluginActionPayload) {
clients.forEach((client) => {
stopPlugin(client, plugin.id);
});
store.dispatch(clearPluginState({pluginId: plugin.id}));
if (!plugin.details.isBundled) {
unloadPluginModule(plugin.details);
}
@@ -194,7 +187,7 @@ function switchPlugin(
function switchClientPlugin(
store: Store,
plugin: ClientPluginDefinition,
plugin: PluginDefinition,
selectedApp: string | undefined,
) {
selectedApp = selectedApp ?? getSelectedAppId(store);
@@ -224,7 +217,7 @@ function switchClientPlugin(
}
}
function switchDevicePlugin(store: Store, plugin: DevicePluginDefinition) {
function switchDevicePlugin(store: Store, plugin: PluginDefinition) {
const {connections} = store.getState();
const devicesWithPlugin = connections.devices.filter((d) =>
d.supportsPlugin(plugin.details),
@@ -244,7 +237,7 @@ function switchDevicePlugin(store: Store, plugin: DevicePluginDefinition) {
function updateClientPlugin(
store: Store,
plugin: typeof FlipperPlugin,
plugin: PluginDefinition,
enable: boolean,
) {
const clients = store.getState().connections.clients;
@@ -266,7 +259,6 @@ function updateClientPlugin(
clientsWithEnabledPlugin.forEach((client) => {
stopPlugin(client, plugin.id);
});
store.dispatch(clearPluginState({pluginId: plugin.id}));
clientsWithEnabledPlugin.forEach((client) => {
startPlugin(client, plugin, true);
});
@@ -279,7 +271,7 @@ function updateClientPlugin(
function updateDevicePlugin(
store: Store,
plugin: DevicePluginDefinition,
plugin: PluginDefinition,
enable: boolean,
) {
if (enable) {
@@ -292,7 +284,6 @@ function updateDevicePlugin(
devicesWithEnabledPlugin.forEach((d) => {
d.unloadDevicePlugin(plugin.id);
});
store.dispatch(clearPluginState({pluginId: plugin.id}));
const previousVersion = store.getState().plugins.devicePlugins.get(plugin.id);
if (previousVersion) {
// unload previous version from Electron cache

View File

@@ -39,8 +39,9 @@ export {default as constants} from './fb-stubs/constants';
export {connect} from 'react-redux';
export {selectPlugin, StaticView} from './reducers/connections';
export {writeBufferToFile, bufferToBlob} from './utils/screenshot';
export {getPluginKey, getPersistedState} from './utils/pluginUtils';
export {Idler, Notification} from 'flipper-plugin';
export {getPluginKey} from './utils/pluginUtils';
export {Notification, Idler} from 'flipper-plugin';
export {IdlerImpl} from './utils/Idler';
export {Store, MiddlewareAPI, State as ReduxState} from './reducers/index';
export {default as BaseDevice} from './devices/BaseDevice';
export {DeviceLogEntry, LogLevel, DeviceLogListener} from 'flipper-plugin';

View File

@@ -27,18 +27,10 @@ import {
type Parameters = {[key: string]: any};
export type PluginDefinition = ClientPluginDefinition | DevicePluginDefinition;
export type PluginDefinition = _SandyPluginDefinition;
export type DevicePluginDefinition =
| typeof FlipperDevicePlugin
| _SandyPluginDefinition;
export type ClientPluginDefinition =
| typeof FlipperPlugin
| _SandyPluginDefinition;
export type ClientPluginMap = Map<string, ClientPluginDefinition>;
export type DevicePluginMap = Map<string, DevicePluginDefinition>;
export type ClientPluginMap = Map<string, PluginDefinition>;
export type DevicePluginMap = Map<string, PluginDefinition>;
// This function is intended to be called from outside of the plugin.
// If you want to `call` from the plugin use, this.client.call
@@ -211,6 +203,10 @@ export abstract class FlipperBasePlugin<
}
}
/**
* @deprecated Please use the newer "Sandy" plugin APIs!
* https://fbflipper.com/docs/extending/sandy-migration
*/
export class FlipperDevicePlugin<
S,
A extends BaseAction,
@@ -240,6 +236,10 @@ export class FlipperDevicePlugin<
}
}
/**
* @deprecated Please use the newer "Sandy" plugin APIs!
* https://fbflipper.com/docs/extending/sandy-migration
*/
export class FlipperPlugin<
S,
A extends BaseAction,

View File

@@ -13,7 +13,17 @@ import BaseDevice from '../../devices/BaseDevice';
import MacDevice from '../../devices/MacDevice';
import {FlipperDevicePlugin} from '../../plugin';
import MetroDevice from '../../devices/MetroDevice';
import {TestUtils} from 'flipper-plugin';
import {TestUtils, _setFlipperLibImplementation} from 'flipper-plugin';
import {wrapSandy} from '../../test-utils/createMockFlipperWithPlugin';
import {createMockFlipperLib} from 'flipper-plugin/src/test-utils/test-utils';
beforeEach(() => {
_setFlipperLibImplementation(createMockFlipperLib());
});
afterEach(() => {
_setFlipperLibImplementation(undefined);
});
test('doing a double REGISTER_DEVICE keeps the last', () => {
const device1 = new BaseDevice('serial', 'physical', 'title', 'Android');

View File

@@ -1,33 +0,0 @@
/**
* 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 {default as reducer, setPluginState, Action} from '../pluginStates';
test('reduce setPluginState', () => {
const result = reducer(
{},
setPluginState({pluginKey: 'myPlugin', state: {a: 1}}),
);
expect(result).toEqual({myPlugin: {a: 1}});
});
test('CLEAR_CLIENT_PLUGINS_STATE removes plugin state', () => {
const clientId = 'app1#device1';
const pluginKey = 'app1#device1#plugin1';
const action: Action = {
type: 'CLEAR_CLIENT_PLUGINS_STATE',
payload: {clientId: clientId, devicePlugins: new Set()},
};
const result = reducer(
{[pluginKey]: {a: 1}, 'anotherPlugin#key': {b: 2}},
action,
);
expect(result).toEqual({'anotherPlugin#key': {b: 2}});
});

View File

@@ -15,18 +15,21 @@ import {
} from '../plugins';
import {FlipperPlugin, FlipperDevicePlugin, BaseAction} from '../../plugin';
import {InstalledPluginDetails} from 'flipper-plugin-lib';
import {wrapSandy} from '../../test-utils/createMockFlipperWithPlugin';
const testPlugin = class extends FlipperPlugin<any, BaseAction, any> {
const testPluginOrig = class extends FlipperPlugin<any, BaseAction, any> {
static id = 'TestPlugin';
};
const testPlugin = wrapSandy(testPluginOrig);
const testDevicePlugin = class extends FlipperDevicePlugin<
const testDevicePluginOrig = class extends FlipperDevicePlugin<
any,
BaseAction,
any
> {
static id = 'TestDevicePlugin';
};
const testDevicePlugin = wrapSandy(testDevicePluginOrig);
test('add clientPlugin', () => {
const res = reducer(

View File

@@ -18,10 +18,6 @@ import connections, {
persistMigrations as devicesPersistMigrations,
persistVersion as devicesPersistVersion,
} from './connections';
import pluginStates, {
State as PluginStatesState,
Action as PluginStatesAction,
} from './pluginStates';
import pluginMessageQueue, {
State as PluginMessageQueueState,
Action as PluginMessageQueueAction,
@@ -81,7 +77,6 @@ import {TransformConfig} from 'redux-persist/es/createTransform';
export type Actions =
| ApplicationAction
| DevicesAction
| PluginStatesAction
| PluginMessageQueueAction
| NotificationsAction
| PluginsAction
@@ -98,7 +93,6 @@ export type Actions =
export type State = {
application: ApplicationState;
connections: DevicesState & PersistPartial;
pluginStates: PluginStatesState;
pluginMessageQueue: PluginMessageQueueState;
notifications: NotificationsState & PersistPartial;
plugins: PluginsState & PersistPartial;
@@ -158,7 +152,6 @@ export function createRootReducer() {
},
connections,
),
pluginStates,
pluginMessageQueue: pluginMessageQueue as any,
notifications: persistReducer(
{

View File

@@ -1,97 +0,0 @@
/**
* 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 {produce} from 'immer';
import {Actions} from '.';
import {deconstructPluginKey} from '../utils/clientUtils';
export type State = {
[pluginKey: string]: any;
};
export const pluginKey = (serial: string, pluginName: string): string => {
return `${serial}#${pluginName}`;
};
export type Action =
| {
type: 'SET_PLUGIN_STATE';
payload: {
pluginKey: string;
state: Object;
};
}
| {
type: 'CLEAR_CLIENT_PLUGINS_STATE';
payload: {clientId: string; devicePlugins: Set<string>};
}
| {
type: 'CLEAR_PLUGIN_STATE';
payload: {pluginId: string};
};
export default function reducer(
state: State | undefined = {},
action: Actions,
): State {
if (action.type === 'SET_PLUGIN_STATE') {
const newPluginState = action.payload.state;
if (newPluginState && newPluginState !== state[action.payload.pluginKey]) {
return {
...state,
[action.payload.pluginKey]: {
...state[action.payload.pluginKey],
...newPluginState,
},
};
}
return {...state};
} else if (action.type === 'CLEAR_CLIENT_PLUGINS_STATE') {
const {payload} = action;
return Object.keys(state).reduce((newState: State, pluginKey) => {
// Only add the pluginState, if its from a plugin other than the one that
// was removed. pluginKeys are in the form of ${clientID}#${pluginID}.
const plugin = deconstructPluginKey(pluginKey);
const clientId = plugin.client;
const pluginId = plugin.pluginName;
if (
clientId !== payload.clientId ||
(pluginId && payload.devicePlugins.has(pluginId))
) {
newState[pluginKey] = state[pluginKey];
}
return newState;
}, {});
} else if (action.type === 'CLEAR_PLUGIN_STATE') {
const {pluginId} = action.payload;
return produce(state, (draft) => {
Object.keys(draft).forEach((pluginKey) => {
const pluginKeyParts = deconstructPluginKey(pluginKey);
if (pluginKeyParts.pluginName === pluginId) {
delete draft[pluginKey];
}
});
});
} else {
return state;
}
}
export const setPluginState = (payload: {
pluginKey: string;
state: Object;
}): Action => ({
type: 'SET_PLUGIN_STATE',
payload,
});
export const clearPluginState = (payload: {pluginId: string}): Action => ({
type: 'CLEAR_PLUGIN_STATE',
payload,
});

View File

@@ -169,7 +169,7 @@ export function SandyApp() {
)}
</TrackingScope>
) : (
<PluginContainer logger={logger} isSandy />
<PluginContainer logger={logger} />
)}
{outOfContentsContainer}
</MainContainer>

View File

@@ -108,10 +108,9 @@ export const getPluginLists = createSelector(
);
export const getExportablePlugins = createSelector(
({plugins, connections, pluginStates, pluginMessageQueue}: State) => ({
({plugins, connections, pluginMessageQueue}: State) => ({
plugins,
connections,
pluginStates,
pluginMessageQueue,
}),
getActiveDevice,

View File

@@ -27,7 +27,7 @@ import {Store} from '../reducers/index';
import Client, {ClientQuery} from '../Client';
import {Logger} from '../fb-interfaces/Logger';
import {FlipperDevicePlugin, PluginDefinition} from '../plugin';
import {FlipperDevicePlugin, FlipperPlugin, PluginDefinition} from '../plugin';
import PluginContainer from '../PluginContainer';
import {getPluginKey, isDevicePluginDefinition} from '../utils/pluginUtils';
import MockFlipper from './MockFlipper';
@@ -66,13 +66,12 @@ type MockOptions = Partial<{
* the base implementation will be used
*/
onSend?: (pluginId: string, method: string, params?: object) => any;
additionalPlugins?: PluginDefinition[];
additionalPlugins?: (PluginDefinition | LegacyPluginDefinition)[];
dontEnableAdditionalPlugins?: true;
asBackgroundPlugin?: true;
supportedPlugins?: string[];
device?: BaseDevice;
archivedDevice?: boolean;
disableLegacyWrapper?: boolean;
}>;
function isPluginEnabled(
@@ -90,13 +89,14 @@ function isPluginEnabled(
);
}
export async function createMockFlipperWithPlugin(
pluginClazz: PluginDefinition,
options?: MockOptions,
): Promise<MockFlipperResult> {
function wrapSandy(clazz: PluginDefinition) {
return clazz instanceof _SandyPluginDefinition ||
options?.disableLegacyWrapper
export type LegacyPluginDefinition =
| typeof FlipperDevicePlugin
| typeof FlipperPlugin;
export function wrapSandy(
clazz: PluginDefinition | LegacyPluginDefinition,
): PluginDefinition {
return clazz instanceof _SandyPluginDefinition
? clazz
: createSandyPluginFromClassicPlugin(
createMockActivatablePluginDetails({
@@ -111,13 +111,16 @@ export async function createMockFlipperWithPlugin(
);
}
pluginClazz = wrapSandy(pluginClazz);
export async function createMockFlipperWithPlugin(
pluginClazzOrig: PluginDefinition | LegacyPluginDefinition,
options?: MockOptions,
): Promise<MockFlipperResult> {
const pluginClazz = wrapSandy(pluginClazzOrig);
const additionalPlugins = options?.additionalPlugins?.map(wrapSandy) ?? [];
const mockFlipper = new MockFlipper();
await mockFlipper.init({
plugins: [
pluginClazz,
...(options?.additionalPlugins?.map(wrapSandy) ?? []),
],
plugins: [pluginClazz, ...additionalPlugins],
});
const logger = mockFlipper.logger;
const store = mockFlipper.store;
@@ -150,7 +153,7 @@ export async function createMockFlipperWithPlugin(
}
if (!options?.dontEnableAdditionalPlugins) {
options?.additionalPlugins?.forEach((plugin) => {
additionalPlugins.forEach((plugin) => {
if (!isPluginEnabled(store, plugin, name)) {
store.dispatch(
switchPlugin({
@@ -246,7 +249,7 @@ export async function createMockFlipperWithPlugin(
type Renderer = RenderResult<typeof queries>;
export async function renderMockFlipperWithPlugin(
pluginClazz: PluginDefinition,
pluginClazzOrig: PluginDefinition | LegacyPluginDefinition,
options?: MockOptions,
): Promise<
MockFlipperResult & {
@@ -254,6 +257,7 @@ export async function renderMockFlipperWithPlugin(
act: (cb: () => void) => void;
}
> {
const pluginClazz = wrapSandy(pluginClazzOrig);
const args = await createMockFlipperWithPlugin(pluginClazz, options);
function selectTestPlugin(store: Store, client: Client) {

View File

@@ -20,7 +20,10 @@ import {
import {FlipperPlugin, FlipperDevicePlugin} from '../../plugin';
import {default as Client, ClientExport} from '../../Client';
import {selectedPlugins, State as PluginsState} from '../../reducers/plugins';
import {createMockFlipperWithPlugin} from '../../test-utils/createMockFlipperWithPlugin';
import {
createMockFlipperWithPlugin,
wrapSandy,
} from '../../test-utils/createMockFlipperWithPlugin';
import {
Notification,
TestUtils,
@@ -39,12 +42,16 @@ function testOnStatusMessage() {
// emtpy stub
}
class TestPlugin extends FlipperPlugin<any, any, any> {}
TestPlugin.title = 'TestPlugin';
TestPlugin.id = 'TestPlugin';
class TestDevicePlugin extends FlipperDevicePlugin<any, any, any> {}
TestDevicePlugin.title = 'TestDevicePlugin';
TestDevicePlugin.id = 'TestDevicePlugin';
class TestPluginOrig extends FlipperPlugin<any, any, any> {}
TestPluginOrig.title = 'TestPlugin';
TestPluginOrig.id = 'TestPlugin';
const TestPlugin = wrapSandy(TestPluginOrig);
class TestDevicePluginOrig extends FlipperDevicePlugin<any, any, any> {}
TestDevicePluginOrig.title = 'TestDevicePlugin';
TestDevicePluginOrig.id = 'TestDevicePlugin';
const TestDevicePlugin = wrapSandy(TestDevicePluginOrig);
const logger = {
track: () => {},
info: () => {},
@@ -193,7 +200,6 @@ test('test processStore function for empty state', async () => {
processStore({
activeNotifications: [],
device: null,
pluginStates: {},
clients: [],
devicePlugins: new Map(),
clientPlugins: new Map(),
@@ -216,7 +222,6 @@ test('test processStore function for an iOS device connected', async () => {
os: 'iOS',
screenshotHandle: null,
}),
pluginStates: {},
pluginStates2: {},
clients: [],
devicePlugins: new Map(),
@@ -238,8 +243,8 @@ test('test processStore function for an iOS device connected', async () => {
expect(deviceType).toEqual('emulator');
expect(title).toEqual('TestiPhone');
expect(os).toEqual('iOS');
const {pluginStates, activeNotifications} = json.store;
expect(pluginStates).toEqual({});
const {activeNotifications} = json.store;
expect(json.pluginStates2).toEqual({});
expect(activeNotifications).toEqual([]);
});
@@ -256,11 +261,11 @@ test('test processStore function for an iOS device connected with client plugin
const json = await processStore({
activeNotifications: [],
device,
pluginStates: {
[`${clientIdentifier}#TestPlugin`]: {msg: 'Test plugin'},
},
pluginStates2: {
[`${clientIdentifier}`]: {TestPlugin2: [{msg: 'Test plugin2'}]},
[clientIdentifier]: {
TestPlugin2: [{msg: 'Test plugin2'}],
TestPlugin: {msg: 'Test plugin'},
},
},
clients: [client],
devicePlugins: new Map(),
@@ -271,25 +276,16 @@ test('test processStore function for an iOS device connected with client plugin
if (!json) {
fail('json is undefined');
}
const {pluginStates} = json.store;
const expectedPluginState = {
[`${generateClientIdentifierWithSalt(
clientIdentifier,
'salt',
)}#TestPlugin`]: JSON.stringify({
msg: 'Test plugin',
}),
};
const expectedPluginState2 = {
[`${generateClientIdentifierWithSalt(clientIdentifier, 'salt')}`]: {
[generateClientIdentifierWithSalt(clientIdentifier, 'salt')]: {
TestPlugin2: [
{
msg: 'Test plugin2',
},
],
TestPlugin: {msg: 'Test plugin'},
},
};
expect(pluginStates).toEqual(expectedPluginState);
expect(json.pluginStates2).toEqual(expectedPluginState2);
});
@@ -324,15 +320,18 @@ test('test processStore function to have only the client for the selected device
const json = await processStore({
activeNotifications: [],
device: selectedDevice,
pluginStates: {
[unselectedDeviceClientIdentifier + '#TestDevicePlugin']: {
pluginStates2: {
[unselectedDeviceClientIdentifier]: {
TestDevicePlugin: {
msg: 'Test plugin unselected device',
},
[selectedDeviceClientIdentifier + '#TestDevicePlugin']: {
},
[selectedDeviceClientIdentifier]: {
TestDevicePlugin: {
msg: 'Test plugin selected device',
},
},
pluginStates2: {},
},
clients: [
selectedDeviceClient,
generateClientFromDevice(unselectedDevice, 'testapp'),
@@ -347,17 +346,18 @@ test('test processStore function to have only the client for the selected device
fail('json is undefined');
}
const {clients} = json;
const {pluginStates} = json.store;
const expectedPluginState = {
[generateClientIdentifierWithSalt(selectedDeviceClientIdentifier, 'salt') +
'#TestDevicePlugin']: JSON.stringify({
[generateClientIdentifierWithSalt(selectedDeviceClientIdentifier, 'salt')]:
{
TestDevicePlugin: {
msg: 'Test plugin selected device',
}),
},
},
};
expect(clients).toEqual([
generateClientFromClientWithSalt(selectedDeviceClient, 'salt'),
]);
expect(pluginStates).toEqual(expectedPluginState);
expect(json.pluginStates2).toEqual(expectedPluginState);
});
test('test processStore function to have multiple clients for the selected device', async () => {
@@ -384,15 +384,18 @@ test('test processStore function to have multiple clients for the selected devic
const json = await processStore({
activeNotifications: [],
device: selectedDevice,
pluginStates: {
[clientIdentifierApp1 + '#TestPlugin']: {
pluginStates2: {
[clientIdentifierApp1]: {
TestPlugin: {
msg: 'Test plugin App1',
},
[clientIdentifierApp2 + '#TestPlugin']: {
},
[clientIdentifierApp2]: {
TestPlugin: {
msg: 'Test plugin App2',
},
},
pluginStates2: {},
},
clients: [
generateClientFromDevice(selectedDevice, 'testapp1'),
generateClientFromDevice(selectedDevice, 'testapp2'),
@@ -407,22 +410,23 @@ test('test processStore function to have multiple clients for the selected devic
fail('json is undefined');
}
const {clients} = json;
const {pluginStates} = json.store;
const expectedPluginState = {
[generateClientIdentifierWithSalt(clientIdentifierApp1, 'salt') +
'#TestPlugin']: JSON.stringify({
[generateClientIdentifierWithSalt(clientIdentifierApp1, 'salt')]: {
TestPlugin: {
msg: 'Test plugin App1',
}),
[generateClientIdentifierWithSalt(clientIdentifierApp2, 'salt') +
'#TestPlugin']: JSON.stringify({
},
},
[generateClientIdentifierWithSalt(clientIdentifierApp2, 'salt')]: {
TestPlugin: {
msg: 'Test plugin App2',
}),
},
},
};
expect(clients).toEqual([
generateClientFromClientWithSalt(client1, 'salt'),
generateClientFromClientWithSalt(client2, 'salt'),
]);
expect(pluginStates).toEqual(expectedPluginState);
expect(json.pluginStates2).toEqual(expectedPluginState);
});
test('test processStore function for device plugin state and no clients', async () => {
@@ -437,12 +441,13 @@ test('test processStore function for device plugin state and no clients', async
const json = await processStore({
activeNotifications: [],
device: selectedDevice,
pluginStates: {
'serial#TestDevicePlugin': {
pluginStates2: {
serial: {
TestDevicePlugin: {
msg: 'Test Device plugin',
},
},
pluginStates2: {},
},
clients: [],
devicePlugins: new Map([['TestDevicePlugin', TestDevicePlugin]]),
clientPlugins: new Map(),
@@ -453,12 +458,11 @@ test('test processStore function for device plugin state and no clients', async
if (!json) {
fail('json is undefined');
}
const {pluginStates} = json.store;
const {clients} = json;
const expectedPluginState = {
'salt-serial#TestDevicePlugin': JSON.stringify({msg: 'Test Device plugin'}),
'salt-serial': {TestDevicePlugin: {msg: 'Test Device plugin'}},
};
expect(pluginStates).toEqual(expectedPluginState);
expect(json.pluginStates2).toEqual(expectedPluginState);
expect(clients).toEqual([]);
});
@@ -474,12 +478,13 @@ test('test processStore function for unselected device plugin state and no clien
const json = await processStore({
activeNotifications: [],
device: selectedDevice,
pluginStates: {
'unselectedDeviceIdentifier#TestDevicePlugin': {
pluginStates2: {
unselectedDeviceIdentifier: {
TestDevicePlugin: {
msg: 'Test Device plugin',
},
},
pluginStates2: {},
},
clients: [],
devicePlugins: new Map([['TestDevicePlugin', TestDevicePlugin]]),
clientPlugins: new Map(),
@@ -489,9 +494,8 @@ test('test processStore function for unselected device plugin state and no clien
if (!json) {
fail('json is undefined');
}
const {pluginStates} = json.store;
const {clients} = json;
expect(pluginStates).toEqual({});
expect(json.pluginStates2).toEqual({});
expect(clients).toEqual([]);
});
@@ -519,7 +523,6 @@ test('test processStore function for notifications for selected device', async (
const json = await processStore({
activeNotifications: [activeNotification],
device: selectedDevice,
pluginStates: {},
pluginStates2: {},
clients: [client],
devicePlugins: new Map([['TestDevicePlugin', TestDevicePlugin]]),
@@ -531,9 +534,8 @@ test('test processStore function for notifications for selected device', async (
if (!json) {
fail('json is undefined');
}
const {pluginStates} = json.store;
const {clients} = json;
expect(pluginStates).toEqual({});
expect(json.pluginStates2).toEqual({});
expect(clients).toEqual([generateClientFromClientWithSalt(client, 'salt')]);
const {activeNotifications} = json.store;
const expectedActiveNotification = {
@@ -580,7 +582,6 @@ test('test processStore function for notifications for unselected device', async
const json = await processStore({
activeNotifications: [activeNotification],
device: selectedDevice,
pluginStates: {},
pluginStates2: {},
clients: [client, unselectedclient],
devicePlugins: new Map(),
@@ -591,9 +592,8 @@ test('test processStore function for notifications for unselected device', async
if (!json) {
fail('json is undefined');
}
const {pluginStates} = json.store;
const {clients} = json;
expect(pluginStates).toEqual({});
expect(json.pluginStates2).toEqual({});
expect(clients).toEqual([generateClientFromClientWithSalt(client, 'salt')]);
const {activeNotifications} = json.store;
expect(activeNotifications).toEqual([]);
@@ -610,18 +610,21 @@ test('test processStore function for selected plugins', async () => {
const client = generateClientFromDevice(selectedDevice, 'app');
const pluginstates = {
[generateClientIdentifier(selectedDevice, 'app') + '#TestDevicePlugin1']: {
[generateClientIdentifier(selectedDevice, 'app')]: {
TestDevicePlugin1: {
msg: 'Test plugin1',
},
[generateClientIdentifier(selectedDevice, 'app') + '#TestDevicePlugin2']: {
},
[generateClientIdentifier(selectedDevice, 'app')]: {
TestDevicePlugin2: {
msg: 'Test plugin2',
},
},
};
const json = await processStore({
activeNotifications: [],
device: selectedDevice,
pluginStates: pluginstates,
pluginStates2: {},
pluginStates2: pluginstates as any,
clients: [client],
devicePlugins: new Map([
['TestDevicePlugin1', TestDevicePlugin],
@@ -634,15 +637,16 @@ test('test processStore function for selected plugins', async () => {
if (!json) {
fail('json is undefined');
}
const {pluginStates} = json.store;
const {clients} = json;
expect(pluginStates).toEqual({
expect(json.pluginStates2).toEqual({
[generateClientIdentifierWithSalt(
generateClientIdentifier(selectedDevice, 'app'),
'salt',
) + '#TestDevicePlugin2']: JSON.stringify({
)]: {
TestDevicePlugin2: {
msg: 'Test plugin2',
}),
},
},
});
expect(clients).toEqual([generateClientFromClientWithSalt(client, 'salt')]);
const {activeNotifications} = json.store;
@@ -659,18 +663,19 @@ test('test processStore function for no selected plugins', async () => {
});
const client = generateClientFromDevice(selectedDevice, 'app');
const pluginstates = {
[generateClientIdentifier(selectedDevice, 'app') + '#TestDevicePlugin1']: {
[generateClientIdentifier(selectedDevice, 'app')]: {
TestDevicePlugin1: {
msg: 'Test plugin1',
},
[generateClientIdentifier(selectedDevice, 'app') + '#TestDevicePlugin2']: {
TestDevicePlugin2: {
msg: 'Test plugin2',
},
},
};
const json = await processStore({
activeNotifications: [],
device: selectedDevice,
pluginStates: pluginstates,
pluginStates2: {},
pluginStates2: pluginstates as any,
clients: [client],
devicePlugins: new Map([
['TestDevicePlugin1', TestDevicePlugin],
@@ -684,21 +689,20 @@ test('test processStore function for no selected plugins', async () => {
if (!json) {
fail('json is undefined');
}
const {pluginStates} = json.store;
const {clients} = json;
expect(pluginStates).toEqual({
expect(json.pluginStates2).toEqual({
[generateClientIdentifierWithSalt(
generateClientIdentifier(selectedDevice, 'app'),
'salt',
) + '#TestDevicePlugin2']: JSON.stringify({
)]: {
TestDevicePlugin2: {
msg: 'Test plugin2',
}),
[generateClientIdentifierWithSalt(
generateClientIdentifier(selectedDevice, 'app'),
'salt',
) + '#TestDevicePlugin1']: JSON.stringify({
},
TestDevicePlugin1: {
msg: 'Test plugin1',
}),
},
},
});
expect(clients).toEqual([generateClientFromClientWithSalt(client, 'salt')]);
const {activeNotifications} = json.store;
@@ -781,14 +785,12 @@ test('test determinePluginsToProcess for mutilple clients having plugins present
pluginKey: `${client1.id}#TestPlugin`,
pluginId: 'TestPlugin',
pluginName: 'TestPlugin',
pluginClass: TestPlugin,
client: client1,
},
{
pluginKey: `${client3.id}#TestPlugin`,
pluginId: 'TestPlugin',
pluginName: 'TestPlugin',
pluginClass: TestPlugin,
client: client3,
},
]);
@@ -905,7 +907,6 @@ test('test determinePluginsToProcess for multiple clients on same device', async
pluginKey: `${client1.id}#TestPlugin`,
pluginId: 'TestPlugin',
pluginName: 'TestPlugin',
pluginClass: TestPlugin,
client: client1,
},
]);
@@ -999,7 +1000,6 @@ test('test determinePluginsToProcess for multiple clients on different device',
pluginKey: `${client1Device2.id}#TestPlugin`,
pluginId: 'TestPlugin',
pluginName: 'TestPlugin',
pluginClass: TestPlugin,
client: client1Device2,
},
]);
@@ -1085,7 +1085,6 @@ test('test determinePluginsToProcess to ignore archived clients', async () => {
pluginKey: `${client.id}#TestPlugin`,
pluginId: 'TestPlugin',
pluginName: 'TestPlugin',
pluginClass: TestPlugin,
client: client,
},
]);

View File

@@ -1,684 +0,0 @@
/**
* 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 {FlipperPlugin, FlipperDevicePlugin} from '../../plugin';
import {createMockFlipperWithPlugin} from '../../test-utils/createMockFlipperWithPlugin';
import {Store, Client, sleep} from '../../';
import {
selectPlugin,
selectClient,
selectDevice,
} from '../../reducers/connections';
import {processMessageQueue} from '../messageQueue';
import {getPluginKey} from '../pluginUtils';
import {TestIdler} from '../Idler';
import pluginMessageQueue, {
State,
queueMessages,
} from '../../reducers/pluginMessageQueue';
import {registerPlugins} from '../../reducers/plugins';
import {switchPlugin} from '../../reducers/pluginManager';
interface PersistedState {
count: 1;
}
class TestPlugin extends FlipperPlugin<any, any, any> {
static id = 'TestPlugin';
static defaultPersistedState = {
count: 0,
};
static persistedStateReducer(
persistedState: PersistedState,
method: string,
payload: {delta?: number},
) {
if (method === 'inc') {
return Object.assign({}, persistedState, {
count: persistedState.count + ((payload && payload?.delta) || 1),
});
}
return persistedState;
}
render() {
return null;
}
}
function switchTestPlugin(store: Store, client: Client) {
store.dispatch(
switchPlugin({
plugin: TestPlugin,
selectedApp: client.query.app,
}),
);
}
function selectDeviceLogs(store: Store) {
store.dispatch(
selectPlugin({
selectedPlugin: 'DeviceLogs',
selectedApp: null,
deepLinkPayload: null,
selectedDevice: store.getState().connections.selectedDevice!,
}),
);
}
function selectTestPlugin(store: Store, client: Client) {
store.dispatch(
selectPlugin({
selectedPlugin: TestPlugin.id,
selectedApp: client.query.app,
deepLinkPayload: null,
selectedDevice: store.getState().connections.selectedDevice!,
}),
);
}
test('queue - events are processed immediately if plugin is selected', async () => {
const {store, client, sendMessage} = await createMockFlipperWithPlugin(
TestPlugin,
{
disableLegacyWrapper: true, // Sandy is already tested in messageQueueSandy.node.tsx
},
);
expect(store.getState().connections.selectedPlugin).toBe('TestPlugin');
sendMessage('noop', {});
sendMessage('noop', {});
sendMessage('inc', {});
sendMessage('inc', {delta: 4});
sendMessage('noop', {});
client.flushMessageBuffer();
expect(store.getState().pluginStates).toMatchInlineSnapshot(`
Object {
"TestApp#Android#MockAndroidDevice#serial#TestPlugin": Object {
"count": 5,
},
}
`);
expect(store.getState().pluginMessageQueue).toMatchInlineSnapshot(
`Object {}`,
);
});
test('queue - events are NOT processed immediately if plugin is NOT selected (but enabled)', async () => {
const {store, client, sendMessage, device} =
await createMockFlipperWithPlugin(TestPlugin, {
disableLegacyWrapper: true, // Sandy is already tested in messageQueueSandy.node.tsx
});
selectDeviceLogs(store);
expect(store.getState().connections.selectedPlugin).not.toBe('TestPlugin');
sendMessage('inc', {});
sendMessage('inc', {delta: 2});
sendMessage('inc', {delta: 3});
expect(store.getState().pluginStates).toMatchInlineSnapshot(`Object {}`);
// the first message is already visible cause of the leading debounce
expect(store.getState().pluginMessageQueue).toMatchInlineSnapshot(`
Object {
"TestApp#Android#MockAndroidDevice#serial#TestPlugin": Array [
Object {
"api": "TestPlugin",
"method": "inc",
"params": Object {},
},
],
}
`);
client.flushMessageBuffer();
expect(store.getState().pluginMessageQueue).toMatchInlineSnapshot(`
Object {
"TestApp#Android#MockAndroidDevice#serial#TestPlugin": Array [
Object {
"api": "TestPlugin",
"method": "inc",
"params": Object {},
},
Object {
"api": "TestPlugin",
"method": "inc",
"params": Object {
"delta": 2,
},
},
Object {
"api": "TestPlugin",
"method": "inc",
"params": Object {
"delta": 3,
},
},
],
}
`);
// process the message
const pluginKey = getPluginKey(client.id, device, TestPlugin.id);
await processMessageQueue(TestPlugin, pluginKey, store);
expect(store.getState().pluginStates).toEqual({
[pluginKey]: {
count: 6,
},
});
expect(store.getState().pluginMessageQueue).toEqual({
[pluginKey]: [],
});
// disable, but, messages still arrives because selected
switchTestPlugin(store, client);
selectTestPlugin(store, client);
sendMessage('inc', {delta: 3});
client.flushMessageBuffer();
// active, immediately processed
expect(store.getState().pluginStates).toEqual({
[pluginKey]: {
count: 9,
},
});
// different plugin, and not enabled, message will never arrive
selectDeviceLogs(store);
sendMessage('inc', {delta: 4});
client.flushMessageBuffer();
expect(store.getState().pluginMessageQueue).toEqual({});
// star again, plugin still not selected, message is queued
switchTestPlugin(store, client);
sendMessage('inc', {delta: 5});
client.flushMessageBuffer();
expect(store.getState().pluginMessageQueue).toEqual({
[pluginKey]: [{api: 'TestPlugin', method: 'inc', params: {delta: 5}}],
});
});
test('queue - events are queued for plugins that are favorite when app is not selected', async () => {
const {device, store, sendMessage, createClient} =
await createMockFlipperWithPlugin(TestPlugin, {
disableLegacyWrapper: true, // Sandy is already tested in messageQueueSandy.node.tsx
});
selectDeviceLogs(store);
expect(store.getState().connections.selectedPlugin).not.toBe('TestPlugin');
const client2 = await createClient(device, 'TestApp2');
store.dispatch(selectClient(client2.id));
// Now we send a message to the second client, it should arrive,
// as the plugin was enabled already on the first client as well
sendMessage('inc', {delta: 2});
expect(store.getState().pluginStates).toMatchInlineSnapshot(`Object {}`);
expect(store.getState().pluginMessageQueue).toMatchInlineSnapshot(`
Object {
"TestApp#Android#MockAndroidDevice#serial#TestPlugin": Array [
Object {
"api": "TestPlugin",
"method": "inc",
"params": Object {
"delta": 2,
},
},
],
}
`);
});
test('queue - events are queued for plugins that are favorite when app is selected on different device', async () => {
const {client, store, sendMessage, createDevice, createClient} =
await createMockFlipperWithPlugin(TestPlugin, {
disableLegacyWrapper: true, // Sandy is already tested in messageQueueSandy.node.tsx
});
selectDeviceLogs(store);
expect(store.getState().connections.selectedPlugin).not.toBe('TestPlugin');
const device2 = createDevice('serial2');
const client2 = await createClient(device2, client.query.app); // same app id
store.dispatch(selectDevice(device2));
store.dispatch(selectClient(client2.id));
// Now we send a message to the first and second client, it should arrive,
// as the plugin was enabled already on the first client as well
sendMessage('inc', {delta: 2});
sendMessage('inc', {delta: 3}, client2);
client.flushMessageBuffer();
client2.flushMessageBuffer();
expect(store.getState().pluginStates).toMatchInlineSnapshot(`Object {}`);
expect(store.getState().pluginMessageQueue).toMatchInlineSnapshot(`
Object {
"TestApp#Android#MockAndroidDevice#serial#TestPlugin": Array [
Object {
"api": "TestPlugin",
"method": "inc",
"params": Object {
"delta": 2,
},
},
],
"TestApp#Android#MockAndroidDevice#serial2#TestPlugin": Array [
Object {
"api": "TestPlugin",
"method": "inc",
"params": Object {
"delta": 3,
},
},
],
}
`);
});
test('queue - events processing will be paused', async () => {
const {client, device, store, sendMessage} =
await createMockFlipperWithPlugin(TestPlugin, {
disableLegacyWrapper: true, // Sandy is already tested in messageQueueSandy.node.tsx
});
selectDeviceLogs(store);
sendMessage('inc', {});
sendMessage('inc', {delta: 3});
sendMessage('inc', {delta: 5});
client.flushMessageBuffer();
// process the message
const pluginKey = getPluginKey(client.id, device, TestPlugin.id);
// controlled idler will signal and and off that idling is needed
const idler = new TestIdler();
const p = processMessageQueue(TestPlugin, pluginKey, store, undefined, idler);
expect(store.getState().pluginStates).toEqual({
[pluginKey]: {
count: 4,
},
});
expect(store.getState().pluginMessageQueue).toEqual({
[pluginKey]: [{api: 'TestPlugin', method: 'inc', params: {delta: 5}}],
});
await idler.next();
expect(store.getState().pluginStates).toEqual({
[pluginKey]: {
count: 9,
},
});
expect(store.getState().pluginMessageQueue).toEqual({
[pluginKey]: [],
});
// don't idle anymore
idler.run();
await p;
});
test('queue - messages that arrive during processing will be queued', async () => {
const {client, device, store, sendMessage} =
await createMockFlipperWithPlugin(TestPlugin, {
disableLegacyWrapper: true, // Sandy is already tested in messageQueueSandy.node.tsx
});
selectDeviceLogs(store);
sendMessage('inc', {});
sendMessage('inc', {delta: 2});
sendMessage('inc', {delta: 3});
client.flushMessageBuffer();
// process the message
const pluginKey = getPluginKey(client.id, device, TestPlugin.id);
const idler = new TestIdler();
const p = processMessageQueue(TestPlugin, pluginKey, store, undefined, idler);
// first message is consumed
expect(store.getState().pluginMessageQueue[pluginKey].length).toBe(1);
expect(store.getState().pluginStates[pluginKey].count).toBe(3);
// Select the current plugin as active, still, messages should end up in the queue
store.dispatch(
selectPlugin({
selectedPlugin: TestPlugin.id,
selectedApp: client.id,
deepLinkPayload: null,
selectedDevice: device,
}),
);
expect(store.getState().connections.selectedPlugin).toBe('TestPlugin');
sendMessage('inc', {delta: 4});
client.flushMessageBuffer();
// should not be processed yet
expect(store.getState().pluginMessageQueue[pluginKey].length).toBe(2);
expect(store.getState().pluginStates[pluginKey].count).toBe(3);
await idler.next();
expect(store.getState().pluginMessageQueue[pluginKey].length).toBe(0);
expect(store.getState().pluginStates[pluginKey].count).toBe(10);
idler.run();
await p;
});
test('queue - processing can be cancelled', async () => {
const {client, device, store, sendMessage} =
await createMockFlipperWithPlugin(TestPlugin, {
disableLegacyWrapper: true, // Sandy is already tested in messageQueueSandy.node.tsx
});
selectDeviceLogs(store);
sendMessage('inc', {});
sendMessage('inc', {delta: 2});
sendMessage('inc', {delta: 3});
sendMessage('inc', {delta: 4});
sendMessage('inc', {delta: 5});
client.flushMessageBuffer();
// process the message
const pluginKey = getPluginKey(client.id, device, TestPlugin.id);
const idler = new TestIdler();
const p = processMessageQueue(TestPlugin, pluginKey, store, undefined, idler);
// first message is consumed
await idler.next();
expect(store.getState().pluginMessageQueue[pluginKey].length).toBe(1);
expect(store.getState().pluginStates[pluginKey].count).toBe(10);
idler.cancel();
expect(store.getState().pluginMessageQueue[pluginKey].length).toBe(1);
expect(store.getState().pluginStates[pluginKey].count).toBe(10);
await p;
});
test('queue - make sure resetting plugin state clears the message queue', async () => {
const {client, device, store, sendMessage} =
await createMockFlipperWithPlugin(TestPlugin, {
disableLegacyWrapper: true, // Sandy is already tested in messageQueueSandy.node.tsx
});
selectDeviceLogs(store);
sendMessage('inc', {});
sendMessage('inc', {delta: 2});
client.flushMessageBuffer();
const pluginKey = getPluginKey(client.id, device, TestPlugin.id);
expect(store.getState().pluginMessageQueue[pluginKey].length).toBe(2);
store.dispatch({
type: 'CLEAR_CLIENT_PLUGINS_STATE',
payload: {clientId: client.id, devicePlugins: new Set()},
});
expect(store.getState().pluginMessageQueue[pluginKey]).toBe(undefined);
});
test('queue will be cleaned up when it exceeds maximum size', () => {
let state: State = {};
const pluginKey = 'test';
const queueSize = 5000;
let i = 0;
for (i = 0; i < queueSize; i++) {
state = pluginMessageQueue(
state,
queueMessages(pluginKey, [{method: 'test', params: {i}}], queueSize),
);
}
// almost full
expect(state[pluginKey][0]).toEqual({method: 'test', params: {i: 0}});
expect(state[pluginKey].length).toBe(queueSize); // ~5000
expect(state[pluginKey][queueSize - 1]).toEqual({
method: 'test',
params: {i: queueSize - 1}, // ~4999
});
state = pluginMessageQueue(
state,
queueMessages(pluginKey, [{method: 'test', params: {i: ++i}}], queueSize),
);
const newLength = Math.ceil(0.9 * queueSize) + 1; // ~4500
expect(state[pluginKey].length).toBe(newLength);
expect(state[pluginKey][0]).toEqual({
method: 'test',
params: {i: queueSize - newLength + 1}, // ~500
});
expect(state[pluginKey][newLength - 1]).toEqual({
method: 'test',
params: {i: i}, // ~50001
});
});
test('client - incoming messages are buffered and flushed together', async () => {
class StubDeviceLogs extends FlipperDevicePlugin<any, any, any> {
static id = 'DevicePlugin';
static supportsDevice() {
return true;
}
static persistedStateReducer = jest.fn();
}
const {client, store, device, sendMessage} =
await createMockFlipperWithPlugin(TestPlugin, {
disableLegacyWrapper: true, // Sandy is already tested in messageQueueSandy.node.tsx
});
selectDeviceLogs(store);
store.dispatch(registerPlugins([StubDeviceLogs]));
sendMessage('inc', {});
sendMessage('inc', {delta: 2});
sendMessage('inc', {delta: 3});
// send a message to device logs
client.onMessage(
JSON.stringify({
method: 'execute',
params: {
api: 'DevicePlugin',
method: 'log',
params: {line: 'suff'},
},
}),
);
expect(store.getState().pluginStates).toMatchInlineSnapshot(`Object {}`);
// the first message is already visible cause of the leading debounce
expect(store.getState().pluginMessageQueue).toMatchInlineSnapshot(`
Object {
"TestApp#Android#MockAndroidDevice#serial#TestPlugin": Array [
Object {
"api": "TestPlugin",
"method": "inc",
"params": Object {},
},
],
}
`);
expect(client.messageBuffer).toMatchInlineSnapshot(`
Object {
"TestApp#Android#MockAndroidDevice#serial#DevicePlugin": Object {
"messages": Array [
Object {
"api": "DevicePlugin",
"method": "log",
"params": Object {
"line": "suff",
},
},
],
"plugin": [Function],
},
"TestApp#Android#MockAndroidDevice#serial#TestPlugin": Object {
"messages": Array [
Object {
"api": "TestPlugin",
"method": "inc",
"params": Object {
"delta": 2,
},
},
Object {
"api": "TestPlugin",
"method": "inc",
"params": Object {
"delta": 3,
},
},
],
"plugin": [Function],
},
}
`);
await sleep(500);
expect(store.getState().pluginMessageQueue).toMatchInlineSnapshot(`
Object {
"TestApp#Android#MockAndroidDevice#serial#DevicePlugin": Array [
Object {
"api": "DevicePlugin",
"method": "log",
"params": Object {
"line": "suff",
},
},
],
"TestApp#Android#MockAndroidDevice#serial#TestPlugin": Array [
Object {
"api": "TestPlugin",
"method": "inc",
"params": Object {},
},
Object {
"api": "TestPlugin",
"method": "inc",
"params": Object {
"delta": 2,
},
},
Object {
"api": "TestPlugin",
"method": "inc",
"params": Object {
"delta": 3,
},
},
],
}
`);
expect(client.messageBuffer).toMatchInlineSnapshot(`Object {}`);
expect(StubDeviceLogs.persistedStateReducer.mock.calls).toMatchInlineSnapshot(
`Array []`,
);
// tigger processing the queue
const pluginKey = getPluginKey(client.id, device, StubDeviceLogs.id);
await processMessageQueue(StubDeviceLogs, pluginKey, store);
expect(StubDeviceLogs.persistedStateReducer.mock.calls)
.toMatchInlineSnapshot(`
Array [
Array [
Object {},
"log",
Object {
"line": "suff",
},
],
]
`);
expect(store.getState().pluginMessageQueue).toMatchInlineSnapshot(`
Object {
"TestApp#Android#MockAndroidDevice#serial#DevicePlugin": Array [],
"TestApp#Android#MockAndroidDevice#serial#TestPlugin": Array [
Object {
"api": "TestPlugin",
"method": "inc",
"params": Object {},
},
Object {
"api": "TestPlugin",
"method": "inc",
"params": Object {
"delta": 2,
},
},
Object {
"api": "TestPlugin",
"method": "inc",
"params": Object {
"delta": 3,
},
},
],
}
`);
});
test('queue - messages that have not yet flushed be lost when disabling the plugin', async () => {
const {client, store, sendMessage, pluginKey} =
await createMockFlipperWithPlugin(TestPlugin, {
disableLegacyWrapper: true, // Sandy is already tested in messageQueueSandy.node.tsx
});
selectDeviceLogs(store);
sendMessage('inc', {});
sendMessage('inc', {delta: 2});
expect(client.messageBuffer).toMatchInlineSnapshot(`
Object {
"TestApp#Android#MockAndroidDevice#serial#TestPlugin": Object {
"messages": Array [
Object {
"api": "TestPlugin",
"method": "inc",
"params": Object {
"delta": 2,
},
},
],
"plugin": [Function],
},
}
`);
expect(store.getState().pluginMessageQueue).toMatchInlineSnapshot(`
Object {
"TestApp#Android#MockAndroidDevice#serial#TestPlugin": Array [
Object {
"api": "TestPlugin",
"method": "inc",
"params": Object {},
},
],
}
`);
// disable
switchTestPlugin(store, client);
expect(client.messageBuffer).toMatchInlineSnapshot(`Object {}`);
expect(store.getState().pluginMessageQueue).toMatchInlineSnapshot(
`Object {}`,
);
// re-enable, no messages arrive
switchTestPlugin(store, client);
client.flushMessageBuffer();
processMessageQueue(TestPlugin, pluginKey, store);
expect(store.getState().pluginStates).toMatchInlineSnapshot(`Object {}`);
});

View File

@@ -7,8 +7,11 @@
* @format
*/
import {FlipperDevicePlugin} from '../../plugin';
import {createMockFlipperWithPlugin} from '../../test-utils/createMockFlipperWithPlugin';
import {FlipperPlugin} from '../../plugin';
import {
createMockFlipperWithPlugin,
wrapSandy,
} from '../../test-utils/createMockFlipperWithPlugin';
import {Store, Client, sleep} from '../../';
import {
selectPlugin,
@@ -26,6 +29,10 @@ import {
_SandyPluginInstance,
} from 'flipper-plugin';
import {switchPlugin} from '../../reducers/pluginManager';
import pluginMessageQueue, {
State,
queueMessages,
} from '../../reducers/pluginMessageQueue';
type Events = {
inc: {
@@ -124,7 +131,6 @@ test('queue - events are NOT processed immediately if plugin is NOT selected (bu
sendMessage('inc', {});
sendMessage('inc', {delta: 2});
sendMessage('inc', {delta: 3});
expect(store.getState().pluginStates).toMatchInlineSnapshot(`Object {}`);
expect(getTestPluginState(client).count).toBe(0);
// the first message is already visible cause of the leading debounce
expect(store.getState().pluginMessageQueue).toMatchInlineSnapshot(`
@@ -482,21 +488,21 @@ test('queue - make sure resetting plugin state clears the message queue', async
});
test('client - incoming messages are buffered and flushed together', async () => {
class StubDeviceLogs extends FlipperDevicePlugin<any, any, any> {
static id = 'DevicePlugin';
static supportsDevice() {
return true;
}
class StubPlugin extends FlipperPlugin<any, any, any> {
static id = 'StubPlugin';
static persistedStateReducer = jest.fn();
}
const StubPluginWrapped = wrapSandy(StubPlugin);
const {client, store, device, sendMessage, pluginKey} =
await createMockFlipperWithPlugin(TestPlugin);
await createMockFlipperWithPlugin(TestPlugin, {
additionalPlugins: [StubPluginWrapped],
});
selectDeviceLogs(store);
store.dispatch(registerPlugins([StubDeviceLogs]));
store.dispatch(registerPlugins([StubPluginWrapped]));
sendMessage('inc', {});
sendMessage('inc', {delta: 2});
sendMessage('inc', {delta: 3});
@@ -506,14 +512,13 @@ test('client - incoming messages are buffered and flushed together', async () =>
JSON.stringify({
method: 'execute',
params: {
api: 'DevicePlugin',
api: 'StubPlugin',
method: 'log',
params: {line: 'suff'},
},
}),
);
expect(store.getState().pluginStates).toMatchInlineSnapshot(`Object {}`);
expect(getTestPluginState(client).count).toBe(0);
// the first message is already visible cause of the leading debounce
expect(store.getState().pluginMessageQueue).toMatchInlineSnapshot(`
@@ -529,17 +534,17 @@ test('client - incoming messages are buffered and flushed together', async () =>
`);
expect(client.messageBuffer).toMatchInlineSnapshot(`
Object {
"TestApp#Android#MockAndroidDevice#serial#DevicePlugin": Object {
"TestApp#Android#MockAndroidDevice#serial#StubPlugin": Object {
"messages": Array [
Object {
"api": "DevicePlugin",
"api": "StubPlugin",
"method": "log",
"params": Object {
"line": "suff",
},
},
],
"plugin": [Function],
"plugin": "[SandyPluginInstance]",
},
"TestApp#Android#MockAndroidDevice#serial#TestPlugin": Object {
"messages": Array [
@@ -569,9 +574,9 @@ test('client - incoming messages are buffered and flushed together', async () =>
await sleep(500);
expect(store.getState().pluginMessageQueue).toMatchInlineSnapshot(`
Object {
"TestApp#Android#MockAndroidDevice#serial#DevicePlugin": Array [
"TestApp#Android#MockAndroidDevice#serial#StubPlugin": Array [
Object {
"api": "DevicePlugin",
"api": "StubPlugin",
"method": "log",
"params": Object {
"line": "suff",
@@ -602,19 +607,22 @@ test('client - incoming messages are buffered and flushed together', async () =>
}
`);
expect(client.messageBuffer).toMatchInlineSnapshot(`Object {}`);
expect(StubDeviceLogs.persistedStateReducer.mock.calls).toMatchInlineSnapshot(
expect(StubPlugin.persistedStateReducer.mock.calls).toMatchInlineSnapshot(
`Array []`,
);
// tigger processing the queue
const pluginKeyDevice = getPluginKey(client.id, device, StubDeviceLogs.id);
await processMessageQueue(StubDeviceLogs, pluginKeyDevice, store);
const pluginKeyDevice = getPluginKey(client.id, device, StubPlugin.id);
await processMessageQueue(
client.sandyPluginStates.get(StubPlugin.id)!,
pluginKeyDevice,
store,
);
expect(StubDeviceLogs.persistedStateReducer.mock.calls)
.toMatchInlineSnapshot(`
expect(StubPlugin.persistedStateReducer.mock.calls).toMatchInlineSnapshot(`
Array [
Array [
Object {},
undefined,
"log",
Object {
"line": "suff",
@@ -625,7 +633,7 @@ test('client - incoming messages are buffered and flushed together', async () =>
expect(store.getState().pluginMessageQueue).toMatchInlineSnapshot(`
Object {
"TestApp#Android#MockAndroidDevice#serial#DevicePlugin": Array [],
"TestApp#Android#MockAndroidDevice#serial#StubPlugin": Array [],
"TestApp#Android#MockAndroidDevice#serial#TestPlugin": Array [
Object {
"api": "TestPlugin",
@@ -704,3 +712,39 @@ test('queue - messages that have not yet flushed be lost when disabling the plug
);
expect(getTestPluginState(client)).toEqual({count: 0});
});
test('queue will be cleaned up when it exceeds maximum size', () => {
let state: State = {};
const pluginKey = 'test';
const queueSize = 5000;
let i = 0;
for (i = 0; i < queueSize; i++) {
state = pluginMessageQueue(
state,
queueMessages(pluginKey, [{method: 'test', params: {i}}], queueSize),
);
}
// almost full
expect(state[pluginKey][0]).toEqual({method: 'test', params: {i: 0}});
expect(state[pluginKey].length).toBe(queueSize); // ~5000
expect(state[pluginKey][queueSize - 1]).toEqual({
method: 'test',
params: {i: queueSize - 1}, // ~4999
});
state = pluginMessageQueue(
state,
queueMessages(pluginKey, [{method: 'test', params: {i: ++i}}], queueSize),
);
const newLength = Math.ceil(0.9 * queueSize) + 1; // ~4500
expect(state[pluginKey].length).toBe(newLength);
expect(state[pluginKey][0]).toEqual({
method: 'test',
params: {i: queueSize - newLength + 1}, // ~500
});
expect(state[pluginKey][newLength - 1]).toEqual({
method: 'test',
params: {i: i}, // ~50001
});
});

View File

@@ -72,7 +72,7 @@ function createMockFlipperPluginWithNoPersistedState(id: string) {
}
test('getActivePersistentPlugins, where the non persistent plugins getting excluded', async () => {
const {store, device, client} = await createMockFlipperWithPlugin(
const {store} = await createMockFlipperWithPlugin(
createMockFlipperPluginWithDefaultPersistedState('ClientPlugin1'),
{
additionalPlugins: [
@@ -85,11 +85,6 @@ test('getActivePersistentPlugins, where the non persistent plugins getting exclu
);
const state = store.getState();
state.pluginStates = {
[getPluginKey(client.id, device, 'ClientPlugin1')]: {msg: 'DevicePlugin1'},
[getPluginKey(client.id, device, 'ClientPlugin4')]: {msg: 'ClientPlugin2'},
};
const list = getExportablePlugins(state);
expect(list).toEqual([
{
@@ -97,23 +92,23 @@ test('getActivePersistentPlugins, where the non persistent plugins getting exclu
label: 'ClientPlugin1',
},
{
id: 'ClientPlugin4',
label: 'ClientPlugin4',
id: 'ClientPlugin2',
label: 'ClientPlugin2',
},
{
id: 'ClientPlugin5',
label: 'ClientPlugin5',
},
// { Never activated, and no data received
// id: 'ClientPlugin5',
// label: 'ClientPlugin5',
// },
]);
});
test('getActivePersistentPlugins, where the plugins not in pluginState or queue gets excluded', async () => {
test('getActivePersistentPlugins, with message queue', async () => {
const {store, device, client} = await createMockFlipperWithPlugin(
createMockFlipperPluginWithDefaultPersistedState('Plugin1'),
{
additionalPlugins: [
createMockDeviceFlipperPlugin('DevicePlugin2'),
createMockFlipperPluginWithDefaultPersistedState('ClientPlugin1'),
createMockFlipperPluginWithNoPersistedState('ClientPlugin1'),
createMockFlipperPluginWithDefaultPersistedState('ClientPlugin2'),
createMockFlipperPluginWithDefaultPersistedState('ClientPlugin3'),
],
@@ -122,9 +117,6 @@ test('getActivePersistentPlugins, where the plugins not in pluginState or queue
const state = store.getState();
state.pluginStates = {
[getPluginKey(client.id, device, 'ClientPlugin2')]: {msg: 'ClientPlugin2'},
};
state.pluginMessageQueue = {
[getPluginKey(client.id, device, 'ClientPlugin3')]: [
{method: 'msg', params: {msg: 'ClientPlugin3'}},

View File

@@ -70,8 +70,7 @@ export function createSandyPluginWrapper<S, A extends BaseAction, P>(
if (
Plugin.persistedStateReducer ||
Plugin.exportPersistedState ||
Plugin.defaultPersistedState ||
Plugin.serializePersistedState
Plugin.defaultPersistedState
) {
client.onExport(async (idler, onStatusMessage) => {
const state = Plugin.exportPersistedState

View File

@@ -11,21 +11,14 @@ import os from 'os';
import path from 'path';
import electron from 'electron';
import {getInstance as getLogger} from '../fb-stubs/Logger';
import {Store, State as ReduxState, MiddlewareAPI} from '../reducers';
import {Store, MiddlewareAPI} from '../reducers';
import {DeviceExport} from '../devices/BaseDevice';
import {State as PluginStatesState} from '../reducers/pluginStates';
import {State as PluginsState} from '../reducers/plugins';
import {PluginNotification} from '../reducers/notifications';
import Client, {ClientExport, ClientQuery} from '../Client';
import {getAppVersion} from './info';
import {pluginKey} from '../reducers/pluginStates';
import {
callClient,
supportsMethod,
PluginDefinition,
DevicePluginMap,
ClientPluginMap,
} from '../plugin';
import {pluginKey} from '../utils/pluginUtils';
import {DevicePluginMap, ClientPluginMap} from '../plugin';
import {default as BaseDevice} from '../devices/BaseDevice';
import {default as ArchivedDevice} from '../devices/ArchivedDevice';
import fs from 'fs';
@@ -34,7 +27,6 @@ import {remote, OpenDialogOptions} from 'electron';
import {readCurrentRevision} from './packageMetadata';
import {tryCatchReportPlatformFailures} from './metrics';
import {promisify} from 'util';
import promiseTimeout from './promiseTimeout';
import {TestIdler} from './Idler';
import {setStaticView} from '../reducers/connections';
import {
@@ -42,10 +34,10 @@ import {
SupportFormRequestDetailsState,
} from '../reducers/supportForm';
import {setSelectPluginsToExportActiveSheet} from '../reducers/application';
import {deconstructClientId, deconstructPluginKey} from '../utils/clientUtils';
import {deconstructClientId} from '../utils/clientUtils';
import {performance} from 'perf_hooks';
import {processMessageQueue} from './messageQueue';
import {getPluginTitle, isSandyPlugin} from './pluginUtils';
import {getPluginTitle} from './pluginUtils';
import {capture} from './screenshot';
import {uploadFlipperMedia} from '../fb-stubs/user';
import {Idler} from 'flipper-plugin';
@@ -71,7 +63,6 @@ export type ExportType = {
device: DeviceExport | null;
deviceScreenshot: string | null;
store: {
pluginStates: PluginStatesExportState;
activeNotifications: Array<PluginNotification>;
};
// The GraphQL plugin relies on this format for generating
@@ -80,15 +71,6 @@ export type ExportType = {
supportRequestDetails?: SupportFormRequestDetailsState;
};
type ProcessPluginStatesOptions = {
clients: Array<ClientExport>;
serial: string;
allPluginStates: PluginStatesState;
devicePlugins: DevicePluginMap;
selectedPlugins: Array<string>;
statusUpdate?: (msg: string) => void;
};
type ProcessNotificationStatesOptions = {
clients: Array<ClientExport>;
serial: string;
@@ -101,7 +83,6 @@ type PluginsToProcess = {
pluginKey: string;
pluginId: string;
pluginName: string;
pluginClass: PluginDefinition;
client: Client;
}[];
@@ -110,7 +91,6 @@ type AddSaltToDeviceSerialOptions = {
device: BaseDevice;
deviceScreenshot: string | null;
clients: Array<ClientExport>;
pluginStates: PluginStatesExportState;
pluginStates2: SandyPluginStates;
devicePluginStates: Record<string, any>;
pluginNotification: Array<PluginNotification>;
@@ -152,51 +132,6 @@ export function processClients(
return filteredClients;
}
export function processPluginStates(
options: ProcessPluginStatesOptions,
): PluginStatesState {
const {
clients,
serial,
allPluginStates,
devicePlugins,
selectedPlugins,
statusUpdate,
} = options;
let pluginStates: PluginStatesState = {};
statusUpdate &&
statusUpdate('Filtering the plugin states for the filtered Clients...');
for (const key in allPluginStates) {
const plugin = deconstructPluginKey(key);
const pluginName = plugin.pluginName;
if (
pluginName &&
selectedPlugins.length > 0 &&
!selectedPlugins.includes(pluginName)
) {
continue;
}
if (plugin.type === 'client') {
if (!clients.some((c) => c.id.includes(plugin.client))) {
continue;
}
}
if (plugin.type === 'device') {
if (
!pluginName ||
!devicePlugins.has(pluginName) ||
serial !== plugin.client
) {
continue;
}
}
pluginStates = {...pluginStates, [key]: allPluginStates[key]};
}
return pluginStates;
}
export function processNotificationStates(
options: ProcessNotificationStatesOptions,
): Array<PluginNotification> {
@@ -216,41 +151,6 @@ export function processNotificationStates(
return activeNotifications;
}
const serializePluginStates = async (
pluginStates: PluginStatesState,
clientPlugins: ClientPluginMap,
devicePlugins: DevicePluginMap,
statusUpdate?: (msg: string) => void,
idler?: Idler,
): Promise<PluginStatesExportState> => {
const pluginsMap = new Map<string, PluginDefinition>([
...clientPlugins.entries(),
...devicePlugins.entries(),
]);
const pluginExportState: PluginStatesExportState = {};
for (const key in pluginStates) {
const pluginName = deconstructPluginKey(key).pluginName;
statusUpdate && statusUpdate(`Serialising ${pluginName}...`);
const serializationMarker = `${EXPORT_FLIPPER_TRACE_EVENT}:serialization-per-plugin`;
performance.mark(serializationMarker);
const pluginClass = pluginName ? pluginsMap.get(pluginName) : null;
if (isSandyPlugin(pluginClass)) {
continue; // Those are already processed by `exportSandyPluginStates`
} else if (pluginClass) {
pluginExportState[key] = await pluginClass.serializePersistedState(
pluginStates[key],
statusUpdate,
idler,
pluginName,
);
getLogger().trackTimeSince(serializationMarker, serializationMarker, {
plugin: pluginName,
});
}
}
return pluginExportState;
};
async function exportSandyPluginStates(
pluginsToProcess: PluginsToProcess,
idler: Idler,
@@ -258,8 +158,8 @@ async function exportSandyPluginStates(
): Promise<SandyPluginStates> {
const res: SandyPluginStates = {};
for (const key in pluginsToProcess) {
const {pluginId, client, pluginClass} = pluginsToProcess[key];
if (isSandyPlugin(pluginClass) && client.sandyPluginStates.has(pluginId)) {
const {pluginId, client} = pluginsToProcess[key];
if (client.sandyPluginStates.has(pluginId)) {
if (!res[client.id]) {
res[client.id] = {};
}
@@ -276,33 +176,6 @@ async function exportSandyPluginStates(
return res;
}
const deserializePluginStates = (
pluginStatesExportState: PluginStatesExportState,
clientPlugins: ClientPluginMap,
devicePlugins: DevicePluginMap,
): PluginStatesState => {
const pluginsMap = new Map<string, PluginDefinition>([
...clientPlugins.entries(),
...devicePlugins.entries(),
]);
const pluginsState: PluginStatesState = {};
for (const key in pluginStatesExportState) {
const pluginName = deconstructPluginKey(key).pluginName;
if (!pluginName || !pluginsMap.get(pluginName)) {
continue;
}
const pluginClass = pluginsMap.get(pluginName);
if (isSandyPlugin(pluginClass)) {
pluginsState[key] = pluginStatesExportState[key];
} else if (pluginClass) {
pluginsState[key] = pluginClass.deserializePersistedState(
pluginStatesExportState[key],
);
}
}
return pluginsState;
};
function replaceSerialsInKeys<T extends Record<string, any>>(
collection: T,
baseSerial: string,
@@ -311,9 +184,7 @@ function replaceSerialsInKeys<T extends Record<string, any>>(
const result: Record<string, any> = {};
for (const key in collection) {
if (!key.includes(baseSerial)) {
throw new Error(
`Error while exporting, plugin state (${key}) does not have ${baseSerial} in its key`,
);
continue;
}
result[key.replace(baseSerial, newSerial)] = collection[key];
}
@@ -325,7 +196,6 @@ async function addSaltToDeviceSerial({
device,
deviceScreenshot,
clients,
pluginStates,
pluginNotification,
statusUpdate,
pluginStates2,
@@ -354,11 +224,6 @@ async function addSaltToDeviceSerial({
statusUpdate(
'Adding salt to the selected device id in the plugin states...',
);
const updatedPluginStates = replaceSerialsInKeys(
pluginStates,
serial,
newSerial,
);
const updatedPluginStates2 = replaceSerialsInKeys(
pluginStates2,
serial,
@@ -385,7 +250,6 @@ async function addSaltToDeviceSerial({
device: {...newDevice.toJSON(), pluginStates: devicePluginStates},
deviceScreenshot: deviceScreenshot,
store: {
pluginStates: updatedPluginStates,
activeNotifications: updatedPluginNotifications,
},
pluginStates2: updatedPluginStates2,
@@ -395,7 +259,6 @@ async function addSaltToDeviceSerial({
type ProcessStoreOptions = {
activeNotifications: Array<PluginNotification>;
device: BaseDevice | null;
pluginStates: PluginStatesState;
pluginStates2: SandyPluginStates;
clients: Array<ClientExport>;
devicePlugins: DevicePluginMap;
@@ -409,11 +272,9 @@ export async function processStore(
{
activeNotifications,
device,
pluginStates,
pluginStates2,
clients,
devicePlugins,
clientPlugins,
salt,
selectedPlugins,
statusUpdate,
@@ -435,14 +296,7 @@ export async function processStore(
})
: null;
const processedClients = processClients(clients, serial, statusUpdate);
const processedPluginStates = processPluginStates({
clients: processedClients,
serial,
allPluginStates: pluginStates,
devicePlugins,
selectedPlugins,
statusUpdate,
});
const processedActiveNotifications = processNotificationStates({
clients: processedClients,
serial,
@@ -451,14 +305,6 @@ export async function processStore(
statusUpdate,
});
const exportPluginState = await serializePluginStates(
processedPluginStates,
clientPlugins,
devicePlugins,
statusUpdate,
idler,
);
const devicePluginStates = await device.exportState(
idler,
statusUpdate,
@@ -478,7 +324,6 @@ export async function processStore(
device,
deviceScreenshot: deviceScreenshotLink,
clients: processedClients,
pluginStates: exportPluginState,
pluginNotification: processedActiveNotifications,
statusUpdate,
selectedPlugins,
@@ -492,97 +337,17 @@ export async function processStore(
throw new Error('Selected device is null, please select a device');
}
export async function fetchMetadata(
pluginsToProcess: PluginsToProcess,
pluginStates: PluginStatesState,
state: ReduxState,
statusUpdate: (msg: string) => void,
idler: Idler,
): Promise<{
pluginStates: PluginStatesState;
errors: {[plugin: string]: Error} | null;
}> {
const newPluginState = {...pluginStates};
let errorObject: {[plugin: string]: Error} | null = null;
for (const {
pluginName,
pluginId,
pluginClass,
client,
pluginKey,
} of pluginsToProcess) {
const exportState =
pluginClass && !isSandyPlugin(pluginClass)
? pluginClass.exportPersistedState
: null;
if (exportState) {
const fetchMetaDataMarker = `${EXPORT_FLIPPER_TRACE_EVENT}:fetch-meta-data-per-plugin`;
const isConnected = client.connected.get();
performance.mark(fetchMetaDataMarker);
try {
statusUpdate &&
statusUpdate(`Fetching metadata for plugin ${pluginName}...`);
const data = await promiseTimeout(
240000, // Fetching MobileConfig data takes ~ 3 mins, thus keeping timeout at 4 mins.
exportState(
isConnected ? callClient(client, pluginId) : undefined,
newPluginState[pluginKey],
state,
idler,
statusUpdate,
isConnected
? supportsMethod(client, pluginId)
: () => Promise.resolve(false),
),
`Timed out while collecting data for ${pluginName}`,
);
if (!data) {
throw new Error(
`Metadata returned by the ${pluginName} is undefined`,
);
}
getLogger().trackTimeSince(fetchMetaDataMarker, fetchMetaDataMarker, {
pluginId,
});
newPluginState[pluginKey] = data;
} catch (e) {
if (!errorObject) {
errorObject = {};
}
errorObject[pluginName] = e;
getLogger().trackTimeSince(fetchMetaDataMarker, fetchMetaDataMarker, {
pluginId,
error: e,
});
continue;
}
}
}
return {pluginStates: newPluginState, errors: errorObject};
}
async function processQueues(
store: MiddlewareAPI,
pluginsToProcess: PluginsToProcess,
statusUpdate?: (msg: string) => void,
idler?: Idler,
) {
for (const {
pluginName,
pluginId,
pluginKey,
pluginClass,
client,
} of pluginsToProcess) {
if (isSandyPlugin(pluginClass) || pluginClass.persistedStateReducer) {
for (const {pluginName, pluginId, pluginKey, client} of pluginsToProcess) {
client.flushMessageBuffer();
const processQueueMarker = `${EXPORT_FLIPPER_TRACE_EVENT}:process-queue-per-plugin`;
performance.mark(processQueueMarker);
const plugin = isSandyPlugin(pluginClass)
? client.sandyPluginStates.get(pluginId)
: pluginClass;
const plugin = client.sandyPluginStates.get(pluginId);
if (!plugin) continue;
await processMessageQueue(
plugin,
@@ -603,7 +368,6 @@ async function processQueues(
});
}
}
}
export function determinePluginsToProcess(
clients: Array<Client>,
@@ -638,7 +402,6 @@ export function determinePluginsToProcess(
client,
pluginId: plugin,
pluginName: getPluginTitle(pluginClass),
pluginClass,
});
}
}
@@ -671,14 +434,6 @@ async function getStoreExport(
performance.mark(fetchMetaDataMarker);
const client = clients.find((client) => client.id === selectedApp);
const metadata = await fetchMetadata(
pluginsToProcess,
state.pluginStates,
state,
statusUpdate,
idler,
);
const newPluginState = metadata.pluginStates;
const pluginStates2 = pluginsToProcess
? await exportSandyPluginStates(pluginsToProcess, idler, statusUpdate)
@@ -687,7 +442,6 @@ async function getStoreExport(
getLogger().trackTimeSince(fetchMetaDataMarker, fetchMetaDataMarker, {
plugins: state.plugins.selectedPlugins,
});
const {errors} = metadata;
const {activeNotifications} = state.notifications;
const {devicePlugins, clientPlugins} = state.plugins;
@@ -695,7 +449,6 @@ async function getStoreExport(
{
activeNotifications,
device: selectedDevice,
pluginStates: newPluginState,
pluginStates2,
clients: client ? [client.toJSON()] : [],
devicePlugins,
@@ -706,7 +459,7 @@ async function getStoreExport(
},
idler,
);
return {exportData, fetchMetaDataErrors: errors};
return {exportData, fetchMetaDataErrors: null};
}
export async function exportStore(
@@ -810,34 +563,9 @@ export function importDataToStore(source: string, data: string, store: Store) {
payload: archivedDevice,
});
const {pluginStates} = json.store;
const processedPluginStates: PluginStatesState = deserializePluginStates(
pluginStates,
store.getState().plugins.clientPlugins,
store.getState().plugins.devicePlugins,
);
const keys = Object.keys(processedPluginStates);
keys.forEach((key) => {
store.dispatch({
type: 'SET_PLUGIN_STATE',
payload: {
pluginKey: key,
state: processedPluginStates[key],
},
});
});
clients.forEach((client: {id: string; query: ClientQuery}) => {
const sandyPluginStates = json.pluginStates2[client.id] || {};
const clientPlugins: Set<string> = new Set([
...keys
.filter((key) => {
const plugin = deconstructPluginKey(key);
return plugin.type === 'client' && client.id === plugin.client;
})
.map((pluginKey) => deconstructPluginKey(pluginKey).pluginName),
...Object.keys(sandyPluginStates),
]);
const clientPlugins = new Set(Object.keys(sandyPluginStates));
store.dispatch({
type: 'NEW_CLIENT',
payload: new Client(

View File

@@ -7,9 +7,8 @@
* @format
*/
import {PersistedStateReducer, FlipperDevicePlugin} from '../plugin';
import type {State, MiddlewareAPI} from '../reducers/index';
import {setPluginState} from '../reducers/pluginStates';
import {FlipperDevicePlugin} from '../plugin';
import type {MiddlewareAPI} from '../reducers/index';
import {
clearMessageQueue,
queueMessages,
@@ -23,31 +22,7 @@ import {defaultEnabledBackgroundPlugins} from './pluginUtils';
import {batch, Idler, _SandyPluginInstance} from 'flipper-plugin';
import {addBackgroundStat} from './pluginStats';
function processMessageClassic(
state: State,
pluginKey: string,
plugin: {
id: string;
persistedStateReducer: PersistedStateReducer | null;
},
message: Message,
): State {
const reducerStartTime = Date.now();
try {
const newPluginState = plugin.persistedStateReducer!(
state,
message.method,
message.params,
);
addBackgroundStat(plugin.id, Date.now() - reducerStartTime);
return newPluginState;
} catch (e) {
console.error(`Failed to process event for plugin ${plugin.id}`, e);
return state;
}
}
function processMessagesSandy(
function processMessagesImmediately(
plugin: _SandyPluginInstance,
messages: Message[],
) {
@@ -63,60 +38,20 @@ function processMessagesSandy(
}
}
export function processMessagesImmediately(
store: MiddlewareAPI,
pluginKey: string,
plugin:
| {
defaultPersistedState: any;
id: string;
persistedStateReducer: PersistedStateReducer | null;
}
| _SandyPluginInstance,
messages: Message[],
) {
if (plugin instanceof _SandyPluginInstance) {
processMessagesSandy(plugin, messages);
} else {
const persistedState = getCurrentPluginState(store, plugin, pluginKey);
const newPluginState = messages.reduce(
(state, message) =>
processMessageClassic(state, pluginKey, plugin, message),
persistedState,
);
if (persistedState !== newPluginState) {
store.dispatch(
setPluginState({
pluginKey,
state: newPluginState,
}),
);
}
}
}
export function processMessagesLater(
store: MiddlewareAPI,
pluginKey: string,
plugin:
| {
defaultPersistedState: any;
id: string;
persistedStateReducer: PersistedStateReducer | null;
maxQueueSize?: number;
}
| _SandyPluginInstance,
plugin: _SandyPluginInstance,
messages: Message[],
) {
const pluginId =
plugin instanceof _SandyPluginInstance ? plugin.definition.id : plugin.id;
const pluginId = plugin.definition.id;
const isSelected =
pluginKey === getSelectedPluginKey(store.getState().connections);
switch (true) {
// Navigation events are always processed immediately, to make sure the navbar stays up to date, see also T69991064
case pluginId === 'Navigation':
case isSelected && getPendingMessages(store, pluginKey).length === 0:
processMessagesImmediately(store, pluginKey, plugin, messages);
processMessagesImmediately(plugin, messages);
break;
case isSelected:
case plugin instanceof _SandyPluginInstance:
@@ -129,13 +64,7 @@ export function processMessagesLater(
pluginId,
):
store.dispatch(
queueMessages(
pluginKey,
messages,
plugin instanceof _SandyPluginInstance
? DEFAULT_MAX_QUEUE_SIZE
: plugin.maxQueueSize,
),
queueMessages(pluginKey, messages, DEFAULT_MAX_QUEUE_SIZE),
);
break;
default:
@@ -148,21 +77,12 @@ export function processMessagesLater(
}
export async function processMessageQueue(
plugin:
| {
defaultPersistedState: any;
id: string;
persistedStateReducer: PersistedStateReducer | null;
}
| _SandyPluginInstance,
plugin: _SandyPluginInstance,
pluginKey: string,
store: MiddlewareAPI,
progressCallback?: (progress: {current: number; total: number}) => void,
idler: Idler = new IdlerImpl(),
): Promise<boolean> {
if (!_SandyPluginInstance.is(plugin) && !plugin.persistedStateReducer) {
return true;
}
const total = getPendingMessages(store, pluginKey).length;
let progress = 0;
do {
@@ -171,25 +91,11 @@ export async function processMessageQueue(
break;
}
// there are messages to process! lets do so until we have to idle
// persistedState is irrelevant for SandyPlugins, as they store state locally
const persistedState = _SandyPluginInstance.is(plugin)
? undefined
: getCurrentPluginState(store, plugin, pluginKey);
let offset = 0;
let newPluginState = persistedState;
batch(() => {
do {
if (_SandyPluginInstance.is(plugin)) {
// Optimization: we could send a batch of messages here
processMessagesSandy(plugin, [messages[offset]]);
} else {
newPluginState = processMessageClassic(
newPluginState,
pluginKey,
plugin,
messages[offset],
);
}
processMessagesImmediately(plugin, [messages[offset]]);
offset++;
progress++;
@@ -203,17 +109,6 @@ export async function processMessageQueue(
// resistent to kicking off this process twice; grabbing, processing messages, saving state is done synchronosly
// until the idler has to break
store.dispatch(clearMessageQueue(pluginKey, offset));
if (
!_SandyPluginInstance.is(plugin) &&
newPluginState !== persistedState
) {
store.dispatch(
setPluginState({
pluginKey,
state: newPluginState,
}),
);
}
});
if (idler.isCancelled()) {
@@ -232,15 +127,3 @@ function getPendingMessages(
): Message[] {
return store.getState().pluginMessageQueue[pluginKey] || [];
}
function getCurrentPluginState(
store: MiddlewareAPI,
plugin: {defaultPersistedState: any},
pluginKey: string,
) {
// possible optimization: don't spread default state here by put proper default state when initializing clients
return {
...plugin.defaultPersistedState,
...store.getState().pluginStates[pluginKey],
};
}

View File

@@ -7,17 +7,9 @@
* @format
*/
import {
FlipperDevicePlugin,
FlipperBasePlugin,
PluginDefinition,
DevicePluginDefinition,
ClientPluginDefinition,
} from '../plugin';
import {PluginDefinition} from '../plugin';
import type {State} from '../reducers';
import type {State as PluginStatesState} from '../reducers/pluginStates';
import type {State as PluginsState} from '../reducers/plugins';
import {_SandyPluginDefinition} from 'flipper-plugin';
import type BaseDevice from '../devices/BaseDevice';
import type Client from '../Client';
import type {
@@ -29,9 +21,9 @@ import type {
import {getLatestCompatibleVersionOfEachPlugin} from '../dispatcher/plugins';
export type PluginLists = {
devicePlugins: DevicePluginDefinition[];
metroPlugins: DevicePluginDefinition[];
enabledPlugins: ClientPluginDefinition[];
devicePlugins: PluginDefinition[];
metroPlugins: PluginDefinition[];
enabledPlugins: PluginDefinition[];
disabledPlugins: PluginDefinition[];
unavailablePlugins: [plugin: PluginDetails, reason: string][];
downloadablePlugins: (DownloadablePluginDetails | BundledPluginDetails)[];
@@ -86,33 +78,12 @@ export function getPluginKey(
return `unknown#${pluginID}`;
}
export function isSandyPlugin(
plugin?: PluginDefinition | null,
): plugin is _SandyPluginDefinition {
return plugin instanceof _SandyPluginDefinition;
}
export function getPersistedState<PersistedState>(
pluginKey: string,
persistingPlugin: typeof FlipperBasePlugin | null,
pluginStates: PluginStatesState,
): PersistedState | null {
if (!persistingPlugin) {
return null;
}
const persistedState: PersistedState = {
...persistingPlugin.defaultPersistedState,
...pluginStates[pluginKey],
export const pluginKey = (serial: string, pluginName: string): string => {
return `${serial}#${pluginName}`;
};
return persistedState;
}
export function computeExportablePlugins(
state: Pick<
State,
'plugins' | 'connections' | 'pluginStates' | 'pluginMessageQueue'
>,
state: Pick<State, 'plugins' | 'connections' | 'pluginMessageQueue'>,
device: BaseDevice | null,
client: Client | null,
availablePlugins: PluginLists,
@@ -131,25 +102,14 @@ export function computeExportablePlugins(
}
function isExportablePlugin(
{
pluginStates,
pluginMessageQueue,
}: Pick<State, 'pluginStates' | 'pluginMessageQueue'>,
{pluginMessageQueue}: Pick<State, 'pluginMessageQueue'>,
device: BaseDevice | null,
client: Client | null,
plugin: PluginDefinition,
): boolean {
// can generate an export when requested
if (!isSandyPlugin(plugin) && plugin.exportPersistedState) {
return true;
}
const pluginKey = isDevicePluginDefinition(plugin)
? getPluginKey(undefined, device, plugin.id)
: getPluginKey(client?.id, undefined, plugin.id);
// plugin has exportable redux state
if (pluginStates[pluginKey]) {
return true;
}
// plugin has exportable sandy state
if (client?.sandyPluginStates.get(plugin.id)?.isPersistable()) {
return true;
@@ -158,10 +118,7 @@ function isExportablePlugin(
return true;
}
// plugin has pending messages and a persisted state reducer or isSandy
if (
pluginMessageQueue[pluginKey] &&
((plugin as any).defaultPersistedState || isSandyPlugin(plugin))
) {
if (pluginMessageQueue[pluginKey]) {
return true;
}
// nothing to serialize
@@ -201,11 +158,8 @@ export function isDevicePlugin(activePlugin: ActivePluginListItem) {
export function isDevicePluginDefinition(
definition: PluginDefinition,
): definition is DevicePluginDefinition {
return (
(definition as any).prototype instanceof FlipperDevicePlugin ||
(definition instanceof _SandyPluginDefinition && definition.isDevicePlugin)
);
): boolean {
return definition.isDevicePlugin;
}
export function getPluginTooltip(details: PluginDetails): string {
@@ -234,9 +188,9 @@ export function computePluginLists(
metroDevice: BaseDevice | null,
client: Client | null,
): {
devicePlugins: DevicePluginDefinition[];
metroPlugins: DevicePluginDefinition[];
enabledPlugins: ClientPluginDefinition[];
devicePlugins: PluginDefinition[];
metroPlugins: PluginDefinition[];
enabledPlugins: PluginDefinition[];
disabledPlugins: PluginDefinition[];
unavailablePlugins: [plugin: PluginDetails, reason: string][];
downloadablePlugins: (DownloadablePluginDetails | BundledPluginDetails)[];
@@ -247,17 +201,13 @@ export function computePluginLists(
...plugins.bundledPlugins.values(),
...plugins.marketplacePlugins,
]).filter((p) => !plugins.loadedPlugins.has(p.id));
const devicePlugins: DevicePluginDefinition[] = [
...plugins.devicePlugins.values(),
]
const devicePlugins: PluginDefinition[] = [...plugins.devicePlugins.values()]
.filter((p) => device?.supportsPlugin(p))
.filter((p) => enabledDevicePluginsState.has(p.id));
const metroPlugins: DevicePluginDefinition[] = [
...plugins.devicePlugins.values(),
]
const metroPlugins: PluginDefinition[] = [...plugins.devicePlugins.values()]
.filter((p) => metroDevice?.supportsPlugin(p))
.filter((p) => enabledDevicePluginsState.has(p.id));
const enabledPlugins: ClientPluginDefinition[] = [];
const enabledPlugins: PluginDefinition[] = [];
const disabledPlugins: PluginDefinition[] = [
...plugins.devicePlugins.values(),
]

View File

@@ -49,6 +49,6 @@ export function getFlipperLibImplementation(): FlipperLib {
return flipperLibInstance;
}
export function setFlipperLibImplementation(impl: FlipperLib) {
export function setFlipperLibImplementation(impl: FlipperLib | undefined) {
flipperLibInstance = impl;
}

View File

@@ -14,7 +14,7 @@ import styled from '@emotion/styled';
import React, {MouseEvent, KeyboardEvent} from 'react';
import {theme} from '../theme';
import {Layout} from '../Layout';
import {tryGetFlipperLibImplementation} from 'flipper-plugin/src/plugin/FlipperLib';
import {_getFlipperLibImplementation} from 'flipper-plugin';
import {DownOutlined, RightOutlined} from '@ant-design/icons';
const {Text} = Typography;
@@ -221,7 +221,7 @@ class ElementsRow extends PureComponent<ElementsRowProps, ElementsRowState> {
{
label: 'Copy',
click: () => {
tryGetFlipperLibImplementation()?.writeTextToClipboard(
_getFlipperLibImplementation()?.writeTextToClipboard(
props.onCopyExpandedTree(props.element, 0),
);
},
@@ -229,7 +229,7 @@ class ElementsRow extends PureComponent<ElementsRowProps, ElementsRowState> {
{
label: 'Copy expanded child elements',
click: () =>
tryGetFlipperLibImplementation()?.writeTextToClipboard(
_getFlipperLibImplementation()?.writeTextToClipboard(
props.onCopyExpandedTree(props.element, 255),
),
},
@@ -253,7 +253,7 @@ class ElementsRow extends PureComponent<ElementsRowProps, ElementsRowState> {
return {
label: `Copy ${o.name}`,
click: () => {
tryGetFlipperLibImplementation()?.writeTextToClipboard(o.value);
_getFlipperLibImplementation()?.writeTextToClipboard(o.value);
},
};
}),
@@ -555,7 +555,7 @@ export class Elements extends PureComponent<ElementsProps, ElementsState> {
) {
e.stopPropagation();
e.preventDefault();
tryGetFlipperLibImplementation()?.writeTextToClipboard(
_getFlipperLibImplementation()?.writeTextToClipboard(
selectedElement.name,
);
return;