From 04e12a28a0dc49dbb9fd8b322deaab1bf51b137c Mon Sep 17 00:00:00 2001 From: Pascal Hartig Date: Tue, 8 Oct 2019 08:43:23 -0700 Subject: [PATCH] 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 --- src/chrome/PluginInstaller.tsx | 54 ++-- src/chrome/__tests__/PluginInstaller.node.tsx | 52 ++++ .../PluginInstaller.node.tsx.snap | 238 ++++++++++++++++++ 3 files changed, 328 insertions(+), 16 deletions(-) create mode 100644 src/chrome/__tests__/PluginInstaller.node.tsx create mode 100644 src/chrome/__tests__/__snapshots__/PluginInstaller.node.tsx.snap diff --git a/src/chrome/PluginInstaller.tsx b/src/chrome/PluginInstaller.tsx index 5c0b46f4f..84e9d32d7 100644 --- a/src/chrome/PluginInstaller.tsx +++ b/src/chrome/PluginInstaller.tsx @@ -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>; + 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} /> ); -} +}; +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>, ): 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(), ); - 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> { const dirs = await fs.readdir(PLUGIN_DIR); const plugins = await Promise.all<[string, PluginDefinition]>( dirs.map( diff --git a/src/chrome/__tests__/PluginInstaller.node.tsx b/src/chrome/__tests__/PluginInstaller.node.tsx new file mode 100644 index 000000000..49f01efc6 --- /dev/null +++ b/src/chrome/__tests__/PluginInstaller.node.tsx @@ -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; + +// *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 = ( + + new Map()} + 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} + /> + + ); + 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(); +}); diff --git a/src/chrome/__tests__/__snapshots__/PluginInstaller.node.tsx.snap b/src/chrome/__tests__/__snapshots__/PluginInstaller.node.tsx.snap new file mode 100644 index 000000000..e70ce1f0b --- /dev/null +++ b/src/chrome/__tests__/__snapshots__/PluginInstaller.node.tsx.snap @@ -0,0 +1,238 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`load PluginInstaller list 1`] = ` +
+
+
+
+ +
+
+
+
+
+
+
+
+ Name +
+
+
+
+
+
+ Version +
+
+
+
+
+
+ Description +
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+ + flipper-plugin-hello + +
+
+ + 0.1.0 + +
+
+
+ + World? + +
+ +
+ +
+
+
+
+ Install +
+
+
+
+
+ + flipper-plugin-world + +
+
+ + 0.2.0 + +
+
+
+ + Hello? + +
+ +
+ +
+
+
+
+ Install +
+
+
+
+
+
+
+`;