Summary: Some styling fixes and minor improvements in DataTable, used by network plugin: - be able to customise the context menu - be able to customise how entire rows are copied and presented on the clipboard to be able to deviate from the standard JSON - deeplink handling was made async, this gives the plugin the opportunity to first handle initial setup and rendering before trying to jump somewhere which is a typical use case for deeplinking Reviewed By: passy Differential Revision: D27947186 fbshipit-source-id: a56f081d60520c4bc2ad3c547a8ca5b9357e71a1
1018 lines
26 KiB
TypeScript
1018 lines
26 KiB
TypeScript
/**
|
|
* 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 produce from 'immer';
|
|
import {FlipperPlugin} from '../plugin';
|
|
import {renderMockFlipperWithPlugin} from '../test-utils/createMockFlipperWithPlugin';
|
|
import {
|
|
_SandyPluginDefinition,
|
|
PluginClient,
|
|
TestUtils,
|
|
usePlugin,
|
|
createState,
|
|
DevicePluginClient,
|
|
DeviceLogEntry,
|
|
useValue,
|
|
} from 'flipper-plugin';
|
|
import {selectPlugin} from '../reducers/connections';
|
|
import {updateSettings} from '../reducers/settings';
|
|
import {switchPlugin} from '../reducers/pluginManager';
|
|
import {sleep} from 'flipper-plugin/src/utils/sleep';
|
|
|
|
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},
|
|
) {
|
|
return produce(persistedState, (draft) => {
|
|
if (method === 'inc') {
|
|
draft.count += payload?.delta || 1;
|
|
}
|
|
});
|
|
}
|
|
|
|
render() {
|
|
return (
|
|
<h1>
|
|
Hello:{' '}
|
|
<span data-testid="counter">{this.props.persistedState.count}</span>
|
|
</h1>
|
|
);
|
|
}
|
|
}
|
|
|
|
test('Plugin container can render plugin and receive updates', async () => {
|
|
const {renderer, sendMessage, act} = await renderMockFlipperWithPlugin(
|
|
TestPlugin,
|
|
);
|
|
expect(renderer.baseElement).toMatchInlineSnapshot(`
|
|
<body>
|
|
<div>
|
|
<div
|
|
class="css-w6yhx2-View-FlexBox-FlexColumn"
|
|
>
|
|
<h1>
|
|
Hello:
|
|
|
|
<span
|
|
data-testid="counter"
|
|
>
|
|
0
|
|
</span>
|
|
</h1>
|
|
</div>
|
|
<div
|
|
class="css-o0040c-View-FlexBox-FlexRow"
|
|
id="detailsSidebar"
|
|
/>
|
|
</div>
|
|
</body>
|
|
`);
|
|
|
|
act(() => {
|
|
sendMessage('inc', {delta: 2});
|
|
});
|
|
|
|
expect((await renderer.findByTestId('counter')).textContent).toBe('2');
|
|
});
|
|
|
|
test('PluginContainer can render Sandy plugins', async () => {
|
|
let renders = 0;
|
|
|
|
function MySandyPlugin() {
|
|
renders++;
|
|
const sandyApi = usePlugin(plugin);
|
|
const count = useValue(sandyApi.count);
|
|
expect(Object.keys(sandyApi)).toEqual([
|
|
'connectedStub',
|
|
'disconnectedStub',
|
|
'activatedStub',
|
|
'deactivatedStub',
|
|
'count',
|
|
]);
|
|
expect(() => {
|
|
// eslint-disable-next-line
|
|
usePlugin(function bla() {
|
|
return {};
|
|
});
|
|
}).toThrowError(/didn't match the type of the requested plugin/);
|
|
return <div>Hello from Sandy{count}</div>;
|
|
}
|
|
|
|
type Events = {
|
|
inc: {delta: number};
|
|
};
|
|
|
|
const plugin = (client: PluginClient<Events>) => {
|
|
const count = createState(0);
|
|
const connectedStub = jest.fn();
|
|
const disconnectedStub = jest.fn();
|
|
const activatedStub = jest.fn();
|
|
const deactivatedStub = jest.fn();
|
|
client.onConnect(connectedStub);
|
|
client.onDisconnect(disconnectedStub);
|
|
client.onActivate(activatedStub);
|
|
client.onDeactivate(deactivatedStub);
|
|
client.onMessage('inc', ({delta}) => {
|
|
count.set(count.get() + delta);
|
|
});
|
|
return {
|
|
connectedStub,
|
|
disconnectedStub,
|
|
activatedStub,
|
|
deactivatedStub,
|
|
count,
|
|
};
|
|
};
|
|
|
|
const definition = new _SandyPluginDefinition(
|
|
TestUtils.createMockPluginDetails(),
|
|
{
|
|
plugin,
|
|
Component: MySandyPlugin,
|
|
},
|
|
);
|
|
const {
|
|
renderer,
|
|
act,
|
|
sendMessage,
|
|
client,
|
|
store,
|
|
} = await renderMockFlipperWithPlugin(definition);
|
|
|
|
expect(client.rawSend).toBeCalledWith('init', {plugin: 'TestPlugin'});
|
|
|
|
expect(renderer.baseElement).toMatchInlineSnapshot(`
|
|
<body>
|
|
<div>
|
|
<div
|
|
class="css-w6yhx2-View-FlexBox-FlexColumn"
|
|
>
|
|
<div>
|
|
Hello from Sandy
|
|
0
|
|
</div>
|
|
</div>
|
|
<div
|
|
class="css-o0040c-View-FlexBox-FlexRow"
|
|
id="detailsSidebar"
|
|
/>
|
|
</div>
|
|
</body>
|
|
`);
|
|
expect(renders).toBe(1);
|
|
|
|
// sending irrelevant message does not cause a re-render
|
|
act(() => {
|
|
sendMessage('oops', {delta: 2});
|
|
});
|
|
expect(renders).toBe(1);
|
|
|
|
// sending a new message cause a re-render
|
|
act(() => {
|
|
sendMessage('inc', {delta: 2});
|
|
});
|
|
expect(renders).toBe(2);
|
|
expect(renderer.baseElement).toMatchInlineSnapshot(`
|
|
<body>
|
|
<div>
|
|
<div
|
|
class="css-w6yhx2-View-FlexBox-FlexColumn"
|
|
>
|
|
<div>
|
|
Hello from Sandy
|
|
2
|
|
</div>
|
|
</div>
|
|
<div
|
|
class="css-o0040c-View-FlexBox-FlexRow"
|
|
id="detailsSidebar"
|
|
/>
|
|
</div>
|
|
</body>
|
|
`);
|
|
|
|
// make sure the plugin gets connected
|
|
const pluginInstance: ReturnType<
|
|
typeof plugin
|
|
> = client.sandyPluginStates.get(definition.id)!.instanceApi;
|
|
expect(pluginInstance.connectedStub).toBeCalledTimes(1);
|
|
expect(pluginInstance.disconnectedStub).toBeCalledTimes(0);
|
|
expect(pluginInstance.activatedStub).toBeCalledTimes(1);
|
|
expect(pluginInstance.deactivatedStub).toBeCalledTimes(0);
|
|
|
|
// select non existing plugin
|
|
act(() => {
|
|
store.dispatch(
|
|
selectPlugin({
|
|
selectedPlugin: 'Logs',
|
|
deepLinkPayload: null,
|
|
}),
|
|
);
|
|
});
|
|
|
|
expect(client.rawSend).toBeCalledWith('deinit', {plugin: 'TestPlugin'});
|
|
|
|
expect(renderer.baseElement).toMatchInlineSnapshot(`
|
|
<body>
|
|
<div />
|
|
</body>
|
|
`);
|
|
expect(pluginInstance.connectedStub).toBeCalledTimes(1);
|
|
expect(pluginInstance.disconnectedStub).toBeCalledTimes(1);
|
|
expect(pluginInstance.activatedStub).toBeCalledTimes(1);
|
|
expect(pluginInstance.deactivatedStub).toBeCalledTimes(1);
|
|
|
|
// send some messages while in BG
|
|
act(() => {
|
|
sendMessage('inc', {delta: 3});
|
|
sendMessage('inc', {delta: 4});
|
|
});
|
|
expect(renders).toBe(2);
|
|
expect(pluginInstance.count.get()).toBe(2);
|
|
|
|
// go back
|
|
act(() => {
|
|
store.dispatch(
|
|
selectPlugin({
|
|
selectedPlugin: definition.id,
|
|
deepLinkPayload: null,
|
|
}),
|
|
);
|
|
});
|
|
// Might be needed, but seems to work reliable without: await sleep(1000);
|
|
expect(renderer.baseElement).toMatchInlineSnapshot(`
|
|
<body>
|
|
<div>
|
|
<div
|
|
class="css-w6yhx2-View-FlexBox-FlexColumn"
|
|
>
|
|
<div>
|
|
Hello from Sandy
|
|
9
|
|
</div>
|
|
</div>
|
|
<div
|
|
class="css-o0040c-View-FlexBox-FlexRow"
|
|
id="detailsSidebar"
|
|
/>
|
|
</div>
|
|
</body>
|
|
`);
|
|
|
|
expect(pluginInstance.count.get()).toBe(9);
|
|
expect(pluginInstance.connectedStub).toBeCalledTimes(2);
|
|
expect(pluginInstance.disconnectedStub).toBeCalledTimes(1);
|
|
expect(pluginInstance.activatedStub).toBeCalledTimes(2);
|
|
expect(pluginInstance.deactivatedStub).toBeCalledTimes(1);
|
|
expect(client.rawSend).toBeCalledWith('init', {plugin: 'TestPlugin'});
|
|
|
|
// disable
|
|
act(() => {
|
|
store.dispatch(
|
|
switchPlugin({
|
|
plugin: definition,
|
|
selectedApp: client.query.app,
|
|
}),
|
|
);
|
|
});
|
|
expect(pluginInstance.connectedStub).toBeCalledTimes(2);
|
|
expect(pluginInstance.disconnectedStub).toBeCalledTimes(2);
|
|
expect(pluginInstance.activatedStub).toBeCalledTimes(2);
|
|
expect(pluginInstance.deactivatedStub).toBeCalledTimes(2);
|
|
expect(client.rawSend).toBeCalledWith('deinit', {plugin: 'TestPlugin'});
|
|
|
|
// re-enable
|
|
act(() => {
|
|
store.dispatch(
|
|
switchPlugin({
|
|
plugin: definition,
|
|
selectedApp: client.query.app,
|
|
}),
|
|
);
|
|
});
|
|
// note: this is the old pluginInstance, so that one is not reconnected!
|
|
expect(pluginInstance.connectedStub).toBeCalledTimes(2);
|
|
expect(pluginInstance.disconnectedStub).toBeCalledTimes(2);
|
|
expect(pluginInstance.activatedStub).toBeCalledTimes(2);
|
|
expect(pluginInstance.deactivatedStub).toBeCalledTimes(2);
|
|
|
|
expect(
|
|
client.sandyPluginStates.get('TestPlugin')!.instanceApi.connectedStub,
|
|
).toBeCalledTimes(1);
|
|
expect(client.rawSend).toBeCalledWith('init', {plugin: 'TestPlugin'});
|
|
expect(
|
|
client.sandyPluginStates.get('TestPlugin')!.instanceApi.count.get(),
|
|
).toBe(0);
|
|
});
|
|
|
|
test('PluginContainer triggers correct lifecycles for background plugin', async () => {
|
|
function MySandyPlugin() {
|
|
return <div>Hello from Sandy</div>;
|
|
}
|
|
|
|
const plugin = (client: PluginClient) => {
|
|
const connectedStub = jest.fn();
|
|
const disconnectedStub = jest.fn();
|
|
const activatedStub = jest.fn();
|
|
const deactivatedStub = jest.fn();
|
|
client.onConnect(connectedStub);
|
|
client.onDisconnect(disconnectedStub);
|
|
client.onActivate(activatedStub);
|
|
client.onDeactivate(deactivatedStub);
|
|
return {connectedStub, disconnectedStub, activatedStub, deactivatedStub};
|
|
};
|
|
|
|
const definition = new _SandyPluginDefinition(
|
|
TestUtils.createMockPluginDetails(),
|
|
{
|
|
plugin,
|
|
Component: MySandyPlugin,
|
|
},
|
|
);
|
|
const {act, client, store} = await renderMockFlipperWithPlugin(definition, {
|
|
onSend(method) {
|
|
if (method === 'getBackgroundPlugins') {
|
|
return {plugins: [definition.id]};
|
|
}
|
|
},
|
|
});
|
|
|
|
expect(client.rawSend).toBeCalledWith('init', {plugin: 'TestPlugin'});
|
|
(client.rawSend as jest.Mock).mockClear();
|
|
// make sure the plugin gets connected
|
|
const pluginInstance: ReturnType<
|
|
typeof plugin
|
|
> = client.sandyPluginStates.get(definition.id)!.instanceApi;
|
|
expect(pluginInstance.connectedStub).toBeCalledTimes(1);
|
|
expect(pluginInstance.disconnectedStub).toBeCalledTimes(0);
|
|
expect(pluginInstance.activatedStub).toBeCalledTimes(1);
|
|
expect(pluginInstance.deactivatedStub).toBeCalledTimes(0);
|
|
|
|
// select non existing plugin
|
|
act(() => {
|
|
store.dispatch(
|
|
selectPlugin({
|
|
selectedPlugin: 'Logs',
|
|
deepLinkPayload: null,
|
|
}),
|
|
);
|
|
});
|
|
// bg plugin!
|
|
expect(client.rawSend).not.toBeCalled();
|
|
(client.rawSend as jest.Mock).mockClear();
|
|
expect(pluginInstance.connectedStub).toBeCalledTimes(1);
|
|
expect(pluginInstance.disconnectedStub).toBeCalledTimes(0);
|
|
expect(pluginInstance.activatedStub).toBeCalledTimes(1);
|
|
expect(pluginInstance.deactivatedStub).toBeCalledTimes(1);
|
|
|
|
// go back
|
|
act(() => {
|
|
store.dispatch(
|
|
selectPlugin({
|
|
selectedPlugin: definition.id,
|
|
deepLinkPayload: null,
|
|
}),
|
|
);
|
|
});
|
|
expect(pluginInstance.connectedStub).toBeCalledTimes(1);
|
|
expect(pluginInstance.disconnectedStub).toBeCalledTimes(0);
|
|
expect(pluginInstance.activatedStub).toBeCalledTimes(2);
|
|
expect(pluginInstance.deactivatedStub).toBeCalledTimes(1);
|
|
expect(client.rawSend).not.toBeCalled();
|
|
(client.rawSend as jest.Mock).mockClear();
|
|
|
|
// disable
|
|
act(() => {
|
|
store.dispatch(
|
|
switchPlugin({
|
|
plugin: definition,
|
|
selectedApp: client.query.app,
|
|
}),
|
|
);
|
|
});
|
|
expect(pluginInstance.connectedStub).toBeCalledTimes(1);
|
|
expect(pluginInstance.disconnectedStub).toBeCalledTimes(1);
|
|
expect(pluginInstance.activatedStub).toBeCalledTimes(2);
|
|
expect(pluginInstance.deactivatedStub).toBeCalledTimes(2);
|
|
expect(client.rawSend).toBeCalledWith('deinit', {plugin: 'TestPlugin'});
|
|
(client.rawSend as jest.Mock).mockClear();
|
|
|
|
// select something else
|
|
act(() => {
|
|
store.dispatch(
|
|
selectPlugin({
|
|
selectedPlugin: 'Logs',
|
|
deepLinkPayload: null,
|
|
}),
|
|
);
|
|
});
|
|
// re-enable
|
|
act(() => {
|
|
store.dispatch(
|
|
switchPlugin({
|
|
plugin: definition,
|
|
selectedApp: client.query.app,
|
|
}),
|
|
);
|
|
});
|
|
// note: this is the old pluginInstance, so that one is not reconnected!
|
|
expect(pluginInstance.connectedStub).toBeCalledTimes(1);
|
|
expect(pluginInstance.disconnectedStub).toBeCalledTimes(1);
|
|
expect(pluginInstance.activatedStub).toBeCalledTimes(2);
|
|
expect(pluginInstance.deactivatedStub).toBeCalledTimes(2);
|
|
|
|
const newPluginInstance: ReturnType<
|
|
typeof plugin
|
|
> = client.sandyPluginStates.get('TestPlugin')!.instanceApi;
|
|
expect(newPluginInstance.connectedStub).toBeCalledTimes(1);
|
|
expect(newPluginInstance.disconnectedStub).toBeCalledTimes(0);
|
|
expect(newPluginInstance.activatedStub).toBeCalledTimes(0);
|
|
expect(newPluginInstance.deactivatedStub).toBeCalledTimes(0);
|
|
expect(client.rawSend).toBeCalledWith('init', {plugin: 'TestPlugin'});
|
|
(client.rawSend as jest.Mock).mockClear();
|
|
|
|
// select new plugin
|
|
act(() => {
|
|
store.dispatch(
|
|
selectPlugin({
|
|
selectedPlugin: definition.id,
|
|
deepLinkPayload: null,
|
|
}),
|
|
);
|
|
});
|
|
|
|
expect(newPluginInstance.connectedStub).toBeCalledTimes(1);
|
|
expect(newPluginInstance.disconnectedStub).toBeCalledTimes(0);
|
|
expect(newPluginInstance.activatedStub).toBeCalledTimes(1);
|
|
expect(newPluginInstance.deactivatedStub).toBeCalledTimes(0);
|
|
expect(client.rawSend).not.toBeCalled();
|
|
(client.rawSend as jest.Mock).mockClear();
|
|
});
|
|
|
|
test('PluginContainer + Sandy plugin supports deeplink', async () => {
|
|
const linksSeen: any[] = [];
|
|
|
|
const plugin = (client: PluginClient) => {
|
|
const linkState = createState('');
|
|
client.onDeepLink((link) => {
|
|
linksSeen.push(link);
|
|
linkState.set(String(link));
|
|
});
|
|
return {
|
|
linkState,
|
|
};
|
|
};
|
|
|
|
const definition = new _SandyPluginDefinition(
|
|
TestUtils.createMockPluginDetails(),
|
|
{
|
|
plugin,
|
|
Component() {
|
|
const instance = usePlugin(plugin);
|
|
const linkState = useValue(instance.linkState);
|
|
return <h1>hello {linkState || 'world'}</h1>;
|
|
},
|
|
},
|
|
);
|
|
const {renderer, act, client, store} = await renderMockFlipperWithPlugin(
|
|
definition,
|
|
);
|
|
|
|
expect(client.rawSend).toBeCalledWith('init', {plugin: 'TestPlugin'});
|
|
|
|
expect(linksSeen).toEqual([]);
|
|
expect(renderer.baseElement).toMatchInlineSnapshot(`
|
|
<body>
|
|
<div>
|
|
<div
|
|
class="css-w6yhx2-View-FlexBox-FlexColumn"
|
|
>
|
|
<h1>
|
|
hello
|
|
world
|
|
</h1>
|
|
</div>
|
|
<div
|
|
class="css-o0040c-View-FlexBox-FlexRow"
|
|
id="detailsSidebar"
|
|
/>
|
|
</div>
|
|
</body>
|
|
`);
|
|
|
|
act(() => {
|
|
store.dispatch(
|
|
selectPlugin({
|
|
selectedPlugin: definition.id,
|
|
deepLinkPayload: 'universe!',
|
|
selectedApp: client.query.app,
|
|
}),
|
|
);
|
|
});
|
|
|
|
await sleep(10);
|
|
expect(linksSeen).toEqual(['universe!']);
|
|
expect(renderer.baseElement).toMatchInlineSnapshot(`
|
|
<body>
|
|
<div>
|
|
<div
|
|
class="css-w6yhx2-View-FlexBox-FlexColumn"
|
|
>
|
|
<h1>
|
|
hello
|
|
universe!
|
|
</h1>
|
|
</div>
|
|
<div
|
|
class="css-o0040c-View-FlexBox-FlexRow"
|
|
id="detailsSidebar"
|
|
/>
|
|
</div>
|
|
</body>
|
|
`);
|
|
|
|
// Sending same link doesn't trigger again
|
|
act(() => {
|
|
store.dispatch(
|
|
selectPlugin({
|
|
selectedPlugin: definition.id,
|
|
deepLinkPayload: 'universe!',
|
|
selectedApp: client.query.app,
|
|
}),
|
|
);
|
|
});
|
|
await sleep(10);
|
|
expect(linksSeen).toEqual(['universe!']);
|
|
|
|
// ...nor does a random other store update that does trigger a plugin container render
|
|
act(() => {
|
|
store.dispatch(
|
|
updateSettings({
|
|
...store.getState().settingsState,
|
|
}),
|
|
);
|
|
});
|
|
expect(linksSeen).toEqual(['universe!']);
|
|
|
|
// Different link does trigger again
|
|
act(() => {
|
|
store.dispatch(
|
|
selectPlugin({
|
|
selectedPlugin: definition.id,
|
|
deepLinkPayload: 'london!',
|
|
selectedApp: client.query.app,
|
|
}),
|
|
);
|
|
});
|
|
await sleep(10);
|
|
expect(linksSeen).toEqual(['universe!', 'london!']);
|
|
|
|
// and same link does trigger if something else was selected in the mean time
|
|
act(() => {
|
|
store.dispatch(
|
|
selectPlugin({
|
|
selectedPlugin: 'Logs',
|
|
deepLinkPayload: 'london!',
|
|
selectedApp: client.query.app,
|
|
}),
|
|
);
|
|
});
|
|
act(() => {
|
|
store.dispatch(
|
|
selectPlugin({
|
|
selectedPlugin: definition.id,
|
|
deepLinkPayload: 'london!',
|
|
selectedApp: client.query.app,
|
|
}),
|
|
);
|
|
});
|
|
await sleep(10);
|
|
expect(linksSeen).toEqual(['universe!', 'london!', 'london!']);
|
|
});
|
|
|
|
test('PluginContainer can render Sandy device plugins', async () => {
|
|
let renders = 0;
|
|
|
|
function MySandyPlugin() {
|
|
renders++;
|
|
const sandyApi = usePlugin(devicePlugin);
|
|
expect(Object.keys(sandyApi)).toEqual([
|
|
'activatedStub',
|
|
'deactivatedStub',
|
|
'lastLogMessage',
|
|
]);
|
|
expect(() => {
|
|
// eslint-disable-next-line
|
|
usePlugin(function bla() {
|
|
return {};
|
|
});
|
|
}).toThrowError(/didn't match the type of the requested plugin/);
|
|
const lastLogMessage = useValue(sandyApi.lastLogMessage);
|
|
return <div>Hello from Sandy: {lastLogMessage?.message}</div>;
|
|
}
|
|
|
|
const devicePlugin = (client: DevicePluginClient) => {
|
|
const lastLogMessage = createState<undefined | DeviceLogEntry>(undefined);
|
|
const activatedStub = jest.fn();
|
|
const deactivatedStub = jest.fn();
|
|
client.onActivate(activatedStub);
|
|
client.onDeactivate(deactivatedStub);
|
|
client.device.onLogEntry((e) => {
|
|
lastLogMessage.set(e);
|
|
});
|
|
return {activatedStub, deactivatedStub, lastLogMessage};
|
|
};
|
|
|
|
const definition = new _SandyPluginDefinition(
|
|
TestUtils.createMockPluginDetails(),
|
|
{
|
|
supportsDevice: () => true,
|
|
devicePlugin,
|
|
Component: MySandyPlugin,
|
|
},
|
|
);
|
|
const {renderer, act, store, device} = await renderMockFlipperWithPlugin(
|
|
definition,
|
|
);
|
|
|
|
expect(renderer.baseElement).toMatchInlineSnapshot(`
|
|
<body>
|
|
<div>
|
|
<div
|
|
class="css-w6yhx2-View-FlexBox-FlexColumn"
|
|
>
|
|
<div>
|
|
Hello from Sandy:
|
|
</div>
|
|
</div>
|
|
<div
|
|
class="css-o0040c-View-FlexBox-FlexRow"
|
|
id="detailsSidebar"
|
|
/>
|
|
</div>
|
|
</body>
|
|
`);
|
|
expect(renders).toBe(1);
|
|
|
|
act(() => {
|
|
device.addLogEntry({
|
|
date: new Date(),
|
|
message: 'helleuh',
|
|
pid: 0,
|
|
tid: 0,
|
|
type: 'info',
|
|
tag: 'test',
|
|
});
|
|
});
|
|
expect(renders).toBe(2);
|
|
|
|
expect(renderer.baseElement).toMatchInlineSnapshot(`
|
|
<body>
|
|
<div>
|
|
<div
|
|
class="css-w6yhx2-View-FlexBox-FlexColumn"
|
|
>
|
|
<div>
|
|
Hello from Sandy:
|
|
helleuh
|
|
</div>
|
|
</div>
|
|
<div
|
|
class="css-o0040c-View-FlexBox-FlexRow"
|
|
id="detailsSidebar"
|
|
/>
|
|
</div>
|
|
</body>
|
|
`);
|
|
|
|
// make sure the plugin gets connected
|
|
const pluginInstance: ReturnType<
|
|
typeof devicePlugin
|
|
> = device.sandyPluginStates.get(definition.id)!.instanceApi;
|
|
expect(pluginInstance.activatedStub).toBeCalledTimes(1);
|
|
expect(pluginInstance.deactivatedStub).toBeCalledTimes(0);
|
|
|
|
// select non existing plugin
|
|
act(() => {
|
|
store.dispatch(
|
|
selectPlugin({
|
|
selectedPlugin: 'Logs',
|
|
deepLinkPayload: null,
|
|
}),
|
|
);
|
|
});
|
|
|
|
expect(renderer.baseElement).toMatchInlineSnapshot(`
|
|
<body>
|
|
<div />
|
|
</body>
|
|
`);
|
|
expect(pluginInstance.activatedStub).toBeCalledTimes(1);
|
|
expect(pluginInstance.deactivatedStub).toBeCalledTimes(1);
|
|
|
|
// go back
|
|
act(() => {
|
|
store.dispatch(
|
|
selectPlugin({
|
|
selectedPlugin: definition.id,
|
|
deepLinkPayload: null,
|
|
}),
|
|
);
|
|
});
|
|
expect(pluginInstance.activatedStub).toBeCalledTimes(2);
|
|
expect(pluginInstance.deactivatedStub).toBeCalledTimes(1);
|
|
});
|
|
|
|
test('PluginContainer + Sandy device plugin supports deeplink', async () => {
|
|
const linksSeen: any[] = [];
|
|
|
|
const devicePlugin = (client: DevicePluginClient) => {
|
|
const linkState = createState('');
|
|
client.onDeepLink((link) => {
|
|
linksSeen.push(link);
|
|
linkState.set(String(link));
|
|
});
|
|
return {
|
|
linkState,
|
|
};
|
|
};
|
|
|
|
const definition = new _SandyPluginDefinition(
|
|
TestUtils.createMockPluginDetails(),
|
|
{
|
|
devicePlugin,
|
|
supportsDevice: () => true,
|
|
Component() {
|
|
const instance = usePlugin(devicePlugin);
|
|
const linkState = useValue(instance.linkState);
|
|
return <h1>hello {linkState || 'world'}</h1>;
|
|
},
|
|
},
|
|
);
|
|
const {renderer, act, store} = await renderMockFlipperWithPlugin(definition);
|
|
|
|
const theUniverse = {
|
|
thisIs: 'theUniverse',
|
|
toString() {
|
|
return JSON.stringify({...this});
|
|
},
|
|
};
|
|
|
|
expect(linksSeen).toEqual([]);
|
|
expect(renderer.baseElement).toMatchInlineSnapshot(`
|
|
<body>
|
|
<div>
|
|
<div
|
|
class="css-w6yhx2-View-FlexBox-FlexColumn"
|
|
>
|
|
<h1>
|
|
hello
|
|
world
|
|
</h1>
|
|
</div>
|
|
<div
|
|
class="css-o0040c-View-FlexBox-FlexRow"
|
|
id="detailsSidebar"
|
|
/>
|
|
</div>
|
|
</body>
|
|
`);
|
|
|
|
act(() => {
|
|
store.dispatch(
|
|
selectPlugin({
|
|
selectedPlugin: definition.id,
|
|
deepLinkPayload: theUniverse,
|
|
selectedApp: null,
|
|
}),
|
|
);
|
|
});
|
|
|
|
await sleep(10);
|
|
expect(linksSeen).toEqual([theUniverse]);
|
|
expect(renderer.baseElement).toMatchInlineSnapshot(`
|
|
<body>
|
|
<div>
|
|
<div
|
|
class="css-w6yhx2-View-FlexBox-FlexColumn"
|
|
>
|
|
<h1>
|
|
hello
|
|
{"thisIs":"theUniverse"}
|
|
</h1>
|
|
</div>
|
|
<div
|
|
class="css-o0040c-View-FlexBox-FlexRow"
|
|
id="detailsSidebar"
|
|
/>
|
|
</div>
|
|
</body>
|
|
`);
|
|
|
|
// Sending same link doesn't trigger again
|
|
act(() => {
|
|
store.dispatch(
|
|
selectPlugin({
|
|
selectedPlugin: definition.id,
|
|
deepLinkPayload: theUniverse,
|
|
selectedApp: null,
|
|
}),
|
|
);
|
|
});
|
|
await sleep(10);
|
|
expect(linksSeen).toEqual([theUniverse]);
|
|
|
|
// ...nor does a random other store update that does trigger a plugin container render
|
|
act(() => {
|
|
store.dispatch(
|
|
updateSettings({
|
|
...store.getState().settingsState,
|
|
}),
|
|
);
|
|
});
|
|
expect(linksSeen).toEqual([theUniverse]);
|
|
|
|
// Different link does trigger again
|
|
act(() => {
|
|
store.dispatch(
|
|
selectPlugin({
|
|
selectedPlugin: definition.id,
|
|
deepLinkPayload: 'london!',
|
|
selectedApp: null,
|
|
}),
|
|
);
|
|
});
|
|
await sleep(10);
|
|
expect(linksSeen).toEqual([theUniverse, 'london!']);
|
|
|
|
// and same link does trigger if something else was selected in the mean time
|
|
act(() => {
|
|
store.dispatch(
|
|
selectPlugin({
|
|
selectedPlugin: 'Logs',
|
|
deepLinkPayload: 'london!',
|
|
selectedApp: null,
|
|
}),
|
|
);
|
|
});
|
|
act(() => {
|
|
store.dispatch(
|
|
selectPlugin({
|
|
selectedPlugin: definition.id,
|
|
deepLinkPayload: 'london!',
|
|
selectedApp: null,
|
|
}),
|
|
);
|
|
});
|
|
await sleep(10);
|
|
expect(linksSeen).toEqual([theUniverse, 'london!', 'london!']);
|
|
});
|
|
|
|
test('Sandy plugins support isPluginSupported + selectPlugin', async () => {
|
|
let renders = 0;
|
|
const linksSeen: any[] = [];
|
|
|
|
function MySandyPlugin() {
|
|
renders++;
|
|
return <h1>Plugin1</h1>;
|
|
}
|
|
|
|
const plugin = (client: PluginClient) => {
|
|
const activatedStub = jest.fn();
|
|
const deactivatedStub = jest.fn();
|
|
client.onDeepLink((link) => {
|
|
linksSeen.push(link);
|
|
});
|
|
client.onActivate(activatedStub);
|
|
client.onDeactivate(deactivatedStub);
|
|
return {
|
|
activatedStub,
|
|
deactivatedStub,
|
|
isPluginAvailable: client.isPluginAvailable,
|
|
selectPlugin: client.selectPlugin,
|
|
};
|
|
};
|
|
|
|
const definition = new _SandyPluginDefinition(
|
|
TestUtils.createMockPluginDetails({id: 'base'}),
|
|
{
|
|
plugin,
|
|
Component: MySandyPlugin,
|
|
},
|
|
);
|
|
const definition2 = new _SandyPluginDefinition(
|
|
TestUtils.createMockPluginDetails({id: 'other'}),
|
|
{
|
|
plugin() {
|
|
return {};
|
|
},
|
|
Component() {
|
|
return <h1>Plugin2</h1>;
|
|
},
|
|
},
|
|
);
|
|
const definition3 = new _SandyPluginDefinition(
|
|
TestUtils.createMockPluginDetails({id: 'device'}),
|
|
{
|
|
supportsDevice() {
|
|
return true;
|
|
},
|
|
devicePlugin() {
|
|
return {};
|
|
},
|
|
Component() {
|
|
return <h1>Plugin3</h1>;
|
|
},
|
|
},
|
|
);
|
|
const {renderer, client, store} = await renderMockFlipperWithPlugin(
|
|
definition,
|
|
{
|
|
additionalPlugins: [definition2, definition3],
|
|
dontEnableAdditionalPlugins: true,
|
|
},
|
|
);
|
|
|
|
expect(renderer.baseElement.querySelector('h1')).toMatchInlineSnapshot(`
|
|
<h1>
|
|
Plugin1
|
|
</h1>
|
|
`);
|
|
expect(renders).toBe(1);
|
|
|
|
const pluginInstance: ReturnType<
|
|
typeof plugin
|
|
> = client.sandyPluginStates.get(definition.id)!.instanceApi;
|
|
expect(pluginInstance.isPluginAvailable(definition.id)).toBeTruthy();
|
|
expect(pluginInstance.isPluginAvailable('nonsense')).toBeFalsy();
|
|
expect(pluginInstance.isPluginAvailable(definition2.id)).toBeFalsy(); // not enabled yet
|
|
expect(pluginInstance.isPluginAvailable(definition3.id)).toBeFalsy(); // not enabled yet
|
|
expect(pluginInstance.activatedStub).toBeCalledTimes(1);
|
|
expect(pluginInstance.deactivatedStub).toBeCalledTimes(0);
|
|
expect(linksSeen).toEqual([]);
|
|
|
|
// star and navigate to a device plugin
|
|
store.dispatch(switchPlugin({plugin: definition3}));
|
|
pluginInstance.selectPlugin(definition3.id);
|
|
expect(pluginInstance.isPluginAvailable(definition3.id)).toBeTruthy();
|
|
expect(store.getState().connections.selectedPlugin).toBe(definition3.id);
|
|
expect(renderer.baseElement.querySelector('h1')).toMatchInlineSnapshot(`
|
|
<h1>
|
|
Plugin3
|
|
</h1>
|
|
`);
|
|
expect(pluginInstance.deactivatedStub).toBeCalledTimes(1);
|
|
|
|
// go back by opening own plugin again (funny, but why not)
|
|
pluginInstance.selectPlugin(definition.id, 'data');
|
|
expect(store.getState().connections.selectedPlugin).toBe(definition.id);
|
|
expect(pluginInstance.activatedStub).toBeCalledTimes(2);
|
|
await sleep(10);
|
|
expect(renderer.baseElement.querySelector('h1')).toMatchInlineSnapshot(`
|
|
<h1>
|
|
Plugin1
|
|
</h1>
|
|
`);
|
|
expect(linksSeen).toEqual(['data']);
|
|
|
|
// try to go to plugin 2, fails (not enabled, so no-op)
|
|
pluginInstance.selectPlugin(definition2.id);
|
|
expect(store.getState().connections.selectedPlugin).toBe(definition.id);
|
|
|
|
// star plugin 2 and navigate to plugin 2
|
|
store.dispatch(
|
|
switchPlugin({
|
|
plugin: definition2,
|
|
selectedApp: client.query.app,
|
|
}),
|
|
);
|
|
pluginInstance.selectPlugin(definition2.id);
|
|
expect(store.getState().connections.selectedPlugin).toBe(definition2.id);
|
|
expect(pluginInstance.deactivatedStub).toBeCalledTimes(2);
|
|
expect(renderer.baseElement.querySelector('h1')).toMatchInlineSnapshot(`
|
|
<h1>
|
|
Plugin2
|
|
</h1>
|
|
`);
|
|
expect(renders).toBe(2);
|
|
});
|