diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/empty_states.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/empty_states.test.tsx
new file mode 100644
index 0000000000000..e75970404dc5e
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/empty_states.test.tsx
@@ -0,0 +1,62 @@
+/*
+ * 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 '../../../test_utils/mock_shallow_usecontext';
+
+import React from 'react';
+import { shallow } from 'enzyme';
+import { EuiEmptyPrompt, EuiCode, EuiLoadingContent } from '@elastic/eui';
+
+jest.mock('../../utils/get_username', () => ({ getUserName: jest.fn() }));
+import { getUserName } from '../../utils/get_username';
+
+import { ErrorState, NoUserState, EmptyState, LoadingState } from './';
+
+describe('ErrorState', () => {
+ it('renders', () => {
+ const wrapper = shallow();
+ const prompt = wrapper.find(EuiEmptyPrompt);
+
+ expect(prompt).toHaveLength(1);
+ expect(prompt.prop('title')).toEqual(
Cannot connect to App Search
);
+ });
+});
+
+describe('NoUserState', () => {
+ it('renders', () => {
+ const wrapper = shallow();
+ const prompt = wrapper.find(EuiEmptyPrompt);
+
+ expect(prompt).toHaveLength(1);
+ expect(prompt.prop('title')).toEqual(Cannot find App Search account
);
+ });
+
+ it('renders with username', () => {
+ getUserName.mockImplementationOnce(() => 'dolores-abernathy');
+ const wrapper = shallow();
+ const prompt = wrapper.find(EuiEmptyPrompt).dive();
+
+ expect(prompt.find(EuiCode).prop('children')).toContain('dolores-abernathy');
+ });
+});
+
+describe('EmptyState', () => {
+ it('renders', () => {
+ const wrapper = shallow();
+ const prompt = wrapper.find(EuiEmptyPrompt);
+
+ expect(prompt).toHaveLength(1);
+ expect(prompt.prop('title')).toEqual(There’s nothing here yet
);
+ });
+});
+
+describe('LoadingState', () => {
+ it('renders', () => {
+ const wrapper = shallow();
+
+ expect(wrapper.find(EuiLoadingContent)).toHaveLength(2);
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.test.tsx
new file mode 100644
index 0000000000000..dd3effce21957
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.test.tsx
@@ -0,0 +1,153 @@
+/*
+ * 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 '../../../test_utils/mock_rr_usehistory';
+
+import React from 'react';
+import { act } from 'react-dom/test-utils';
+import { render } from 'enzyme';
+
+import { KibanaContext } from '../../../';
+import { mountWithKibanaContext, mockKibanaContext } from '../../../test_utils';
+
+import { EmptyState, ErrorState, NoUserState } from '../empty_states';
+import { EngineTable } from './engine_table';
+
+import { EngineOverview } from './';
+
+describe('EngineOverview', () => {
+ describe('non-happy-path states', () => {
+ it('isLoading', () => {
+ // We use render() instead of mount() here to not trigger lifecycle methods (i.e., useEffect)
+ const wrapper = render(
+
+
+
+ );
+
+ // render() directly renders HTML which means we have to look for selectors instead of for LoadingState directly
+ expect(wrapper.find('.euiLoadingContent')).toHaveLength(2);
+ });
+
+ it('isEmpty', async () => {
+ const wrapper = await mountWithApiMock({
+ get: () => ({
+ results: [],
+ meta: { page: { total_results: 0 } },
+ }),
+ });
+
+ expect(wrapper.find(EmptyState)).toHaveLength(1);
+ });
+
+ it('hasErrorConnecting', async () => {
+ const wrapper = await mountWithApiMock({
+ get: () => ({ invalidPayload: true }),
+ });
+ expect(wrapper.find(ErrorState)).toHaveLength(1);
+ });
+
+ it('hasNoAccount', async () => {
+ const wrapper = await mountWithApiMock({
+ get: () => ({ message: 'no-as-account' }),
+ });
+ expect(wrapper.find(NoUserState)).toHaveLength(1);
+ });
+ });
+
+ describe('happy-path states', () => {
+ const mockedApiResponse = {
+ results: [
+ {
+ name: 'hello-world',
+ created_at: 'somedate',
+ document_count: 50,
+ field_count: 10,
+ },
+ ],
+ meta: {
+ page: {
+ current: 1,
+ total_pages: 10,
+ total_results: 100,
+ size: 10,
+ },
+ },
+ };
+ const mockApi = jest.fn(() => mockedApiResponse);
+ let wrapper;
+
+ beforeAll(async () => {
+ wrapper = await mountWithApiMock({ get: mockApi });
+ });
+
+ it('renders', () => {
+ expect(wrapper.find(EngineTable)).toHaveLength(2);
+ });
+
+ it('calls the engines API', () => {
+ expect(mockApi).toHaveBeenNthCalledWith(1, '/api/app_search/engines', {
+ query: {
+ type: 'indexed',
+ pageIndex: 1,
+ },
+ });
+ expect(mockApi).toHaveBeenNthCalledWith(2, '/api/app_search/engines', {
+ query: {
+ type: 'meta',
+ pageIndex: 1,
+ },
+ });
+ });
+
+ describe('pagination', () => {
+ const getTablePagination = () =>
+ wrapper
+ .find(EngineTable)
+ .first()
+ .prop('pagination');
+
+ it('passes down page data from the API', () => {
+ const pagination = getTablePagination();
+
+ expect(pagination.totalEngines).toEqual(100);
+ expect(pagination.pageIndex).toEqual(0);
+ });
+
+ it('re-polls the API on page change', async () => {
+ await act(async () => getTablePagination().onPaginate(5));
+ wrapper.update();
+
+ expect(mockApi).toHaveBeenLastCalledWith('/api/app_search/engines', {
+ query: {
+ type: 'indexed',
+ pageIndex: 5,
+ },
+ });
+ expect(getTablePagination().pageIndex).toEqual(4);
+ });
+ });
+ });
+
+ /**
+ * Test helpers
+ */
+
+ const mountWithApiMock = async ({ get }) => {
+ let wrapper;
+ const httpMock = { ...mockKibanaContext.http, get };
+
+ // We get a lot of act() warning/errors in the terminal without this.
+ // TBH, I don't fully understand why since Enzyme's mount is supposed to
+ // have act() baked in - could be because of the wrapping context provider?
+ await act(async () => {
+ wrapper = mountWithKibanaContext(, { http: httpMock });
+ });
+ wrapper.update(); // This seems to be required for the DOM to actually update
+
+ return wrapper;
+ };
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx
index c55f1f46c0e50..8c3c6d61c89d8 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx
@@ -42,35 +42,40 @@ export const EngineOverview: ReactFC<> = () => {
const [metaEnginesPage, setMetaEnginesPage] = useState(1);
const [metaEnginesTotal, setMetaEnginesTotal] = useState(0);
- const getEnginesData = ({ type, pageIndex }) => {
- return http.get('/api/app_search/engines', {
+ const getEnginesData = async ({ type, pageIndex }) => {
+ return await http.get('/api/app_search/engines', {
query: { type, pageIndex },
});
};
const hasValidData = response => {
- return response && response.results && response.meta;
+ return (
+ response &&
+ Array.isArray(response.results) &&
+ response.meta &&
+ response.meta.page &&
+ typeof response.meta.page.total_results === 'number'
+ ); // TODO: Move to optional chaining once Prettier has been updated to support it
};
const hasNoAccountError = response => {
return response && response.message === 'no-as-account';
};
- const setEnginesData = (params, callbacks) => {
- getEnginesData(params)
- .then(response => {
- if (!hasValidData(response)) {
- if (hasNoAccountError(response)) {
- return setHasNoAccount(true);
- }
- throw new Error('App Search engines response is missing valid data');
+ const setEnginesData = async (params, callbacks) => {
+ try {
+ const response = await getEnginesData(params);
+ if (!hasValidData(response)) {
+ if (hasNoAccountError(response)) {
+ return setHasNoAccount(true);
}
-
- callbacks.setResults(response.results);
- callbacks.setResultsTotal(response.meta.page.total_results);
- setIsLoading(false);
- })
- .catch(error => {
- // TODO - should we be logging errors to telemetry or elsewhere for debugging?
- setHasErrorConnecting(true);
- });
+ throw new Error('App Search engines response is missing valid data');
+ }
+
+ callbacks.setResults(response.results);
+ callbacks.setResultsTotal(response.meta.page.total_results);
+ setIsLoading(false);
+ } catch (error) {
+ // TODO - should we be logging errors to telemetry or elsewhere for debugging?
+ setHasErrorConnecting(true);
+ }
};
useEffect(() => {
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_table.test.tsx
new file mode 100644
index 0000000000000..0c05131e80835
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_table.test.tsx
@@ -0,0 +1,80 @@
+/*
+ * 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 React from 'react';
+import { EuiBasicTable, EuiPagination, EuiButtonEmpty, EuiLink } from '@elastic/eui';
+
+import { mountWithKibanaContext } from '../../../test_utils';
+jest.mock('../../../shared/telemetry', () => ({ sendTelemetry: jest.fn() }));
+import { sendTelemetry } from '../../../shared/telemetry';
+
+import { EngineTable } from './engine_table';
+
+describe('EngineTable', () => {
+ const onPaginate = jest.fn(); // onPaginate updates the engines API call upstream
+
+ const wrapper = mountWithKibanaContext(
+
+ );
+ const table = wrapper.find(EuiBasicTable);
+
+ it('renders', () => {
+ expect(table).toHaveLength(1);
+ expect(table.prop('pagination').totalItemCount).toEqual(50);
+
+ const tableContent = table.text();
+ expect(tableContent).toContain('test-engine');
+ expect(tableContent).toContain('January 1, 1970');
+ expect(tableContent).toContain('99,999');
+ expect(tableContent).toContain('10');
+
+ expect(table.find(EuiPagination).find(EuiButtonEmpty)).toHaveLength(5); // Should display 5 pages at 10 engines per page
+ });
+
+ it('contains engine links which send telemetry', () => {
+ const engineLinks = wrapper.find(EuiLink);
+
+ engineLinks.forEach(link => {
+ expect(link.prop('href')).toEqual('http://localhost:3002/as/engines/test-engine');
+ link.simulate('click');
+
+ expect(sendTelemetry).toHaveBeenCalledWith({
+ http: expect.any(Object),
+ product: 'app_search',
+ action: 'clicked',
+ metric: 'engine_table_link',
+ });
+ });
+ });
+
+ it('triggers onPaginate', () => {
+ table.prop('onChange')({ page: { index: 4 } });
+
+ expect(onPaginate).toHaveBeenCalledWith(5);
+ });
+
+ it('handles empty data', () => {
+ const emptyWrapper = mountWithKibanaContext(
+
+ );
+ const emptyTable = wrapper.find(EuiBasicTable);
+ expect(emptyTable.prop('pagination').pageIndex).toEqual(0);
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_table.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_table.tsx
index ed51c40671b4a..8db8538e82788 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_table.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_table.tsx
@@ -23,13 +23,18 @@ interface IEngineTableProps {
onPaginate(pageIndex: number);
};
}
+interface IOnChange {
+ page: {
+ index: number;
+ };
+}
export const EngineTable: ReactFC = ({
data,
pagination: { totalEngines, pageIndex = 0, onPaginate },
}) => {
const { enterpriseSearchUrl, http } = useContext(KibanaContext) as IKibanaContext;
- const engineLinkProps = {
+ const engineLinkProps = name => ({
href: `${enterpriseSearchUrl}/as/engines/${name}`,
target: '_blank',
onClick: () =>
@@ -39,13 +44,13 @@ export const EngineTable: ReactFC = ({
action: 'clicked',
metric: 'engine_table_link',
}),
- };
+ });
const columns = [
{
field: 'name',
name: 'Name',
- render: name => {name},
+ render: name => {name},
width: '30%',
truncateText: true,
mobileOptions: {
@@ -86,7 +91,7 @@ export const EngineTable: ReactFC = ({
field: 'name',
name: 'Actions',
dataType: 'string',
- render: name => Manage,
+ render: name => Manage,
align: 'right',
width: '100px',
},
@@ -102,7 +107,7 @@ export const EngineTable: ReactFC = ({
totalItemCount: totalEngines,
hidePerPageOptions: true,
}}
- onChange={({ page = {} }) => {
+ onChange={({ page }): IOnChange => {
const { index } = page;
onPaginate(index + 1); // Note on paging - App Search's API pages start at 1, EuiBasicTables' pages start at 0
}}
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview_header/engine_overview_header.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview_header/engine_overview_header.test.tsx
index 19ea683eb878c..03801e2b9f82d 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview_header/engine_overview_header.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview_header/engine_overview_header.test.tsx
@@ -4,52 +4,58 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import React from 'react';
+import '../../../test_utils/mock_shallow_usecontext';
+
+import React, { useContext } from 'react';
+import { shallow } from 'enzyme';
+
+jest.mock('../../../shared/telemetry', () => ({ sendTelemetry: jest.fn() }));
+import { sendTelemetry } from '../../../shared/telemetry';
import { EngineOverviewHeader } from '../engine_overview_header';
-import { mountWithKibanaContext } from '../../../test_utils/helpers';
describe('EngineOverviewHeader', () => {
describe('when enterpriseSearchUrl is set', () => {
- let wrapper;
+ let button;
- beforeEach(() => {
- wrapper = mountWithKibanaContext(, {
- enterpriseSearchUrl: 'http://localhost:3002',
- });
+ beforeAll(() => {
+ useContext.mockImplementationOnce(() => ({ enterpriseSearchUrl: 'http://localhost:3002' }));
+ const wrapper = shallow();
+ button = wrapper.find('[data-test-subj="launchButton"]');
});
describe('the Launch App Search button', () => {
- const subject = () => wrapper.find('EuiButton[data-test-subj="launchButton"]');
-
it('should not be disabled', () => {
- expect(subject().props().isDisabled).toBeFalsy();
+ expect(button.props().isDisabled).toBeFalsy();
});
it('should use the enterpriseSearchUrl as the base path for its href', () => {
- expect(subject().props().href).toBe('http://localhost:3002/as');
+ expect(button.props().href).toBe('http://localhost:3002/as');
+ });
+
+ it('should send telemetry when clicked', () => {
+ button.simulate('click');
+ expect(sendTelemetry).toHaveBeenCalled();
});
});
});
describe('when enterpriseSearchUrl is not set', () => {
- let wrapper;
+ let button;
- beforeEach(() => {
- wrapper = mountWithKibanaContext(, {
- enterpriseSearchUrl: undefined,
- });
+ beforeAll(() => {
+ useContext.mockImplementationOnce(() => ({ enterpriseSearchUrl: undefined }));
+ const wrapper = shallow();
+ button = wrapper.find('[data-test-subj="launchButton"]');
});
describe('the Launch App Search button', () => {
- const subject = () => wrapper.find('EuiButton[data-test-subj="launchButton"]');
-
it('should be disabled', () => {
- expect(subject().props().isDisabled).toBe(true);
+ expect(button.props().isDisabled).toBe(true);
});
it('should not have an href', () => {
- expect(subject().props().href).toBeUndefined();
+ expect(button.props().href).toBeUndefined();
});
});
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/setup_guide/setup_guide.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/setup_guide/setup_guide.test.tsx
new file mode 100644
index 0000000000000..0307d8a1555ec
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/setup_guide/setup_guide.test.tsx
@@ -0,0 +1,20 @@
+/*
+ * 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 React from 'react';
+import { shallow } from 'enzyme';
+import { EuiPageSideBar, EuiTabbedContent } from '@elastic/eui';
+
+import { SetupGuide } from './';
+
+describe('SetupGuide', () => {
+ it('renders', () => {
+ const wrapper = shallow();
+
+ expect(wrapper.find(EuiTabbedContent)).toHaveLength(1);
+ expect(wrapper.find(EuiPageSideBar)).toHaveLength(1);
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx
new file mode 100644
index 0000000000000..45d094f3c255a
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx
@@ -0,0 +1,44 @@
+/*
+ * 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 '../test_utils/mock_shallow_usecontext';
+
+import React, { useContext } from 'react';
+import { Redirect } from 'react-router-dom';
+import { shallow } from 'enzyme';
+
+import { SetupGuide } from './components/setup_guide';
+import { EngineOverview } from './components/engine_overview';
+
+import { AppSearch } from './';
+
+describe('App Search Routes', () => {
+ describe('/app_search', () => {
+ it('redirects to Setup Guide when enterpriseSearchUrl is not set', () => {
+ useContext.mockImplementationOnce(() => ({ enterpriseSearchUrl: '' }));
+ const wrapper = shallow();
+
+ expect(wrapper.find(Redirect)).toHaveLength(1);
+ expect(wrapper.find(EngineOverview)).toHaveLength(0);
+ });
+
+ it('renders Engine Overview when enterpriseSearchUrl is set', () => {
+ useContext.mockImplementationOnce(() => ({ enterpriseSearchUrl: 'https://foo.bar' }));
+ const wrapper = shallow();
+
+ expect(wrapper.find(EngineOverview)).toHaveLength(1);
+ expect(wrapper.find(Redirect)).toHaveLength(0);
+ });
+ });
+
+ describe('/app_search/setup_guide', () => {
+ it('renders', () => {
+ const wrapper = shallow();
+
+ expect(wrapper.find(SetupGuide)).toHaveLength(1);
+ });
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/utils/get_username.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/utils/get_username.test.ts
new file mode 100644
index 0000000000000..c0a9ee5a90ea5
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/utils/get_username.test.ts
@@ -0,0 +1,29 @@
+/*
+ * 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 { getUserName } from './get_username';
+
+describe('getUserName', () => {
+ it('fetches the current username from the DOM', () => {
+ document.body.innerHTML =
+ '';
+
+ expect(getUserName()).toEqual('foo_bar_baz');
+ });
+
+ it('returns null if the expected DOM does not exist', () => {
+ document.body.innerHTML = '';
+ expect(getUserName()).toEqual(null);
+
+ document.body.innerHTML = '';
+ expect(getUserName()).toEqual(null);
+
+ document.body.innerHTML = '';
+ expect(getUserName()).toEqual(null);
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/utils/get_username.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/utils/get_username.ts
index 16320af0f3757..3010da50f913e 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/utils/get_username.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/utils/get_username.ts
@@ -8,12 +8,12 @@
* Attempt to get the current Kibana user's username
* by querying the DOM
*/
-export const getUserName: () => undefined | string = () => {
+export const getUserName: () => null | string = () => {
const userMenu = document.getElementById('headerUserMenu');
- if (!userMenu) return;
+ if (!userMenu) return null;
const avatar = userMenu.querySelector('.euiAvatar');
- if (!avatar) return;
+ if (!avatar) return null;
const username = avatar.getAttribute('aria-label');
return username;
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_breadcrumbs/generate_breadcrumbs.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_breadcrumbs/generate_breadcrumbs.test.ts
index aa2b584d98425..a76170fdf795e 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_breadcrumbs/generate_breadcrumbs.test.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_breadcrumbs/generate_breadcrumbs.test.ts
@@ -4,18 +4,52 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { appSearchBreadcrumbs, enterpriseSearchBreadcrumbs } from '../kibana_breadcrumbs';
+import { generateBreadcrumb } from './generate_breadcrumbs';
+import { appSearchBreadcrumbs, enterpriseSearchBreadcrumbs } from './';
-jest.mock('../react_router_helpers', () => ({
- letBrowserHandleEvent: () => false,
-}));
+import { mockHistory } from '../../test_utils';
-describe('appSearchBreadcrumbs', () => {
- const historyMock = {
- createHref: jest.fn().mockImplementation(path => path.pathname),
- push: jest.fn(),
- };
+jest.mock('../react_router_helpers', () => ({ letBrowserHandleEvent: jest.fn(() => false) }));
+import { letBrowserHandleEvent } from '../react_router_helpers';
+
+describe('generateBreadcrumb', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it("creates a breadcrumb object matching EUI's breadcrumb type", () => {
+ const breadcrumb = generateBreadcrumb({
+ text: 'Hello World',
+ path: '/hello_world',
+ history: mockHistory,
+ });
+ expect(breadcrumb).toEqual({
+ text: 'Hello World',
+ href: '/enterprise_search/hello_world',
+ onClick: expect.any(Function),
+ });
+ });
+
+ it('prevents default navigation and uses React Router history on click', () => {
+ const breadcrumb = generateBreadcrumb({ text: '', path: '/', history: mockHistory });
+ const event = { preventDefault: jest.fn() };
+ breadcrumb.onClick(event);
+ expect(mockHistory.push).toHaveBeenCalled();
+ expect(event.preventDefault).toHaveBeenCalled();
+ });
+
+ it('does not prevents default browser behavior on new tab/window clicks', () => {
+ const breadcrumb = generateBreadcrumb({ text: '', path: '/', history: mockHistory });
+
+ letBrowserHandleEvent.mockImplementationOnce(() => true);
+ breadcrumb.onClick();
+
+ expect(mockHistory.push).not.toHaveBeenCalled();
+ });
+});
+
+describe('appSearchBreadcrumbs', () => {
const breadCrumbs = [
{
text: 'Page 1',
@@ -27,37 +61,52 @@ describe('appSearchBreadcrumbs', () => {
},
];
- afterEach(() => {
+ beforeEach(() => {
jest.clearAllMocks();
});
- const subject = () => appSearchBreadcrumbs(historyMock)(breadCrumbs);
+ const subject = () => appSearchBreadcrumbs(mockHistory)(breadCrumbs);
it('Builds a chain of breadcrumbs with Enterprise Search and App Search at the root', () => {
expect(subject()).toEqual([
{
- href: '/',
+ href: '/enterprise_search/',
onClick: expect.any(Function),
text: 'Enterprise Search',
},
{
- href: '/app_search',
+ href: '/enterprise_search/app_search',
onClick: expect.any(Function),
text: 'App Search',
},
{
- href: '/page1',
+ href: '/enterprise_search/page1',
onClick: expect.any(Function),
text: 'Page 1',
},
{
- href: '/page2',
+ href: '/enterprise_search/page2',
onClick: expect.any(Function),
text: 'Page 2',
},
]);
});
+ it('shows just the root if breadcrumbs is empty', () => {
+ expect(appSearchBreadcrumbs(mockHistory)()).toEqual([
+ {
+ href: '/enterprise_search/',
+ onClick: expect.any(Function),
+ text: 'Enterprise Search',
+ },
+ {
+ href: '/enterprise_search/app_search',
+ onClick: expect.any(Function),
+ text: 'App Search',
+ },
+ ]);
+ });
+
describe('links', () => {
const eventMock = {
preventDefault: jest.fn(),
@@ -65,32 +114,27 @@ describe('appSearchBreadcrumbs', () => {
it('has a link to Enterprise Search Home page first', () => {
subject()[0].onClick(eventMock);
- expect(historyMock.push).toHaveBeenCalledWith('/');
+ expect(mockHistory.push).toHaveBeenCalledWith('/');
});
it('has a link to App Search second', () => {
subject()[1].onClick(eventMock);
- expect(historyMock.push).toHaveBeenCalledWith('/app_search');
+ expect(mockHistory.push).toHaveBeenCalledWith('/app_search');
});
it('has a link to page 1 third', () => {
subject()[2].onClick(eventMock);
- expect(historyMock.push).toHaveBeenCalledWith('/page1');
+ expect(mockHistory.push).toHaveBeenCalledWith('/page1');
});
it('has a link to page 2 last', () => {
subject()[3].onClick(eventMock);
- expect(historyMock.push).toHaveBeenCalledWith('/page2');
+ expect(mockHistory.push).toHaveBeenCalledWith('/page2');
});
});
});
describe('enterpriseSearchBreadcrumbs', () => {
- const historyMock = {
- createHref: jest.fn(),
- push: jest.fn(),
- };
-
const breadCrumbs = [
{
text: 'Page 1',
@@ -102,32 +146,42 @@ describe('enterpriseSearchBreadcrumbs', () => {
},
];
- afterEach(() => {
+ beforeEach(() => {
jest.clearAllMocks();
});
- const subject = () => enterpriseSearchBreadcrumbs(historyMock)(breadCrumbs);
+ const subject = () => enterpriseSearchBreadcrumbs(mockHistory)(breadCrumbs);
it('Builds a chain of breadcrumbs with Enterprise Search at the root', () => {
expect(subject()).toEqual([
{
- href: undefined,
+ href: '/enterprise_search/',
onClick: expect.any(Function),
text: 'Enterprise Search',
},
{
- href: undefined,
+ href: '/enterprise_search/page1',
onClick: expect.any(Function),
text: 'Page 1',
},
{
- href: undefined,
+ href: '/enterprise_search/page2',
onClick: expect.any(Function),
text: 'Page 2',
},
]);
});
+ it('shows just the root if breadcrumbs is empty', () => {
+ expect(enterpriseSearchBreadcrumbs(mockHistory)()).toEqual([
+ {
+ href: '/enterprise_search/',
+ onClick: expect.any(Function),
+ text: 'Enterprise Search',
+ },
+ ]);
+ });
+
describe('links', () => {
const eventMock = {
preventDefault: jest.fn(),
@@ -135,17 +189,17 @@ describe('enterpriseSearchBreadcrumbs', () => {
it('has a link to Enterprise Search Home page first', () => {
subject()[0].onClick(eventMock);
- expect(historyMock.push).toHaveBeenCalledWith('/');
+ expect(mockHistory.push).toHaveBeenCalledWith('/');
});
it('has a link to page 1 second', () => {
subject()[1].onClick(eventMock);
- expect(historyMock.push).toHaveBeenCalledWith('/page1');
+ expect(mockHistory.push).toHaveBeenCalledWith('/page1');
});
it('has a link to page 2 last', () => {
subject()[2].onClick(eventMock);
- expect(historyMock.push).toHaveBeenCalledWith('/page2');
+ expect(mockHistory.push).toHaveBeenCalledWith('/page2');
});
});
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_breadcrumbs/set_breadcrumbs.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_breadcrumbs/set_breadcrumbs.test.tsx
index 788800d86ec84..5da0effd15ba5 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_breadcrumbs/set_breadcrumbs.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_breadcrumbs/set_breadcrumbs.test.tsx
@@ -6,23 +6,11 @@
import React from 'react';
-import { SetAppSearchBreadcrumbs } from '../kibana_breadcrumbs';
-import { mountWithKibanaContext } from '../../test_utils/helpers';
+import '../../test_utils/mock_rr_usehistory';
+import { mountWithKibanaContext } from '../../test_utils';
-jest.mock('./generate_breadcrumbs', () => ({
- appSearchBreadcrumbs: jest.fn(),
-}));
-import { appSearchBreadcrumbs } from './generate_breadcrumbs';
-
-jest.mock('react-router-dom', () => ({
- useHistory: () => ({
- createHref: jest.fn(),
- push: jest.fn(),
- location: {
- pathname: '/current-path',
- },
- }),
-}));
+jest.mock('./generate_breadcrumbs', () => ({ appSearchBreadcrumbs: jest.fn() }));
+import { appSearchBreadcrumbs, SetAppSearchBreadcrumbs } from './';
describe('SetAppSearchBreadcrumbs', () => {
const setBreadcrumbs = jest.fn();
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.test.tsx
new file mode 100644
index 0000000000000..0ae97383c93bb
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.test.tsx
@@ -0,0 +1,79 @@
+/*
+ * 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 React from 'react';
+import { shallow } from 'enzyme';
+import { EuiLink, EuiButton } from '@elastic/eui';
+
+import '../../test_utils/mock_rr_usehistory';
+import { mockHistory } from '../../test_utils';
+
+import { EuiReactRouterLink, EuiReactRouterButton } from './eui_link';
+
+describe('EUI & React Router Component Helpers', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('renders', () => {
+ const wrapper = shallow();
+
+ expect(wrapper.find(EuiLink)).toHaveLength(1);
+ });
+
+ it('renders an EuiButton', () => {
+ const wrapper = shallow()
+ .find(EuiReactRouterLink)
+ .dive();
+
+ expect(wrapper.find(EuiButton)).toHaveLength(1);
+ });
+
+ it('passes down all ...rest props', () => {
+ const wrapper = shallow();
+ const link = wrapper.find(EuiLink);
+
+ expect(link.prop('disabled')).toEqual(true);
+ expect(link.prop('data-test-subj')).toEqual('foo');
+ });
+
+ it('renders with the correct href and onClick props', () => {
+ const wrapper = shallow();
+ const link = wrapper.find(EuiLink);
+
+ expect(link.prop('onClick')).toBeInstanceOf(Function);
+ expect(link.prop('href')).toEqual('/enterprise_search/foo/bar');
+ expect(mockHistory.createHref).toHaveBeenCalled();
+ });
+
+ describe('onClick', () => {
+ it('prevents default navigation and uses React Router history', () => {
+ const wrapper = shallow();
+
+ const simulatedEvent = {
+ button: 0,
+ target: { getAttribute: () => '_self' },
+ preventDefault: jest.fn(),
+ };
+ wrapper.find(EuiLink).simulate('click', simulatedEvent);
+
+ expect(simulatedEvent.preventDefault).toHaveBeenCalled();
+ expect(mockHistory.push).toHaveBeenCalled();
+ });
+
+ it('does not prevent default browser behavior on new tab/window clicks', () => {
+ const wrapper = shallow();
+
+ const simulatedEvent = {
+ shiftKey: true,
+ target: { getAttribute: () => '_blank' },
+ };
+ wrapper.find(EuiLink).simulate('click', simulatedEvent);
+
+ expect(mockHistory.push).not.toHaveBeenCalled();
+ });
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.test.tsx
index a46e2124d95c5..6d92a32a502b1 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.test.tsx
@@ -5,10 +5,9 @@
*/
import React from 'react';
-import { mount } from 'enzyme';
import { httpServiceMock } from 'src/core/public/mocks';
-import { mountWithKibanaContext } from '../../test_utils/helpers';
+import { mountWithKibanaContext } from '../../test_utils';
import { sendTelemetry, SendAppSearchTelemetry } from './';
describe('Shared Telemetry Helpers', () => {
diff --git a/x-pack/plugins/enterprise_search/public/applications/test_utils/helpers.tsx b/x-pack/plugins/enterprise_search/public/applications/test_utils/helpers.tsx
deleted file mode 100644
index 9343e927e82ac..0000000000000
--- a/x-pack/plugins/enterprise_search/public/applications/test_utils/helpers.tsx
+++ /dev/null
@@ -1,25 +0,0 @@
-/*
- * 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 React from 'react';
-import { mount } from 'enzyme';
-
-import { KibanaContext } from '..';
-
-export const mountWithKibanaContext = (node, contextProps) => {
- return mount(
-
- {node}
-
- );
-};
diff --git a/x-pack/plugins/enterprise_search/public/applications/test_utils/index.ts b/x-pack/plugins/enterprise_search/public/applications/test_utils/index.ts
new file mode 100644
index 0000000000000..11627df8d15ba
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/test_utils/index.ts
@@ -0,0 +1,11 @@
+/*
+ * 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.
+ */
+
+export { mockHistory } from './mock_rr_usehistory';
+export { mockKibanaContext } from './mock_kibana_context';
+export { mountWithKibanaContext } from './mount_with_context';
+
+// Note: mock_shallow_usecontext must be imported directly as a file
diff --git a/x-pack/plugins/enterprise_search/public/applications/test_utils/mock_kibana_context.ts b/x-pack/plugins/enterprise_search/public/applications/test_utils/mock_kibana_context.ts
new file mode 100644
index 0000000000000..fcfa1b0a21f13
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/test_utils/mock_kibana_context.ts
@@ -0,0 +1,17 @@
+/*
+ * 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 { httpServiceMock } from 'src/core/public/mocks';
+
+/**
+ * A set of default Kibana context values to use across component tests.
+ * @see enterprise_search/public/index.tsx for the KibanaContext definition/import
+ */
+export const mockKibanaContext = {
+ http: httpServiceMock.createSetupContract(),
+ setBreadcrumbs: jest.fn(),
+ enterpriseSearchUrl: 'http://localhost:3002',
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/test_utils/mock_rr_usehistory.ts b/x-pack/plugins/enterprise_search/public/applications/test_utils/mock_rr_usehistory.ts
new file mode 100644
index 0000000000000..fd422465d87f1
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/test_utils/mock_rr_usehistory.ts
@@ -0,0 +1,25 @@
+/*
+ * 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.
+ */
+
+/**
+ * NOTE: This variable name MUST start with 'mock*' in order for
+ * Jest to accept its use within a jest.mock()
+ */
+export const mockHistory = {
+ createHref: jest.fn(({ pathname }) => `/enterprise_search${pathname}`),
+ push: jest.fn(),
+ location: {
+ pathname: '/current-path',
+ },
+};
+
+jest.mock('react-router-dom', () => ({
+ useHistory: jest.fn(() => mockHistory),
+}));
+
+/**
+ * For example usage, @see public/applications/shared/react_router_helpers/eui_link.test.tsx
+ */
diff --git a/x-pack/plugins/enterprise_search/public/applications/test_utils/mock_shallow_usecontext.ts b/x-pack/plugins/enterprise_search/public/applications/test_utils/mock_shallow_usecontext.ts
new file mode 100644
index 0000000000000..eca7a7ab6e354
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/test_utils/mock_shallow_usecontext.ts
@@ -0,0 +1,39 @@
+/*
+ * 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.
+ */
+
+/**
+ * NOTE: This variable name MUST start with 'mock*' in order for
+ * Jest to accept its use within a jest.mock()
+ */
+import { mockKibanaContext } from './mock_kibana_context';
+
+jest.mock('react', () => ({
+ ...jest.requireActual('react'),
+ useContext: jest.fn(() => mockKibanaContext),
+}));
+
+/**
+ * Example usage within a component test using shallow():
+ *
+ * import '../../../test_utils/mock_shallow_usecontext'; // Must come before React's import, adjust relative path as needed
+ *
+ * import React from 'react';
+ * import { shallow } from 'enzyme';
+ *
+ * // ... etc.
+ */
+
+/**
+ * If you need to override the default mock context values, you can do so via jest.mockImplementation:
+ *
+ * import React, { useContext } from 'react';
+ *
+ * // ... etc.
+ *
+ * it('some test', () => {
+ * useContext.mockImplementationOnce(() => ({ enterpriseSearchUrl: 'someOverride' }));
+ * });
+ */
diff --git a/x-pack/plugins/enterprise_search/public/applications/test_utils/mount_with_context.tsx b/x-pack/plugins/enterprise_search/public/applications/test_utils/mount_with_context.tsx
new file mode 100644
index 0000000000000..856f3faa7332b
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/test_utils/mount_with_context.tsx
@@ -0,0 +1,27 @@
+/*
+ * 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 React from 'react';
+import { mount } from 'enzyme';
+
+import { KibanaContext } from '../';
+import { mockKibanaContext } from './mock_kibana_context';
+
+/**
+ * This helper mounts a component with a set of default KibanaContext,
+ * while also allowing custom context to be passed in via a second arg
+ *
+ * Example usage:
+ *
+ * const wrapper = mountWithKibanaContext(, { enterpriseSearchUrl: 'someOverride' });
+ */
+export const mountWithKibanaContext = (node, contextProps) => {
+ return mount(
+
+ {node}
+
+ );
+};