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:
committed by
Facebook Github Bot
parent
3730c523ec
commit
04e12a28a0
@@ -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(
|
||||
|
||||
52
src/chrome/__tests__/PluginInstaller.node.tsx
Normal file
52
src/chrome/__tests__/PluginInstaller.node.tsx
Normal 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();
|
||||
});
|
||||
238
src/chrome/__tests__/__snapshots__/PluginInstaller.node.tsx.snap
Normal file
238
src/chrome/__tests__/__snapshots__/PluginInstaller.node.tsx.snap
Normal 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>
|
||||
`;
|
||||
Reference in New Issue
Block a user