Introduce PluginRenderer to render plugins

Summary: PluginContainer will now wrap Sandy plugins in PluginRenderer. PluginRenderer will also be used by plugin unit tests in the future

Reviewed By: jknoxville

Differential Revision: D22159359

fbshipit-source-id: 69f9c8f4bec9392022c1d7a14957f5aca0339d97
This commit is contained in:
Michel Weststrate
2020-07-01 08:58:40 -07:00
committed by Facebook GitHub Bot
parent ba01fa5bc9
commit f2c39aed55
7 changed files with 216 additions and 66 deletions

View File

@@ -47,6 +47,7 @@ import {Message} from './reducers/pluginMessageQueue';
import {Idler} from './utils/Idler';
import {processMessageQueue} from './utils/messageQueue';
import {ToggleButton, SmallText} from './ui';
import {SandyPluginRenderer} from 'flipper-plugin';
const Container = styled(FlexColumn)({
width: 0,
@@ -332,10 +333,21 @@ class PluginContainer extends PureComponent<Props, State> {
console.warn(`No selected plugin. Rendering empty!`);
return null;
}
let pluginElement: null | React.ReactElement<any>;
if (isSandyPlugin(activePlugin)) {
// TODO:
return null;
if (target instanceof Client) {
// Make sure we throw away the container for different pluginKey!
pluginElement = (
<SandyPluginRenderer
key={pluginKey}
plugin={target.sandyPluginStates.get(activePlugin.id)!}
/>
);
} else {
// TODO: target might be a device as well, support that T68738317
pluginElement = null;
}
} else {
const props: PluginProps<Object> & {
key: string;
ref: (
@@ -349,14 +361,14 @@ class PluginContainer extends PureComponent<Props, State> {
key: pluginKey,
logger: this.props.logger,
selectedApp,
settingsState,
persistedState: activePlugin.defaultPersistedState
? {
...activePlugin.defaultPersistedState,
...pluginState,
}
: pluginState,
setStaticView: (payload: StaticView) => this.props.setStaticView(payload),
setStaticView: (payload: StaticView) =>
this.props.setStaticView(payload),
setPersistedState: (state) => setPluginState({pluginKey, state}),
target,
deepLinkPayload: this.props.deepLinkPayload,
@@ -367,10 +379,16 @@ class PluginContainer extends PureComponent<Props, State> {
target instanceof Client &&
target.plugins.some((p) => p === pluginID)
) {
this.props.selectPlugin({selectedPlugin: pluginID, deepLinkPayload});
this.props.selectPlugin({
selectedPlugin: pluginID,
deepLinkPayload,
});
return true;
} else if (target instanceof BaseDevice) {
this.props.selectPlugin({selectedPlugin: pluginID, deepLinkPayload});
this.props.selectPlugin({
selectedPlugin: pluginID,
deepLinkPayload,
});
return true;
} else {
return false;
@@ -378,7 +396,10 @@ class PluginContainer extends PureComponent<Props, State> {
},
ref: this.refChanged,
isArchivedDevice,
settingsState,
};
pluginElement = React.createElement(activePlugin, props);
}
return (
<React.Fragment>
<Container key="plugin">
@@ -386,7 +407,7 @@ class PluginContainer extends PureComponent<Props, State> {
heading={`Plugin "${
activePlugin.title || 'Unknown'
}" encountered an error during render`}>
{React.createElement(activePlugin, props)}
{pluginElement}
</ErrorBoundary>
</Container>
<SidebarContainer id="detailsSidebar" />

View File

@@ -7,10 +7,14 @@
* @format
*/
import React from 'react';
import React, {useContext} from 'react';
import produce from 'immer';
import {FlipperPlugin} from '../plugin';
import {renderMockFlipperWithPlugin} from '../test-utils/createMockFlipperWithPlugin';
import {
renderMockFlipperWithPlugin,
createMockPluginDetails,
} from '../test-utils/createMockFlipperWithPlugin';
import {SandyPluginContext, SandyPluginDefinition} from 'flipper-plugin';
interface PersistedState {
count: 1;
@@ -79,3 +83,55 @@ test('Plugin container can render plugin and receive updates', async () => {
expect((await renderer.findByTestId('counter')).textContent).toBe('2');
});
test('PluginContainer can render Sandy plugins', async () => {
let renders = 0;
function MySandyPlugin() {
renders++;
const sandyContext = useContext(SandyPluginContext);
expect(sandyContext).not.toBe(null);
return <div>Hello from Sandy</div>;
}
const plugin = () => ({});
const definition = new SandyPluginDefinition(createMockPluginDetails(), {
plugin,
Component: MySandyPlugin,
});
// any cast because this plugin is not enriched with the meta data that the plugin loader
// normally adds. Our further sandy plugin test infra won't need this, but
// for this test we do need to act a s a loaded plugin, to make sure PluginContainer itself can handle it
const {renderer, act, sendMessage} = await renderMockFlipperWithPlugin(
definition,
);
expect(renderer.baseElement).toMatchInlineSnapshot(`
<body>
<div>
<div
class="css-1orvm1g-View-FlexBox-FlexColumn"
>
<div>
Hello from Sandy
</div>
</div>
<div
class="css-bxcvv9-View-FlexBox-FlexRow"
id="detailsSidebar"
/>
</div>
</body>
`);
expect(renders).toBe(1);
// sending a new message doesn't cause a re-render
act(() => {
sendMessage('inc', {delta: 2});
});
// TODO: check that onConnect is called T68683507
// TODO: check that messages have arrived T68683442
expect(renders).toBe(1);
});

View File

@@ -7,7 +7,10 @@
* @format
*/
import {createMockFlipperWithPlugin} from '../../test-utils/createMockFlipperWithPlugin';
import {
createMockFlipperWithPlugin,
createMockPluginDetails,
} from '../../test-utils/createMockFlipperWithPlugin';
import {Store, Client} from '../../';
import {selectPlugin, starPlugin} from '../../reducers/connections';
import {registerPlugins} from '../../reducers/plugins';
@@ -21,18 +24,7 @@ interface PersistedState {
count: 1;
}
const pluginDetails = {
id: 'TestPlugin',
dir: '',
name: 'TestPlugin',
specVersion: 0,
entry: '',
isDefault: false,
main: '',
source: '',
title: 'Testing Plugin',
version: '',
} as const;
const pluginDetails = createMockPluginDetails();
let TestPlugin: SandyPluginDefinition;
@@ -114,11 +106,10 @@ test('it should not initialize a sandy plugin if not enabled', async () => {
const {client, store} = await createMockFlipperWithPlugin(TestPlugin);
const Plugin2 = new SandyPluginDefinition(
{
...pluginDetails,
createMockPluginDetails({
name: 'Plugin2',
id: 'Plugin2',
},
}),
{
plugin: jest.fn().mockImplementation((client) => {
const destroyStub = jest.fn();

View File

@@ -29,11 +29,12 @@ import Client, {ClientQuery} from '../Client';
import {buildClientId} from '../utils/clientUtils';
import {Logger} from '../fb-interfaces/Logger';
import {FlipperPlugin, PluginDefinition} from '../plugin';
import {PluginDefinition} from '../plugin';
import {registerPlugins} from '../reducers/plugins';
import PluginContainer from '../PluginContainer';
import {getPluginKey} from '../utils/pluginUtils';
import {getInstance} from '../fb-stubs/Logger';
import {PluginDetails} from 'flipper-plugin-lib';
type MockFlipperResult = {
client: Client;
@@ -182,7 +183,7 @@ export async function createMockFlipperWithPlugin(
type Renderer = RenderResult<typeof import('testing-library__dom/queries')>;
export async function renderMockFlipperWithPlugin(
pluginClazz: typeof FlipperPlugin,
pluginClazz: PluginDefinition,
): Promise<
MockFlipperResult & {
renderer: Renderer;
@@ -219,3 +220,21 @@ export async function renderMockFlipperWithPlugin(
},
};
}
export function createMockPluginDetails(
details?: Partial<PluginDetails>,
): PluginDetails {
return {
id: 'TestPlugin',
dir: '',
name: 'TestPlugin',
specVersion: 0,
entry: '',
isDefault: false,
main: '',
source: '',
title: 'Testing Plugin',
version: '',
...details,
};
}

View File

@@ -9,3 +9,5 @@
export * from './plugin/Plugin';
export * from './plugin/SandyPluginDefinition';
export * from './plugin/PluginRenderer';
export * from './plugin/PluginContext';

View File

@@ -0,0 +1,23 @@
/**
* 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 {createContext} from 'react';
export type SandyPluginContext = {
deactivate(): void;
};
// TODO: to be filled in later with testing and such
const stubPluginContext: SandyPluginContext = {
deactivate() {},
};
export const SandyPluginContext = createContext<SandyPluginContext>(
stubPluginContext,
);

View File

@@ -0,0 +1,38 @@
/**
* 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, {memo, useEffect, createElement} from 'react';
import {SandyPluginContext} from './PluginContext';
import {SandyPluginInstance} from './Plugin';
type Props = {
plugin: SandyPluginInstance;
};
/**
* Component to render a Sandy plugin container
*/
export const SandyPluginRenderer = memo(
({plugin}: Props) => {
useEffect(() => {
plugin.deactivate();
}, [plugin]);
return (
<SandyPluginContext.Provider value={plugin}>
{createElement(plugin.definition.module.Component)}
</SandyPluginContext.Provider>
);
},
() => {
// One of the goals of the ModernPluginContainer is that we want to prevent it from rendering
// for any outside change. Whatever happens outside of us, we don't care. If it is relevant for use, we take care about it from the insde
return true;
},
);