Refactor PluginInstaller component

Summary:
- Made side-effecting elements injectable via props.
- Added default props so usage doesn't change.
- Added testing-library/react snapshot test that waits for test data to appear in the list.

One slightly annoying part here is that this now that we have an `autoHeight` prop which is only useful for testing it as it prevents a problem with a height-detection in the test runner. We could even change the default as it doesn't affect the display in prod, but this still feels slightly cleaner.

Reviewed By: jknoxville

Differential Revision: D17808510

fbshipit-source-id: 2ae70886c58282d5bdc98ba4215e8248e4c7f159
This commit is contained in:
Pascal Hartig
2019-10-08 08:43:23 -07:00
committed by Facebook Github Bot
parent 3730c523ec
commit 04e12a28a0
3 changed files with 328 additions and 16 deletions

View File

@@ -41,7 +41,7 @@ const PluginManager = new PM({
ignoredDependencies: ['flipper', 'react', 'react-dom', '@types/*'],
});
type PluginDefinition = {
export type PluginDefinition = {
name: string;
version: string;
description: string;
@@ -92,10 +92,31 @@ const RestartBar = styled(FlexColumn)({
textAlign: 'center',
});
export default function() {
type Props = {
searchIndexFactory: () => algoliasearch.Index;
getInstalledPlugins: () => Promise<Map<string, PluginDefinition>>;
autoHeight: boolean;
};
const defaultProps: Props = {
searchIndexFactory: () => {
const client = algoliasearch(ALGOLIA_APPLICATION_ID, ALGOLIA_API_KEY);
return client.initIndex('npm-search');
},
getInstalledPlugins: _getInstalledPlugins,
autoHeight: false,
};
const PluginInstaller = function props(props: Props) {
const [restartRequired, setRestartRequired] = useState(false);
const [query, setQuery] = useState('');
const rows = useNPMSearch(setRestartRequired, query, setQuery);
const rows = useNPMSearch(
setRestartRequired,
query,
setQuery,
props.searchIndexFactory,
props.getInstalledPlugins,
);
const restartApp = useCallback(() => {
remote.app.relaunch();
remote.app.exit();
@@ -126,11 +147,14 @@ export default function() {
columns={columns}
highlightableRows={false}
highlightedRows={new Set()}
autoHeight={props.autoHeight}
rows={rows}
/>
</Container>
);
}
};
PluginInstaller.defaultProps = defaultProps;
export default PluginInstaller;
const TableButton = styled(Button)({
marginTop: 2,
@@ -248,29 +272,27 @@ function useNPMSearch(
setRestartRequired: (restart: boolean) => void,
query: string,
setQuery: (query: string) => void,
searchClientFactory: () => algoliasearch.Index,
getInstalledPlugins: () => Promise<Map<string, PluginDefinition>>,
): TableRows_immutable {
const index = useMemo(() => {
const client = algoliasearch(ALGOLIA_APPLICATION_ID, ALGOLIA_API_KEY);
return client.initIndex('npm-search');
}, []);
const index = useMemo(searchClientFactory, []);
const [installedPlugins, setInstalledPlugins] = useState(
new Map<string, PluginDefinition>(),
);
useEffect(() => {
reportUsage(`${TAG}:open`);
const getAndSetInstalledPlugins = () =>
reportPlatformFailures(
getInstalledPlugins(),
`${TAG}:getInstalledPlugins`,
).then(setInstalledPlugins);
useEffect(() => {
reportUsage(`${TAG}:open`);
getAndSetInstalledPlugins();
}, []);
const onInstall = useCallback(async () => {
reportPlatformFailures(
getInstalledPlugins(),
`${TAG}:getInstalledPlugins`,
).then(setInstalledPlugins);
getAndSetInstalledPlugins();
setRestartRequired(true);
}, []);
@@ -336,7 +358,7 @@ function useNPMSearch(
return List(results.map(createRow));
}
async function getInstalledPlugins() {
async function _getInstalledPlugins(): Promise<Map<string, PluginDefinition>> {
const dirs = await fs.readdir(PLUGIN_DIR);
const plugins = await Promise.all<[string, PluginDefinition]>(
dirs.map(

View File

@@ -0,0 +1,52 @@
/**
* Copyright 2018-present Facebook.
* 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 PluginInstaller, PluginDefinition} from '../PluginInstaller';
import React from 'react';
import {render, waitForElement} from '@testing-library/react';
import {init as initLogger} from '../../fb-stubs/Logger';
import configureStore from 'redux-mock-store';
const mockStore = configureStore([])({application: {sessionId: 'mysession'}});
import {Provider} from 'react-redux';
const SEARCH_RESULTS = ({
hits: [
{name: 'flipper-plugin-hello', version: '0.1.0', description: 'World?'},
{name: 'flipper-plugin-world', version: '0.2.0', description: 'Hello?'},
],
} as unknown) as algoliasearch.Response<any>;
// *Very* incomplete mock, but that's all we use.
const indexMock: algoliasearch.Index = ({
search: jest.fn(),
} as unknown) as algoliasearch.Index;
beforeEach(() => {
indexMock.search = jest.fn(async () => SEARCH_RESULTS);
initLogger(mockStore as any, {isTest: true});
});
test('load PluginInstaller list', async () => {
const component = (
<Provider store={mockStore}>
<PluginInstaller
getInstalledPlugins={async () => new Map<string, PluginDefinition>()}
searchIndexFactory={() => indexMock}
// Bit ugly to have this as an effectively test-only option, but
// without, we rely on height information from Electron which we don't
// have, causing no items to be rendered.
autoHeight={true}
/>
</Provider>
);
const {container, getByText} = render(component);
await waitForElement(() => getByText('flipper-plugin-hello'));
expect((indexMock.search as jest.Mock).mock.calls.length).toBe(2);
expect(container).toMatchSnapshot();
});

View File

@@ -0,0 +1,238 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`load PluginInstaller list 1`] = `
<div>
<div
class="css-1nm3y77"
>
<div
class="css-4zga7z"
>
<div
class="css-e0frhe"
>
<input
class="css-ww9vf1"
placeholder="Search Flipper plugins..."
type="text"
value=""
/>
</div>
</div>
<div
class="css-1nekdsz"
>
<div
class="css-1k4677w"
>
<div
class="css-vd30c4"
>
<div
class="css-1vb6sy6"
title="name"
width="25%"
>
<div
class="css-glr1wj"
style="z-index: auto; right: 0px; bottom: 0px; width: 100%; height: 100%;"
>
<div
class="css-1db3q1"
>
Name
</div>
</div>
</div>
<div
class="css-1xchw24"
title="version"
width="10%"
>
<div
class="css-glr1wj"
style="z-index: auto; right: 0px; bottom: 0px; width: 100%; height: 100%;"
>
<div
class="css-1db3q1"
>
Version
</div>
</div>
</div>
<div
class="css-173sh01"
title="description"
width="flex"
>
<div
class="css-glr1wj"
style="z-index: auto; right: 0px; bottom: 0px; width: 100%; height: 100%;"
>
<div
class="css-1db3q1"
>
Description
</div>
</div>
</div>
<div
class="css-f0p3z5"
title="install"
width="15%"
>
<div
class="css-glr1wj"
style="z-index: auto; right: 0px; bottom: 0px; width: 100%; height: 100%;"
>
<div
class="css-1db3q1"
>
</div>
</div>
</div>
</div>
</div>
<div
class="css-1nekdsz"
>
<div
class="css-1jjayz9"
data-key="flipper-plugin-hello"
>
<div
class="css-1jes4os"
title=""
width="25%"
>
<span
class="css-1pso8q0"
>
flipper-plugin-hello
</span>
</div>
<div
class="css-1kkefm9"
title=""
width="10%"
>
<span
class="css-1pso8q0"
>
0.1.0
</span>
</div>
<div
class="css-v10b1x"
title=""
width="flex"
>
<div
class="css-9qtipk"
>
<span
class="css-1pso8q0"
>
World?
</span>
<div
class="css-12zzrdt"
/>
<span
class="css-1b8mb9l"
>
<div
class="css-17bs008"
color="#bec2c9"
size="16"
src="https://external.xx.fbcdn.net/assets/?name=info-circle&variant=filled&size=16&set=facebook_icons&density=1x"
/>
</span>
</div>
</div>
<div
class="css-yt1ntn"
title=""
width="15%"
>
<div
class="css-z1ci4f"
type="primary"
>
Install
</div>
</div>
</div>
<div
class="css-1ut44ht"
data-key="flipper-plugin-world"
>
<div
class="css-1jes4os"
title=""
width="25%"
>
<span
class="css-1pso8q0"
>
flipper-plugin-world
</span>
</div>
<div
class="css-1kkefm9"
title=""
width="10%"
>
<span
class="css-1pso8q0"
>
0.2.0
</span>
</div>
<div
class="css-v10b1x"
title=""
width="flex"
>
<div
class="css-9qtipk"
>
<span
class="css-1pso8q0"
>
Hello?
</span>
<div
class="css-12zzrdt"
/>
<span
class="css-1b8mb9l"
>
<div
class="css-17bs008"
color="#bec2c9"
size="16"
src="https://external.xx.fbcdn.net/assets/?name=info-circle&variant=filled&size=16&set=facebook_icons&density=1x"
/>
</span>
</div>
</div>
<div
class="css-yt1ntn"
title=""
width="15%"
>
<div
class="css-z1ci4f"
type="primary"
>
Install
</div>
</div>
</div>
</div>
</div>
</div>
</div>
`;