[Workplace Search] Add tests for remaining Sources components (#89026)

* Remove history params

We already replace the history.push functionality with KibanaLogic.values.navigateToUrl but the history object was still being passed around.

* Add org sources container tests

* Add tests for source router

* Clean up leftover history imports

* Add tests for SourcesRouter

* Quick refactor for cleaner existence check

Optional chaining FTW

* Refactor to simplify setInterval logic

This commit does a refactor to move the logic for polling for status to the logic file. In doing this I realized that we were intializing sources in the SourcesView, when we are actually already initializing sources in the components that use this, which are OrganizationSources and PrivateSources, the top-level containers.

Because of this, I was able to remove the useEffect entireley, as the flash messages are cleared between page transitions in Kibana and the initialization of the sources ahppens in the containers.

* Add tests for SourcesView

* Fix type issue
This commit is contained in:
Scotty Bollinger 2021-01-21 17:32:18 -06:00 committed by GitHub
parent c495093f76
commit 4281a347c6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 337 additions and 34 deletions

View file

@ -0,0 +1,63 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import '../../../__mocks__/shallow_useeffect.mock';
import { setMockValues, setMockActions } from '../../../__mocks__';
import { shallow } from 'enzyme';
import React from 'react';
import { Redirect } from 'react-router-dom';
import { contentSources } from '../../__mocks__/content_sources.mock';
import { Loading } from '../../../shared/loading';
import { SourcesTable } from '../../components/shared/sources_table';
import { ViewContentHeader } from '../../components/shared/view_content_header';
import { ADD_SOURCE_PATH, getSourcesPath } from '../../routes';
import { OrganizationSources } from './organization_sources';
describe('OrganizationSources', () => {
const initializeSources = jest.fn();
const setSourceSearchability = jest.fn();
const mockValues = {
contentSources,
dataLoading: false,
};
beforeEach(() => {
setMockActions({
initializeSources,
setSourceSearchability,
});
setMockValues({ ...mockValues });
});
it('renders', () => {
const wrapper = shallow(<OrganizationSources />);
expect(wrapper.find(SourcesTable)).toHaveLength(1);
expect(wrapper.find(ViewContentHeader)).toHaveLength(1);
});
it('returns loading when loading', () => {
setMockValues({ ...mockValues, dataLoading: true });
const wrapper = shallow(<OrganizationSources />);
expect(wrapper.find(Loading)).toHaveLength(1);
});
it('returns redirect when no sources', () => {
setMockValues({ ...mockValues, contentSources: [] });
const wrapper = shallow(<OrganizationSources />);
expect(wrapper.find(Redirect).prop('to')).toEqual(getSourcesPath(ADD_SOURCE_PATH, true));
});
});

View file

@ -27,10 +27,11 @@ const ORG_HEADER_DESCRIPTION =
'Organization sources are available to the entire organization and can be assigned to specific user groups.';
export const OrganizationSources: React.FC = () => {
const { initializeSources, setSourceSearchability } = useActions(SourcesLogic);
const { initializeSources, setSourceSearchability, resetSourcesState } = useActions(SourcesLogic);
useEffect(() => {
initializeSources();
return resetSourcesState;
}, []);
const { dataLoading, contentSources } = useValues(SourcesLogic);

View file

@ -39,7 +39,7 @@ export interface SourceActions {
): { sourceId: string; source: { name: string } };
resetSourceState(): void;
removeContentSource(sourceId: string): { sourceId: string };
initializeSource(sourceId: string, history: object): { sourceId: string; history: object };
initializeSource(sourceId: string): { sourceId: string };
getSourceConfigData(serviceType: string): { serviceType: string };
setButtonNotLoading(): void;
}
@ -88,7 +88,7 @@ export const SourceLogic = kea<MakeLogicType<SourceValues, SourceActions>>({
setSearchResults: (searchResultsResponse: SearchResultsResponse) => searchResultsResponse,
setContentFilterValue: (contentFilterValue: string) => contentFilterValue,
setActivePage: (activePage: number) => activePage,
initializeSource: (sourceId: string, history: object) => ({ sourceId, history }),
initializeSource: (sourceId: string) => ({ sourceId }),
initializeFederatedSummary: (sourceId: string) => ({ sourceId }),
searchContentSourceDocuments: (sourceId: string) => ({ sourceId }),
updateContentSource: (sourceId: string, source: { name: string }) => ({ sourceId, source }),

View file

@ -0,0 +1,120 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import '../../../__mocks__/shallow_useeffect.mock';
import { setMockValues, setMockActions } from '../../../__mocks__';
import React from 'react';
import { shallow } from 'enzyme';
import { useParams } from 'react-router-dom';
import { Route, Switch } from 'react-router-dom';
import { contentSources } from '../../__mocks__/content_sources.mock';
import { SetWorkplaceSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome';
import { NAV } from '../../constants';
import { Loading } from '../../../shared/loading';
import { DisplaySettingsRouter } from './components/display_settings';
import { Overview } from './components/overview';
import { Schema } from './components/schema';
import { SchemaChangeErrors } from './components/schema/schema_change_errors';
import { SourceContent } from './components/source_content';
import { SourceSettings } from './components/source_settings';
import { SourceRouter } from './source_router';
describe('SourceRouter', () => {
const initializeSource = jest.fn();
const contentSource = contentSources[1];
const customSource = contentSources[0];
const mockValues = {
contentSource,
dataLoading: false,
};
beforeEach(() => {
setMockActions({
initializeSource,
});
setMockValues({ ...mockValues });
(useParams as jest.Mock).mockImplementationOnce(() => ({
sourceId: '1',
}));
});
it('returns Loading when loading', () => {
setMockValues({ ...mockValues, dataLoading: true });
const wrapper = shallow(<SourceRouter />);
expect(wrapper.find(Loading)).toHaveLength(1);
});
it('renders source routes (standard)', () => {
const wrapper = shallow(<SourceRouter />);
expect(wrapper.find(Overview)).toHaveLength(1);
expect(wrapper.find(SourceSettings)).toHaveLength(1);
expect(wrapper.find(SourceContent)).toHaveLength(1);
expect(wrapper.find(Switch)).toHaveLength(1);
expect(wrapper.find(Route)).toHaveLength(3);
});
it('renders source routes (custom)', () => {
setMockValues({ ...mockValues, contentSource: customSource });
const wrapper = shallow(<SourceRouter />);
expect(wrapper.find(DisplaySettingsRouter)).toHaveLength(1);
expect(wrapper.find(Schema)).toHaveLength(1);
expect(wrapper.find(SchemaChangeErrors)).toHaveLength(1);
expect(wrapper.find(Route)).toHaveLength(6);
});
it('handles breadcrumbs while loading (standard)', () => {
setMockValues({
...mockValues,
contentSource: {},
});
const loadingBreadcrumbs = ['Sources', '...'];
const wrapper = shallow(<SourceRouter />);
const overviewBreadCrumb = wrapper.find(SetPageChrome).at(0);
const contentBreadCrumb = wrapper.find(SetPageChrome).at(1);
const settingsBreadCrumb = wrapper.find(SetPageChrome).at(2);
expect(overviewBreadCrumb.prop('trail')).toEqual([...loadingBreadcrumbs, NAV.OVERVIEW]);
expect(contentBreadCrumb.prop('trail')).toEqual([...loadingBreadcrumbs, NAV.CONTENT]);
expect(settingsBreadCrumb.prop('trail')).toEqual([...loadingBreadcrumbs, NAV.SETTINGS]);
});
it('handles breadcrumbs while loading (custom)', () => {
setMockValues({
...mockValues,
contentSource: { serviceType: 'custom' },
});
const loadingBreadcrumbs = ['Sources', '...'];
const wrapper = shallow(<SourceRouter />);
const schemaBreadCrumb = wrapper.find(SetPageChrome).at(2);
const schemaErrorsBreadCrumb = wrapper.find(SetPageChrome).at(3);
const displaySettingsBreadCrumb = wrapper.find(SetPageChrome).at(4);
expect(schemaBreadCrumb.prop('trail')).toEqual([...loadingBreadcrumbs, NAV.SCHEMA]);
expect(schemaErrorsBreadCrumb.prop('trail')).toEqual([...loadingBreadcrumbs, NAV.SCHEMA]);
expect(displaySettingsBreadCrumb.prop('trail')).toEqual([
...loadingBreadcrumbs,
NAV.DISPLAY_SETTINGS,
]);
});
});

View file

@ -6,10 +6,9 @@
import React, { useEffect } from 'react';
import { History } from 'history';
import { useActions, useValues } from 'kea';
import moment from 'moment';
import { Route, Switch, useHistory, useParams } from 'react-router-dom';
import { Route, Switch, useParams } from 'react-router-dom';
import { EuiButton, EuiCallOut, EuiHorizontalRule, EuiSpacer } from '@elastic/eui';
@ -46,14 +45,13 @@ import { SourceInfoCard } from './components/source_info_card';
import { SourceSettings } from './components/source_settings';
export const SourceRouter: React.FC = () => {
const history = useHistory() as History;
const { sourceId } = useParams() as { sourceId: string };
const { initializeSource } = useActions(SourceLogic);
const { contentSource, dataLoading } = useValues(SourceLogic);
const { isOrganization } = useValues(AppLogic);
useEffect(() => {
initializeSource(sourceId, history);
initializeSource(sourceId);
}, []);
if (dataLoading) return <Loading />;

View file

@ -77,6 +77,9 @@ interface ISourcesServerResponse {
serviceTypes: Connector[];
}
let pollingInterval: number;
const POLLING_INTERVAL = 10000;
export const SourcesLogic = kea<MakeLogicType<ISourcesValues, ISourcesActions>>({
path: ['enterprise_search', 'workplace_search', 'sources_logic'],
actions: {
@ -169,6 +172,7 @@ export const SourcesLogic = kea<MakeLogicType<ISourcesValues, ISourcesActions>>(
try {
const response = await HttpLogic.values.http.get(route);
actions.pollForSourceStatusChanges();
actions.onInitializeSources(response);
} catch (e) {
flashAPIErrors(e);
@ -181,18 +185,20 @@ export const SourcesLogic = kea<MakeLogicType<ISourcesValues, ISourcesActions>>(
}
},
// We poll the server and if the status update, we trigger a new fetch of the sources.
pollForSourceStatusChanges: async () => {
pollForSourceStatusChanges: () => {
const { isOrganization } = AppLogic.values;
if (!isOrganization) return;
const serverStatuses = values.serverStatuses;
const sourceStatuses = await fetchSourceStatuses(isOrganization);
pollingInterval = window.setInterval(async () => {
const sourceStatuses = await fetchSourceStatuses(isOrganization);
sourceStatuses.some((source: ContentSourceStatus) => {
if (serverStatuses && serverStatuses[source.id] !== source.status.status) {
return actions.initializeSources();
}
});
sourceStatuses.some((source: ContentSourceStatus) => {
if (serverStatuses && serverStatuses[source.id] !== source.status.status) {
return actions.initializeSources();
}
});
}, POLLING_INTERVAL);
},
setSourceSearchability: async ({ sourceId, searchable }) => {
const { isOrganization } = AppLogic.values;
@ -235,6 +241,14 @@ export const SourcesLogic = kea<MakeLogicType<ISourcesValues, ISourcesActions>>(
resetFlashMessages: () => {
clearFlashMessages();
},
resetSourcesState: () => {
clearInterval(pollingInterval);
},
}),
events: () => ({
beforeUnmount() {
clearInterval(pollingInterval);
},
}),
});

View file

@ -0,0 +1,60 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import '../../../__mocks__/shallow_useeffect.mock';
import { setMockValues, setMockActions } from '../../../__mocks__';
import React from 'react';
import { shallow } from 'enzyme';
import { Route, Switch, Redirect } from 'react-router-dom';
import { ADD_SOURCE_PATH, PERSONAL_SOURCES_PATH, SOURCES_PATH, getSourcesPath } from '../../routes';
import { SourcesRouter } from './sources_router';
describe('SourcesRouter', () => {
const resetSourcesState = jest.fn();
const mockValues = {
account: { canCreatePersonalSources: true },
isOrganization: true,
hasPlatinumLicense: true,
};
beforeEach(() => {
setMockActions({
resetSourcesState,
});
setMockValues({ ...mockValues });
});
it('renders sources routes', () => {
const TOTAL_ROUTES = 62;
const wrapper = shallow(<SourcesRouter />);
expect(wrapper.find(Switch)).toHaveLength(1);
expect(wrapper.find(Route)).toHaveLength(TOTAL_ROUTES);
});
it('redirects when nonplatinum license and accountOnly context', () => {
setMockValues({ ...mockValues, hasPlatinumLicense: false });
const wrapper = shallow(<SourcesRouter />);
expect(wrapper.find(Redirect).first().prop('from')).toEqual(ADD_SOURCE_PATH);
expect(wrapper.find(Redirect).first().prop('to')).toEqual(SOURCES_PATH);
});
it('redirects when cannot create sources', () => {
setMockValues({ ...mockValues, account: { canCreatePersonalSources: false } });
const wrapper = shallow(<SourcesRouter />);
expect(wrapper.find(Redirect).last().prop('from')).toEqual(
getSourcesPath(ADD_SOURCE_PATH, false)
);
expect(wrapper.find(Redirect).last().prop('to')).toEqual(PERSONAL_SOURCES_PATH);
});
});

View file

@ -0,0 +1,64 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import '../../../__mocks__/shallow_useeffect.mock';
import { setMockValues, setMockActions } from '../../../__mocks__';
import { shallow } from 'enzyme';
import React from 'react';
import { EuiModal } from '@elastic/eui';
import { Loading } from '../../../shared/loading';
import { SourcesView } from './sources_view';
describe('SourcesView', () => {
const resetPermissionsModal = jest.fn();
const permissionsModal = {
addedSourceName: 'mySource',
serviceType: 'jira',
additionalConfiguration: true,
};
const mockValues = {
permissionsModal,
dataLoading: false,
};
const children = <p data-test-subj="TestChildren">test</p>;
beforeEach(() => {
setMockActions({
resetPermissionsModal,
});
setMockValues({ ...mockValues });
});
it('renders', () => {
const wrapper = shallow(<SourcesView>{children}</SourcesView>);
expect(wrapper.find('PermissionsModal')).toHaveLength(1);
expect(wrapper.find('[data-test-subj="TestChildren"]')).toHaveLength(1);
});
it('returns loading when loading', () => {
setMockValues({ ...mockValues, dataLoading: true });
const wrapper = shallow(<SourcesView>{children}</SourcesView>);
expect(wrapper.find(Loading)).toHaveLength(1);
});
it('calls function on modal close', () => {
const wrapper = shallow(<SourcesView>{children}</SourcesView>);
const modal = wrapper.find('PermissionsModal').dive().find(EuiModal);
modal.prop('onClose')();
expect(resetPermissionsModal).toHaveBeenCalled();
});
});

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React, { useEffect } from 'react';
import React from 'react';
import { useActions, useValues } from 'kea';
@ -22,8 +22,6 @@ import {
EuiText,
} from '@elastic/eui';
import { clearFlashMessages } from '../../../shared/flash_messages';
import { Loading } from '../../../shared/loading';
import { SourceIcon } from '../../components/shared/source_icon';
@ -31,29 +29,14 @@ import { EXTERNAL_IDENTITIES_DOCS_URL, DOCUMENT_PERMISSIONS_DOCS_URL } from '../
import { SourcesLogic } from './sources_logic';
const POLLING_INTERVAL = 10000;
interface SourcesViewProps {
children: React.ReactNode;
}
export const SourcesView: React.FC<SourcesViewProps> = ({ children }) => {
const { initializeSources, pollForSourceStatusChanges, resetPermissionsModal } = useActions(
SourcesLogic
);
const { resetPermissionsModal } = useActions(SourcesLogic);
const { dataLoading, permissionsModal } = useValues(SourcesLogic);
useEffect(() => {
initializeSources();
const pollingInterval = window.setInterval(pollForSourceStatusChanges, POLLING_INTERVAL);
return () => {
clearFlashMessages();
clearInterval(pollingInterval);
};
}, []);
if (dataLoading) return <Loading />;
const PermissionsModal = ({
@ -113,7 +96,7 @@ export const SourcesView: React.FC<SourcesViewProps> = ({ children }) => {
return (
<>
{!!permissionsModal && permissionsModal.additionalConfiguration && (
{permissionsModal?.additionalConfiguration && (
<PermissionsModal
addedSourceName={permissionsModal.addedSourceName}
serviceType={permissionsModal.serviceType}