From 5ec359eafa61b4aa865a18540d2d5408a161af21 Mon Sep 17 00:00:00 2001 From: Constance Chen Date: Tue, 28 Apr 2020 13:28:14 -0700 Subject: [PATCH 01/13] Write tests for React Router+EUI helper components --- .../react_router_helpers/eui_link.test.tsx | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.test.tsx 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 00000000000000..80947ae869e5a9 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.test.tsx @@ -0,0 +1,86 @@ +/* + * 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'; + +jest.mock('react-router-dom', () => ({ useHistory: jest.fn() })); +import { useHistory } from 'react-router-dom'; + +import { EuiReactRouterLink, EuiReactRouterButton } from './eui_link'; + +describe('EUI & React Router Component Helpers', () => { + const historyPushMock = jest.fn(); + const historycreateHrefMock = jest.fn(({ pathname }) => `/app_search${pathname}`); + useHistory.mockImplementation(() => ({ + push: historyPushMock, + createHref: historycreateHrefMock, + })); + + 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('/app_search/foo/bar'); + expect(historycreateHrefMock).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(historyPushMock).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(historyPushMock).not.toHaveBeenCalled(); + }); + }); +}); From ab467f4ef1818e634f2b7a13088863973d3c5b50 Mon Sep 17 00:00:00 2001 From: Constance Chen Date: Tue, 28 Apr 2020 14:35:20 -0700 Subject: [PATCH 02/13] Update generate_breadcrumbs test - add test suite for generateBreadcrumb() itself (in order to cover a missing branch) - minor lint fixes - remove unnecessary import from set_breadcrumbs test --- .../generate_breadcrumbs.test.ts | 50 +++++++++++++++++-- .../set_breadcrumbs.test.tsx | 1 - .../shared/telemetry/send_telemetry.test.tsx | 1 - 3 files changed, 47 insertions(+), 5 deletions(-) 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 aa2b584d984255..3981d41fab2a8b 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,15 +4,59 @@ * 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, + letBrowserHandleEvent: jest.fn(() => false), })); +import { letBrowserHandleEvent } from '../react_router_helpers'; + +describe('generateBreadcrumb', () => { + const historyMock = { + createHref: ({ pathname }) => `/foo/bar${pathname}`, + push: jest.fn(), + }; + + afterEach(() => { + jest.clearAllMocks(); + }); + + it("creates a breadcrumb object matching EUI's breadcrumb type", () => { + const breadcrumb = generateBreadcrumb({ + text: 'Hello World', + path: '/baz', + history: historyMock, + }); + expect(breadcrumb).toEqual({ + text: 'Hello World', + href: '/foo/bar/baz', + onClick: expect.any(Function), + }); + }); + + it('prevents default navigation and uses React Router history on click', () => { + const breadcrumb = generateBreadcrumb({ text: '', path: '/', history: historyMock }); + const event = { preventDefault: jest.fn() }; + breadcrumb.onClick(event); + + expect(historyMock.push).toHaveBeenCalled(); + expect(event.preventDefault).toHaveBeenCalled(); + }); + + it('does not prevents default browser behavior on new tab/window clicks', () => { + const breadcrumb = generateBreadcrumb({ text: '', path: '/', history: historyMock }); + + letBrowserHandleEvent.mockImplementationOnce(() => true); + breadcrumb.onClick(); + + expect(historyMock.push).not.toHaveBeenCalled(); + }); +}); describe('appSearchBreadcrumbs', () => { const historyMock = { - createHref: jest.fn().mockImplementation(path => path.pathname), + createHref: jest.fn(path => path.pathname), push: jest.fn(), }; 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 7d632208c007a4..788800d86ec84d 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 @@ -24,7 +24,6 @@ jest.mock('react-router-dom', () => ({ }), })); - describe('SetAppSearchBreadcrumbs', () => { const setBreadcrumbs = jest.fn(); const builtBreadcrumbs = []; 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 61e43685b6ee7b..9f136d8d359aa1 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,7 +5,6 @@ */ import React from 'react'; -import { mount } from 'enzyme'; import { httpServiceMock } from 'src/core/public/mocks'; import { mountWithKibanaContext } from '../../test_utils/helpers'; From 578de168544309ae0f5617021b36ac62cf87144a Mon Sep 17 00:00:00 2001 From: Constance Chen Date: Tue, 28 Apr 2020 14:45:18 -0700 Subject: [PATCH 03/13] Write test for get_username util + update test to return a more consistent falsey value (null) --- .../app_search/utils/get_username.test.ts | 29 +++++++++++++++++++ .../app_search/utils/get_username.ts | 6 ++-- 2 files changed, 32 insertions(+), 3 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/utils/get_username.test.ts 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 00000000000000..c0a9ee5a90ea5e --- /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 16320af0f3757c..3010da50f913e5 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; From 500533e6234593320fef3bacdb7c6eb007b66a78 Mon Sep 17 00:00:00 2001 From: Constance Chen Date: Tue, 28 Apr 2020 15:07:04 -0700 Subject: [PATCH 04/13] Add test for SetupGuide --- .../setup_guide/setup_guide.test.tsx | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/setup_guide/setup_guide.test.tsx 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 00000000000000..0307d8a1555ec2 --- /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); + }); +}); From 634ae1431ee5e1ea5614a8e6d34dbac880dbe89d Mon Sep 17 00:00:00 2001 From: Constance Chen Date: Wed, 29 Apr 2020 10:54:56 -0700 Subject: [PATCH 05/13] [Refactor] Pull out various Kibana context mocks into separate files - I'm creating a reusable useContext mock for shallow()ed enzyme components + add more documentation comments + examples --- .../engine_overview_header.test.tsx | 2 +- .../set_breadcrumbs.test.tsx | 2 +- .../shared/telemetry/send_telemetry.test.tsx | 2 +- .../applications/test_utils/helpers.tsx | 25 ------------ .../public/applications/test_utils/index.ts | 10 +++++ .../test_utils/mock_kibana_context.ts | 17 ++++++++ .../test_utils/mock_shallow_usecontext.ts | 39 +++++++++++++++++++ .../test_utils/mount_with_context.tsx | 27 +++++++++++++ 8 files changed, 96 insertions(+), 28 deletions(-) delete mode 100644 x-pack/plugins/enterprise_search/public/applications/test_utils/helpers.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/test_utils/index.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/test_utils/mock_kibana_context.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/test_utils/mock_shallow_usecontext.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/test_utils/mount_with_context.tsx 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 19ea683eb878c8..bd483ade211452 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 @@ -7,7 +7,7 @@ import React from 'react'; import { EngineOverviewHeader } from '../engine_overview_header'; -import { mountWithKibanaContext } from '../../../test_utils/helpers'; +import { mountWithKibanaContext } from '../../../test_utils'; describe('EngineOverviewHeader', () => { describe('when enterpriseSearchUrl is set', () => { 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 788800d86ec84d..459bd5d5b0f623 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 @@ -7,7 +7,7 @@ import React from 'react'; import { SetAppSearchBreadcrumbs } from '../kibana_breadcrumbs'; -import { mountWithKibanaContext } from '../../test_utils/helpers'; +import { mountWithKibanaContext } from '../../test_utils'; jest.mock('./generate_breadcrumbs', () => ({ appSearchBreadcrumbs: jest.fn(), 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 9f136d8d359aa1..5548409591a3ea 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 @@ -7,7 +7,7 @@ import React from 'react'; 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 9343e927e82ac1..00000000000000 --- 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 00000000000000..e7737c6f6d6088 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/test_utils/index.ts @@ -0,0 +1,10 @@ +/* + * 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 { 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 00000000000000..f3ee8fb2b6fc11 --- /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_shallow_usecontext.ts b/x-pack/plugins/enterprise_search/public/applications/test_utils/mock_shallow_usecontext.ts new file mode 100644 index 00000000000000..eca7a7ab6e3545 --- /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 00000000000000..856f3faa7332ba --- /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} + + ); +}; From 279b26714d8265bf5ea020195903b8e56b3e59f5 Mon Sep 17 00:00:00 2001 From: Constance Chen Date: Wed, 29 Apr 2020 11:56:25 -0700 Subject: [PATCH 06/13] Write tests for empty state components + test new usecontext shallow mock --- .../empty_states/empty_states.test.tsx | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/empty_states.test.tsx 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 00000000000000..5d1afe1e337767 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/empty_states.test.tsx @@ -0,0 +1,51 @@ +/* + * 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, EuiLoadingContent } from '@elastic/eui'; + +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

); + }); +}); + +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); + }); +}); From 48cf48abe76fdd93d12549ceea7d53f84dd3e7d5 Mon Sep 17 00:00:00 2001 From: Constance Chen Date: Wed, 29 Apr 2020 17:37:31 -0700 Subject: [PATCH 07/13] Empty state components: Add extra getUserName branch test --- .../components/empty_states/empty_states.test.tsx | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) 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 index 5d1afe1e337767..e75970404dc5e5 100644 --- 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 @@ -8,7 +8,10 @@ import '../../../test_utils/mock_shallow_usecontext'; import React from 'react'; import { shallow } from 'enzyme'; -import { EuiEmptyPrompt, EuiLoadingContent } from '@elastic/eui'; +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 './'; @@ -30,6 +33,14 @@ describe('NoUserState', () => { 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', () => { From afa53e6b8734030986327a8b1cdad2212a67809b Mon Sep 17 00:00:00 2001 From: Constance Chen Date: Wed, 29 Apr 2020 17:42:42 -0700 Subject: [PATCH 08/13] Write test for app search index/routes --- .../applications/app_search/index.test.tsx | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx 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 00000000000000..45d094f3c255a7 --- /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); + }); + }); +}); From 3e2a867c137f200728ff824ce2cc5512519bb96b Mon Sep 17 00:00:00 2001 From: Constance Chen Date: Wed, 29 Apr 2020 17:43:54 -0700 Subject: [PATCH 09/13] Write tests for engine overview table + fix bonus bug --- .../engine_overview/engine_table.test.tsx | 72 +++++++++++++++++++ .../engine_overview/engine_table.tsx | 8 +-- 2 files changed, 76 insertions(+), 4 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_table.test.tsx 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 00000000000000..155226e0ad71c5 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_table.test.tsx @@ -0,0 +1,72 @@ +/* + * 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); + }); +}); 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 ed51c40671b4ae..fee4bdf7d2cbde 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 @@ -29,7 +29,7 @@ export const EngineTable: ReactFC = ({ 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 +39,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 +86,7 @@ export const EngineTable: ReactFC = ({ field: 'name', name: 'Actions', dataType: 'string', - render: name => Manage, + render: name => Manage, align: 'right', width: '100px', }, From 6b787432bb2ee1bcfe75cf6bb0a00952983d4f37 Mon Sep 17 00:00:00 2001 From: Constance Chen Date: Wed, 29 Apr 2020 18:14:23 -0700 Subject: [PATCH 10/13] Write Engine Overview tests + Update EngineOverview logic to account for issues found during tests :) - Move http to async/await syntax instead of promise syntax (works better with existing HttpServiceMock jest.fn()s) - hasValidData wasn't strict enough in type checking/object nest checking and was causing the app itself to crash (no bueno) --- .../engine_overview/engine_overview.test.tsx | 161 ++++++++++++++++++ .../engine_overview/engine_overview.tsx | 45 ++--- 2 files changed, 186 insertions(+), 20 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.test.tsx 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 00000000000000..43fbcbdfa445c4 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.test.tsx @@ -0,0 +1,161 @@ +/* + * 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 { act } from 'react-dom/test-utils'; +import { render } from 'enzyme'; + +jest.mock('react-router-dom', () => ({ + useHistory: () => ({ + createHref: jest.fn(), + push: jest.fn(), + location: { + pathname: '/current-path', + }, + }), +})); + +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 c55f1f46c0e505..8c3c6d61c89d84 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(() => { From 077b1e1bbc290265942042e3405259210626ba3c Mon Sep 17 00:00:00 2001 From: Constance Chen Date: Tue, 28 Apr 2020 14:53:20 -0700 Subject: [PATCH 11/13] Refactor EngineOverviewHeader test to use shallow + to full coverage - missed adding this test during telemetry work - switching to shallow and beforeAll reduces the test time from 5s to 4s! --- .../engine_overview_header.test.tsx | 46 +++++++++++-------- 1 file changed, 26 insertions(+), 20 deletions(-) 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 bd483ade211452..03801e2b9f82d0 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'; 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(); }); }); }); From 8d25b053735da9c06998dec1d8bd9b174a8a4ff9 Mon Sep 17 00:00:00 2001 From: Constance Chen Date: Thu, 30 Apr 2020 12:11:22 -0700 Subject: [PATCH 12/13] [Refactor] Pull out React Router history mocks into a test util helper + minor refactors/updates --- .../engine_overview/engine_overview.test.tsx | 12 +-- .../generate_breadcrumbs.test.ts | 73 ++++++++----------- .../set_breadcrumbs.test.tsx | 18 +---- .../react_router_helpers/eui_link.test.tsx | 19 ++--- .../public/applications/test_utils/index.ts | 1 + .../test_utils/mock_rr_usehistory.ts | 25 +++++++ 6 files changed, 66 insertions(+), 82 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/test_utils/mock_rr_usehistory.ts 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 index 43fbcbdfa445c4..dd3effce219571 100644 --- 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 @@ -4,20 +4,12 @@ * 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'; -jest.mock('react-router-dom', () => ({ - useHistory: () => ({ - createHref: jest.fn(), - push: jest.fn(), - location: { - pathname: '/current-path', - }, - }), -})); - import { KibanaContext } from '../../../'; import { mountWithKibanaContext, mockKibanaContext } from '../../../test_utils'; 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 3981d41fab2a8b..f318b5cd60a9b7 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 @@ -7,59 +7,49 @@ import { generateBreadcrumb } from './generate_breadcrumbs'; import { appSearchBreadcrumbs, enterpriseSearchBreadcrumbs } from './'; -jest.mock('../react_router_helpers', () => ({ - letBrowserHandleEvent: jest.fn(() => false), -})); +import { mockHistory } from '../../test_utils'; + +jest.mock('../react_router_helpers', () => ({ letBrowserHandleEvent: jest.fn(() => false) })); import { letBrowserHandleEvent } from '../react_router_helpers'; describe('generateBreadcrumb', () => { - const historyMock = { - createHref: ({ pathname }) => `/foo/bar${pathname}`, - push: jest.fn(), - }; - - afterEach(() => { + beforeEach(() => { jest.clearAllMocks(); }); it("creates a breadcrumb object matching EUI's breadcrumb type", () => { const breadcrumb = generateBreadcrumb({ text: 'Hello World', - path: '/baz', - history: historyMock, + path: '/hello_world', + history: mockHistory, }); expect(breadcrumb).toEqual({ text: 'Hello World', - href: '/foo/bar/baz', + 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: historyMock }); + const breadcrumb = generateBreadcrumb({ text: '', path: '/', history: mockHistory }); const event = { preventDefault: jest.fn() }; breadcrumb.onClick(event); - expect(historyMock.push).toHaveBeenCalled(); + 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: historyMock }); + const breadcrumb = generateBreadcrumb({ text: '', path: '/', history: mockHistory }); letBrowserHandleEvent.mockImplementationOnce(() => true); breadcrumb.onClick(); - expect(historyMock.push).not.toHaveBeenCalled(); + expect(mockHistory.push).not.toHaveBeenCalled(); }); }); describe('appSearchBreadcrumbs', () => { - const historyMock = { - createHref: jest.fn(path => path.pathname), - push: jest.fn(), - }; - const breadCrumbs = [ { text: 'Page 1', @@ -71,31 +61,31 @@ 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', }, @@ -109,32 +99,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', @@ -146,26 +131,26 @@ 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', }, @@ -179,17 +164,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 459bd5d5b0f623..5da0effd15ba5e 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 '../../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 index 80947ae869e5a9..0ae97383c93bb7 100644 --- 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 @@ -8,19 +8,12 @@ import React from 'react'; import { shallow } from 'enzyme'; import { EuiLink, EuiButton } from '@elastic/eui'; -jest.mock('react-router-dom', () => ({ useHistory: jest.fn() })); -import { useHistory } from 'react-router-dom'; +import '../../test_utils/mock_rr_usehistory'; +import { mockHistory } from '../../test_utils'; import { EuiReactRouterLink, EuiReactRouterButton } from './eui_link'; describe('EUI & React Router Component Helpers', () => { - const historyPushMock = jest.fn(); - const historycreateHrefMock = jest.fn(({ pathname }) => `/app_search${pathname}`); - useHistory.mockImplementation(() => ({ - push: historyPushMock, - createHref: historycreateHrefMock, - })); - beforeEach(() => { jest.clearAllMocks(); }); @@ -52,8 +45,8 @@ describe('EUI & React Router Component Helpers', () => { const link = wrapper.find(EuiLink); expect(link.prop('onClick')).toBeInstanceOf(Function); - expect(link.prop('href')).toEqual('/app_search/foo/bar'); - expect(historycreateHrefMock).toHaveBeenCalled(); + expect(link.prop('href')).toEqual('/enterprise_search/foo/bar'); + expect(mockHistory.createHref).toHaveBeenCalled(); }); describe('onClick', () => { @@ -68,7 +61,7 @@ describe('EUI & React Router Component Helpers', () => { wrapper.find(EuiLink).simulate('click', simulatedEvent); expect(simulatedEvent.preventDefault).toHaveBeenCalled(); - expect(historyPushMock).toHaveBeenCalled(); + expect(mockHistory.push).toHaveBeenCalled(); }); it('does not prevent default browser behavior on new tab/window clicks', () => { @@ -80,7 +73,7 @@ describe('EUI & React Router Component Helpers', () => { }; wrapper.find(EuiLink).simulate('click', simulatedEvent); - expect(historyPushMock).not.toHaveBeenCalled(); + expect(mockHistory.push).not.toHaveBeenCalled(); }); }); }); 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 index e7737c6f6d6088..11627df8d15ba2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/test_utils/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/test_utils/index.ts @@ -4,6 +4,7 @@ * 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'; 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 00000000000000..fd422465d87f1b --- /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 + */ From 38d07db1d7896541713122695a33fa919d39021f Mon Sep 17 00:00:00 2001 From: Constance Chen Date: Thu, 30 Apr 2020 12:14:22 -0700 Subject: [PATCH 13/13] Add small tests to increase branch coverage - mostly testing fallbacks or removing fallbacks in favor of strict type interface - these are slightly obsessive so I'd also be fine ditching them if they aren't terribly valuable --- .../engine_overview/engine_table.test.tsx | 8 ++++++ .../engine_overview/engine_table.tsx | 7 +++++- .../generate_breadcrumbs.test.ts | 25 +++++++++++++++++++ 3 files changed, 39 insertions(+), 1 deletion(-) 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 index 155226e0ad71c5..0c05131e808352 100644 --- 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 @@ -69,4 +69,12 @@ describe('EngineTable', () => { 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 fee4bdf7d2cbde..8db8538e82788d 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,6 +23,11 @@ interface IEngineTableProps { onPaginate(pageIndex: number); }; } +interface IOnChange { + page: { + index: number; + }; +} export const EngineTable: ReactFC = ({ data, @@ -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/shared/kibana_breadcrumbs/generate_breadcrumbs.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_breadcrumbs/generate_breadcrumbs.test.ts index f318b5cd60a9b7..a76170fdf795e5 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 @@ -92,6 +92,21 @@ describe('appSearchBreadcrumbs', () => { ]); }); + 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(), @@ -157,6 +172,16 @@ describe('enterpriseSearchBreadcrumbs', () => { ]); }); + 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(),