Skip to content

Commit

Permalink
[Workplace Search] Add tests for remaining Sources components (#89026)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
scottybollinger authored Jan 21, 2021
1 parent c495093 commit 4281a34
Show file tree
Hide file tree
Showing 9 changed files with 337 additions and 34 deletions.
Original file line number Diff line number Diff line change
@@ -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));
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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 }),
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
]);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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 />;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -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);
Expand All @@ -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;
Expand Down Expand Up @@ -235,6 +241,14 @@ export const SourcesLogic = kea<MakeLogicType<ISourcesValues, ISourcesActions>>(
resetFlashMessages: () => {
clearFlashMessages();
},
resetSourcesState: () => {
clearInterval(pollingInterval);
},
}),
events: () => ({
beforeUnmount() {
clearInterval(pollingInterval);
},
}),
});

Expand Down
Original file line number Diff line number Diff line change
@@ -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);
});
});
Loading

0 comments on commit 4281a34

Please sign in to comment.