From 9b268f3bc9b6ad2b6c18916cec25baf9fb027867 Mon Sep 17 00:00:00 2001 From: Nick Peihl Date: Wed, 20 Sep 2023 18:06:58 -0400 Subject: [PATCH] [Dashboard Navigation] Unit tests (#166297) Part of #161287 These unit tests were designed to test complex cases for the components. Please review carefully and suggest any test cases that I may have overlooked. --- src/plugins/dashboard/jest_setup.ts | 5 +- .../clone_panel_action.test.tsx | 12 +- .../expand_panel_action.test.tsx | 12 +- .../export_csv_action.test.tsx | 12 +- .../replace_panel_action.test.tsx | 12 +- .../dashboard_empty_screen.test.tsx | 2 +- .../component/grid/dashboard_grid.test.tsx | 22 +- .../embeddable/dashboard_container.test.tsx | 50 ++-- src/plugins/dashboard/public/mocks.tsx | 26 +- .../navigation_embeddable/common/mocks.tsx | 62 +++++ .../navigation_embeddable/jest.config.js | 1 + .../navigation_embeddable/jest_setup.ts | 13 + .../dashboard_link_component.test.tsx | 227 ++++++++++++++++++ .../dashboard_link_component.tsx | 8 +- ...avigation_embeddable_panel_editor.test.tsx | 129 ++++++++++ .../navigation_embeddable_panel_editor.tsx | 14 +- ...n_embeddable_panel_editor_empty_prompt.tsx | 2 +- ...avigation_embeddable_panel_editor_link.tsx | 1 + .../external_link_component.test.tsx | 106 ++++++++ .../external_link/external_link_component.tsx | 2 + .../navigation_embeddable/public/mocks.tsx | 23 ++ .../navigation_embeddable/tsconfig.json | 2 +- 22 files changed, 682 insertions(+), 61 deletions(-) create mode 100644 src/plugins/navigation_embeddable/common/mocks.tsx create mode 100644 src/plugins/navigation_embeddable/jest_setup.ts create mode 100644 src/plugins/navigation_embeddable/public/components/dashboard_link/dashboard_link_component.test.tsx create mode 100644 src/plugins/navigation_embeddable/public/components/editor/navigation_embeddable_panel_editor.test.tsx create mode 100644 src/plugins/navigation_embeddable/public/components/external_link/external_link_component.test.tsx create mode 100644 src/plugins/navigation_embeddable/public/mocks.tsx diff --git a/src/plugins/dashboard/jest_setup.ts b/src/plugins/dashboard/jest_setup.ts index 5683ecd4e288b..c6318bc3c4df6 100644 --- a/src/plugins/dashboard/jest_setup.ts +++ b/src/plugins/dashboard/jest_setup.ts @@ -6,7 +6,6 @@ * Side Public License, v 1. */ -import { pluginServices } from './public/services/plugin_services'; -import { registry } from './public/services/plugin_services.stub'; +import { setStubDashboardServices } from './public/mocks'; -pluginServices.setRegistry(registry.start({})); +setStubDashboardServices(); diff --git a/src/plugins/dashboard/public/dashboard_actions/clone_panel_action.test.tsx b/src/plugins/dashboard/public/dashboard_actions/clone_panel_action.test.tsx index 5ec0ac57c574b..76b62f28993ad 100644 --- a/src/plugins/dashboard/public/dashboard_actions/clone_panel_action.test.tsx +++ b/src/plugins/dashboard/public/dashboard_actions/clone_panel_action.test.tsx @@ -47,11 +47,13 @@ beforeEach(async () => { .fn() .mockReturnValue(mockEmbeddableFactory); container = buildMockDashboard({ - panels: { - '123': getSampleDashboardPanel({ - explicitInput: { firstName: 'Kibanana', id: '123' }, - type: CONTACT_CARD_EMBEDDABLE, - }), + overrides: { + panels: { + '123': getSampleDashboardPanel({ + explicitInput: { firstName: 'Kibanana', id: '123' }, + type: CONTACT_CARD_EMBEDDABLE, + }), + }, }, }); diff --git a/src/plugins/dashboard/public/dashboard_actions/expand_panel_action.test.tsx b/src/plugins/dashboard/public/dashboard_actions/expand_panel_action.test.tsx index 877488c6d8041..194edc675b108 100644 --- a/src/plugins/dashboard/public/dashboard_actions/expand_panel_action.test.tsx +++ b/src/plugins/dashboard/public/dashboard_actions/expand_panel_action.test.tsx @@ -31,11 +31,13 @@ pluginServices.getServices().embeddable.getEmbeddableFactory = jest beforeEach(async () => { container = buildMockDashboard({ - panels: { - '123': getSampleDashboardPanel({ - explicitInput: { firstName: 'Sam', id: '123' }, - type: CONTACT_CARD_EMBEDDABLE, - }), + overrides: { + panels: { + '123': getSampleDashboardPanel({ + explicitInput: { firstName: 'Sam', id: '123' }, + type: CONTACT_CARD_EMBEDDABLE, + }), + }, }, }); diff --git a/src/plugins/dashboard/public/dashboard_actions/export_csv_action.test.tsx b/src/plugins/dashboard/public/dashboard_actions/export_csv_action.test.tsx index 350db8fad40b1..0fbbe9c76b2cf 100644 --- a/src/plugins/dashboard/public/dashboard_actions/export_csv_action.test.tsx +++ b/src/plugins/dashboard/public/dashboard_actions/export_csv_action.test.tsx @@ -46,11 +46,13 @@ describe('Export CSV action', () => { }; container = buildMockDashboard({ - panels: { - '123': getSampleDashboardPanel({ - explicitInput: { firstName: 'Kibanana', id: '123' }, - type: CONTACT_CARD_EXPORTABLE_EMBEDDABLE, - }), + overrides: { + panels: { + '123': getSampleDashboardPanel({ + explicitInput: { firstName: 'Kibanana', id: '123' }, + type: CONTACT_CARD_EXPORTABLE_EMBEDDABLE, + }), + }, }, }); diff --git a/src/plugins/dashboard/public/dashboard_actions/replace_panel_action.test.tsx b/src/plugins/dashboard/public/dashboard_actions/replace_panel_action.test.tsx index 0829f89424ede..5873253e105d4 100644 --- a/src/plugins/dashboard/public/dashboard_actions/replace_panel_action.test.tsx +++ b/src/plugins/dashboard/public/dashboard_actions/replace_panel_action.test.tsx @@ -29,11 +29,13 @@ let container: DashboardContainer; let embeddable: ContactCardEmbeddable; beforeEach(async () => { container = buildMockDashboard({ - panels: { - '123': getSampleDashboardPanel({ - explicitInput: { firstName: 'Sam', id: '123' }, - type: CONTACT_CARD_EMBEDDABLE, - }), + overrides: { + panels: { + '123': getSampleDashboardPanel({ + explicitInput: { firstName: 'Sam', id: '123' }, + type: CONTACT_CARD_EMBEDDABLE, + }), + }, }, }); diff --git a/src/plugins/dashboard/public/dashboard_container/component/empty_screen/dashboard_empty_screen.test.tsx b/src/plugins/dashboard/public/dashboard_container/component/empty_screen/dashboard_empty_screen.test.tsx index 439fc43ce8eb0..fb2f6e2f16b28 100644 --- a/src/plugins/dashboard/public/dashboard_container/component/empty_screen/dashboard_empty_screen.test.tsx +++ b/src/plugins/dashboard/public/dashboard_container/component/empty_screen/dashboard_empty_screen.test.tsx @@ -22,7 +22,7 @@ pluginServices.getServices().visualizations.getAliases = jest describe('DashboardEmptyScreen', () => { function mountComponent(viewMode: ViewMode) { - const dashboardContainer = buildMockDashboard({ viewMode }); + const dashboardContainer = buildMockDashboard({ overrides: { viewMode } }); return mountWithIntl( diff --git a/src/plugins/dashboard/public/dashboard_container/component/grid/dashboard_grid.test.tsx b/src/plugins/dashboard/public/dashboard_container/component/grid/dashboard_grid.test.tsx index be21a9ba6645e..8c587bf175bc7 100644 --- a/src/plugins/dashboard/public/dashboard_container/component/grid/dashboard_grid.test.tsx +++ b/src/plugins/dashboard/public/dashboard_container/component/grid/dashboard_grid.test.tsx @@ -39,16 +39,18 @@ jest.mock('./dashboard_grid_item', () => { const createAndMountDashboardGrid = () => { const dashboardContainer = buildMockDashboard({ - panels: { - '1': { - gridData: { x: 0, y: 0, w: 6, h: 6, i: '1' }, - type: CONTACT_CARD_EMBEDDABLE, - explicitInput: { id: '1' }, - }, - '2': { - gridData: { x: 6, y: 6, w: 6, h: 6, i: '2' }, - type: CONTACT_CARD_EMBEDDABLE, - explicitInput: { id: '2' }, + overrides: { + panels: { + '1': { + gridData: { x: 0, y: 0, w: 6, h: 6, i: '1' }, + type: CONTACT_CARD_EMBEDDABLE, + explicitInput: { id: '1' }, + }, + '2': { + gridData: { x: 6, y: 6, w: 6, h: 6, i: '2' }, + type: CONTACT_CARD_EMBEDDABLE, + explicitInput: { id: '2' }, + }, }, }, }); diff --git a/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.test.tsx b/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.test.tsx index 67bb482b45676..b80c7d11dcfe8 100644 --- a/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.test.tsx +++ b/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.test.tsx @@ -43,11 +43,13 @@ pluginServices.getServices().embeddable.getEmbeddableFactory = jest test('DashboardContainer initializes embeddables', (done) => { const container = buildMockDashboard({ - panels: { - '123': getSampleDashboardPanel({ - explicitInput: { firstName: 'Sam', id: '123' }, - type: CONTACT_CARD_EMBEDDABLE, - }), + overrides: { + panels: { + '123': getSampleDashboardPanel({ + explicitInput: { firstName: 'Sam', id: '123' }, + type: CONTACT_CARD_EMBEDDABLE, + }), + }, }, }); @@ -94,11 +96,13 @@ test('DashboardContainer.replacePanel', (done) => { const ID = '123'; const container = buildMockDashboard({ - panels: { - [ID]: getSampleDashboardPanel({ - explicitInput: { firstName: 'Sam', id: ID }, - type: CONTACT_CARD_EMBEDDABLE, - }), + overrides: { + panels: { + [ID]: getSampleDashboardPanel({ + explicitInput: { firstName: 'Sam', id: ID }, + type: CONTACT_CARD_EMBEDDABLE, + }), + }, }, }); let counter = 0; @@ -134,11 +138,13 @@ test('DashboardContainer.replacePanel', (done) => { test('Container view mode change propagates to existing children', async () => { const container = buildMockDashboard({ - panels: { - '123': getSampleDashboardPanel({ - explicitInput: { firstName: 'Sam', id: '123' }, - type: CONTACT_CARD_EMBEDDABLE, - }), + overrides: { + panels: { + '123': getSampleDashboardPanel({ + explicitInput: { firstName: 'Sam', id: '123' }, + type: CONTACT_CARD_EMBEDDABLE, + }), + }, }, }); @@ -192,7 +198,7 @@ test('DashboardContainer in edit mode shows edit mode actions', async () => { uiActionsSetup.registerAction(editModeAction); uiActionsSetup.addTriggerAction(CONTEXT_MENU_TRIGGER, editModeAction); - const container = buildMockDashboard({ viewMode: ViewMode.VIEW }); + const container = buildMockDashboard({ overrides: { viewMode: ViewMode.VIEW } }); const embeddable = await container.addNewEmbeddable< ContactCardEmbeddableInput, @@ -268,8 +274,10 @@ describe('getInheritedInput', () => { test('Should pass dashboard timeRange and timeslice to panel when panel does not have custom time range', async () => { const container = buildMockDashboard({ - timeRange: dashboardTimeRange, - timeslice: dashboardTimeslice, + overrides: { + timeRange: dashboardTimeRange, + timeslice: dashboardTimeslice, + }, }); const embeddable = await container.addNewEmbeddable( CONTACT_CARD_EMBEDDABLE, @@ -291,8 +299,10 @@ describe('getInheritedInput', () => { test('Should not pass dashboard timeRange and timeslice to panel when panel has custom time range', async () => { const container = buildMockDashboard({ - timeRange: dashboardTimeRange, - timeslice: dashboardTimeslice, + overrides: { + timeRange: dashboardTimeRange, + timeslice: dashboardTimeslice, + }, }); const embeddableTimeRange = { to: 'now', diff --git a/src/plugins/dashboard/public/mocks.tsx b/src/plugins/dashboard/public/mocks.tsx index 69ec7efadf1ad..778f0471b191b 100644 --- a/src/plugins/dashboard/public/mocks.tsx +++ b/src/plugins/dashboard/public/mocks.tsx @@ -12,6 +12,8 @@ import { mockedReduxEmbeddablePackage } from '@kbn/presentation-util-plugin/publ import { DashboardStart } from './plugin'; import { DashboardContainerInput, DashboardPanelState } from '../common'; import { DashboardContainer } from './dashboard_container/embeddable/dashboard_container'; +import { pluginServices } from './services/plugin_services'; +import { registry } from './services/plugin_services.stub'; export type Start = jest.Mocked; @@ -66,9 +68,25 @@ export function setupIntersectionObserverMock({ }); } -export function buildMockDashboard(overrides?: Partial) { +export function buildMockDashboard({ + overrides, + savedObjectId, +}: { + overrides?: Partial; + savedObjectId?: string; +} = {}) { const initialInput = getSampleDashboardInput(overrides); - const dashboardContainer = new DashboardContainer(initialInput, mockedReduxEmbeddablePackage); + const dashboardContainer = new DashboardContainer( + initialInput, + mockedReduxEmbeddablePackage, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + savedObjectId + ); return dashboardContainer; } @@ -120,3 +138,7 @@ export function getSampleDashboardPanel { + return { + getNavigationEmbeddableAttributeService: jest.fn(() => { + return { + saveMethod: jest.fn(), + unwrapMethod: jest.fn(), + checkForDuplicateTitle: jest.fn(), + unwrapAttributes: jest.fn((attributes: NavigationEmbeddableByValueInput) => + Promise.resolve(attributes) + ), + wrapAttributes: jest.fn((attributes: NavigationEmbeddableAttributes) => + Promise.resolve(attributes) + ), + }; + }), + }; +}); + +export const mockNavigationEmbeddableInput = ( + partial?: Partial +): NavigationEmbeddableByValueInput => ({ + id: 'mocked_links_panel', + attributes: { + title: 'mocked_links', + }, + ...(partial ?? {}), +}); + +export const mockNavigationEmbeddable = async ({ + explicitInput, + dashboardExplicitInput, +}: { + explicitInput?: Partial; + dashboardExplicitInput?: Partial; +}) => { + const dashboardContainer = buildMockDashboard({ + overrides: dashboardExplicitInput, + savedObjectId: '123', + }); + const navigationEmbeddableFactoryStub = new NavigationEmbeddableFactoryDefinition(); + + const navigationEmbeddable = await navigationEmbeddableFactoryStub.create( + mockNavigationEmbeddableInput(explicitInput), + dashboardContainer + ); + + return navigationEmbeddable; +}; diff --git a/src/plugins/navigation_embeddable/jest.config.js b/src/plugins/navigation_embeddable/jest.config.js index d864b4a234669..208efe01a7815 100644 --- a/src/plugins/navigation_embeddable/jest.config.js +++ b/src/plugins/navigation_embeddable/jest.config.js @@ -15,4 +15,5 @@ module.exports = { collectCoverageFrom: [ '/src/plugins/navigation_embeddable/{common,public,server}/**/*.{ts,tsx}', ], + setupFiles: ['/src/plugins/navigation_embeddable/jest_setup.ts'], }; diff --git a/src/plugins/navigation_embeddable/jest_setup.ts b/src/plugins/navigation_embeddable/jest_setup.ts new file mode 100644 index 0000000000000..5ea91dfb399f4 --- /dev/null +++ b/src/plugins/navigation_embeddable/jest_setup.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { setStubDashboardServices } from '@kbn/dashboard-plugin/public/mocks'; +import { setStubKibanaServices } from './public/mocks'; + +setStubKibanaServices(); +setStubDashboardServices(); diff --git a/src/plugins/navigation_embeddable/public/components/dashboard_link/dashboard_link_component.test.tsx b/src/plugins/navigation_embeddable/public/components/dashboard_link/dashboard_link_component.test.tsx new file mode 100644 index 0000000000000..7c47314c952a1 --- /dev/null +++ b/src/plugins/navigation_embeddable/public/components/dashboard_link/dashboard_link_component.test.tsx @@ -0,0 +1,227 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; + +import userEvent from '@testing-library/user-event'; +import { createEvent, fireEvent, render, screen, waitFor } from '@testing-library/react'; +import { DEFAULT_DASHBOARD_DRILLDOWN_OPTIONS } from '@kbn/presentation-util-plugin/public'; +import { DashboardLinkStrings } from './dashboard_link_strings'; +import { + NavigationEmbeddable, + NavigationEmbeddableContext, +} from '../../embeddable/navigation_embeddable'; +import { mockNavigationEmbeddable } from '../../../common/mocks'; +import { NAV_VERTICAL_LAYOUT } from '../../../common/content_management'; +import { DashboardLinkComponent } from './dashboard_link_component'; +import { fetchDashboard, getDashboardHref, getDashboardLocator } from './dashboard_link_tools'; +import { coreServices } from '../../services/kibana_services'; + +jest.mock('./dashboard_link_tools'); + +describe('Dashboard link component', () => { + const mockDashboards = [ + { + id: '456', + status: 'success', + attributes: { + title: 'another dashboard', + description: 'something awesome', + panelsJSON: [], + timeRestore: false, + version: '1', + }, + }, + { + id: '123', + status: 'success', + attributes: { + title: 'current dashboard', + description: '', + panelsJSON: [], + timeRestore: false, + version: '1', + }, + }, + ]; + + const defaultLinkInfo = { + destination: '456', + order: 1, + id: 'foo', + type: 'dashboardLink' as const, + }; + + let navEmbeddable: NavigationEmbeddable; + beforeEach(async () => { + window.open = jest.fn(); + (fetchDashboard as jest.Mock).mockResolvedValue(mockDashboards[0]); + (getDashboardLocator as jest.Mock).mockResolvedValue({ + app: 'dashboard', + path: '/dashboardItem/456', + state: {}, + }); + (getDashboardHref as jest.Mock).mockReturnValue('https://my-kibana.com/dashboard/123'); + navEmbeddable = await mockNavigationEmbeddable({ + dashboardExplicitInput: mockDashboards[1].attributes, + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('by default uses navigateToApp to open in same tab', async () => { + render( + + + + ); + + await waitFor(() => expect(fetchDashboard).toHaveBeenCalledTimes(1)); + expect(fetchDashboard).toHaveBeenCalledWith(defaultLinkInfo.destination); + expect(getDashboardLocator).toHaveBeenCalledTimes(1); + expect(getDashboardLocator).toHaveBeenCalledWith({ + link: { + ...defaultLinkInfo, + options: DEFAULT_DASHBOARD_DRILLDOWN_OPTIONS, + }, + navEmbeddable, + }); + + const link = await screen.findByTestId('dashboardLink--foo'); + expect(link).toHaveTextContent('another dashboard'); + await userEvent.click(link); + expect(coreServices.application.navigateToApp).toBeCalledTimes(1); + expect(coreServices.application.navigateToApp).toBeCalledWith('dashboard', { + path: '/dashboardItem/456', + state: {}, + }); + }); + + test('modified click does not trigger event.preventDefault', async () => { + render( + + + + ); + await waitFor(() => expect(fetchDashboard).toHaveBeenCalledTimes(1)); + const link = await screen.findByTestId('dashboardLink--foo'); + const clickEvent = createEvent.click(link, { ctrlKey: true }); + const preventDefault = jest.spyOn(clickEvent, 'preventDefault'); + fireEvent(link, clickEvent); + expect(preventDefault).toHaveBeenCalledTimes(0); + }); + + test('openInNewTab uses window.open, not navigateToApp', async () => { + const linkInfo = { + ...defaultLinkInfo, + options: { ...DEFAULT_DASHBOARD_DRILLDOWN_OPTIONS, openInNewTab: true }, + }; + render( + + + + ); + await waitFor(() => expect(fetchDashboard).toHaveBeenCalledTimes(1)); + expect(fetchDashboard).toHaveBeenCalledWith(linkInfo.destination); + expect(getDashboardLocator).toHaveBeenCalledWith({ link: linkInfo, navEmbeddable }); + const link = await screen.findByTestId('dashboardLink--foo'); + expect(link).toBeInTheDocument(); + await userEvent.click(link); + expect(coreServices.application.navigateToApp).toBeCalledTimes(0); + expect(window.open).toHaveBeenCalledWith('https://my-kibana.com/dashboard/123', '_blank'); + }); + + test('passes linkOptions to getDashboardLocator', async () => { + const linkInfo = { + ...defaultLinkInfo, + options: { + ...DEFAULT_DASHBOARD_DRILLDOWN_OPTIONS, + useCurrentFilters: false, + useCurrentTimeRange: false, + useCurrentDateRange: false, + }, + }; + render( + + + + ); + await waitFor(() => expect(fetchDashboard).toHaveBeenCalledTimes(1)); + expect(getDashboardLocator).toHaveBeenCalledWith({ link: linkInfo, navEmbeddable }); + }); + + test('shows an error when fetchDashboard fails', async () => { + (fetchDashboard as jest.Mock).mockRejectedValue(new Error('some error')); + const linkInfo = { + ...defaultLinkInfo, + id: 'notfound', + }; + render( + + + + ); + await waitFor(() => expect(fetchDashboard).toHaveBeenCalledTimes(1)); + const link = await screen.findByTestId('dashboardLink--notfound--error'); + expect(link).toHaveTextContent(DashboardLinkStrings.getDashboardErrorLabel()); + }); + + test('current dashboard is not a clickable href', async () => { + const linkInfo = { + ...defaultLinkInfo, + destination: '123', + id: 'bar', + }; + render( + + + + ); + await waitFor(() => expect(fetchDashboard).toHaveBeenCalledTimes(1)); + const link = await screen.findByTestId('dashboardLink--bar'); + expect(link).toHaveTextContent('current dashboard'); + await userEvent.click(link); + expect(coreServices.application.navigateToApp).toBeCalledTimes(0); + expect(window.open).toBeCalledTimes(0); + }); + + test('shows dashboard title and description in tooltip', async () => { + render( + + + + ); + await waitFor(() => expect(fetchDashboard).toHaveBeenCalledTimes(1)); + const link = await screen.findByTestId('dashboardLink--foo'); + await userEvent.hover(link); + const tooltip = await screen.findByTestId('dashboardLink--foo--tooltip'); + expect(tooltip).toHaveTextContent('another dashboard'); // title + expect(tooltip).toHaveTextContent('something awesome'); // description + }); + + test('can override link label', async () => { + const label = 'my custom label'; + const linkInfo = { + ...defaultLinkInfo, + label, + }; + render( + + + + ); + await waitFor(() => expect(fetchDashboard).toHaveBeenCalledTimes(1)); + const link = await screen.findByTestId('dashboardLink--foo'); + expect(link).toHaveTextContent(label); + await userEvent.hover(link); + const tooltip = await screen.findByTestId('dashboardLink--foo--tooltip'); + expect(tooltip).toHaveTextContent(label); + }); +}); diff --git a/src/plugins/navigation_embeddable/public/components/dashboard_link/dashboard_link_component.tsx b/src/plugins/navigation_embeddable/public/components/dashboard_link/dashboard_link_component.tsx index 71627054ea430..1cb9df3efbdcf 100644 --- a/src/plugins/navigation_embeddable/public/components/dashboard_link/dashboard_link_component.tsx +++ b/src/plugins/navigation_embeddable/public/components/dashboard_link/dashboard_link_component.tsx @@ -139,7 +139,11 @@ export const DashboardLinkComponent = ({ return loadingDestinationDashboard ? ( @@ -156,6 +160,7 @@ export const DashboardLinkComponent = ({ position: layout === NAV_VERTICAL_LAYOUT ? 'right' : 'bottom', repositionOnScroll: true, delay: 'long', + 'data-test-subj': `dashboardLink--${link.id}--tooltip`, }} iconType={error ? 'warning' : undefined} iconProps={{ className: 'dashboardLinkIcon' }} @@ -166,6 +171,7 @@ export const DashboardLinkComponent = ({ 'dashboardLinkError--noLabel': !link.label, })} label={linkLabel} + data-test-subj={error ? `dashboardLink--${link.id}--error` : `dashboardLink--${link.id}`} /> ); }; diff --git a/src/plugins/navigation_embeddable/public/components/editor/navigation_embeddable_panel_editor.test.tsx b/src/plugins/navigation_embeddable/public/components/editor/navigation_embeddable_panel_editor.test.tsx new file mode 100644 index 0000000000000..250695cfc0a1d --- /dev/null +++ b/src/plugins/navigation_embeddable/public/components/editor/navigation_embeddable_panel_editor.test.tsx @@ -0,0 +1,129 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import userEvent from '@testing-library/user-event'; +import { render, screen, waitFor } from '@testing-library/react'; +import NavigationEmbeddablePanelEditor from './navigation_embeddable_panel_editor'; +import { NavEmbeddableStrings } from '../navigation_embeddable_strings'; +import { NavigationEmbeddableLink, NAV_VERTICAL_LAYOUT } from '../../../common/content_management'; +import { fetchDashboard } from '../dashboard_link/dashboard_link_tools'; + +jest.mock('../dashboard_link/dashboard_link_tools', () => { + return { + fetchDashboard: jest.fn().mockImplementation((id: string) => + Promise.resolve({ + id, + status: 'success', + attributes: { + title: `dashboard #${id}`, + description: '', + panelsJSON: [], + timeRestore: false, + version: '1', + }, + references: [], + }) + ), + }; +}); + +describe('NavigationEmbeddablePanelEditor', () => { + const defaultProps = { + onSaveToLibrary: jest.fn().mockImplementation(() => Promise.resolve()), + onAddToDashboard: jest.fn(), + onClose: jest.fn(), + isByReference: false, + }; + + const someLinks: NavigationEmbeddableLink[] = [ + { + id: 'foo', + type: 'dashboardLink' as const, + order: 1, + destination: '123', + }, + { + id: 'bar', + type: 'dashboardLink' as const, + order: 4, + destination: '456', + }, + { + id: 'bizz', + type: 'externalLink' as const, + order: 3, + destination: 'http://example.com', + }, + { + id: 'buzz', + type: 'externalLink' as const, + order: 2, + destination: 'http://elastic.co', + }, + ]; + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('shows empty state with no links', async () => { + render(); + expect(screen.getByTestId('navEmbeddable--panelEditor--title')).toHaveTextContent( + NavEmbeddableStrings.editor.panelEditor.getCreateFlyoutTitle() + ); + expect(screen.getByTestId('navEmbeddable--panelEditor--emptyPrompt')).toBeInTheDocument(); + expect(screen.getByTestId('navEmbeddable--panelEditor--saveBtn')).toBeDisabled(); + + await userEvent.click(screen.getByTestId('navEmbeddable--panelEditor--closeBtn')); + expect(defaultProps.onClose).toHaveBeenCalledTimes(1); + }); + + test('shows links in order', async () => { + const expectedLinkIds = [...someLinks].sort((a, b) => a.order - b.order).map(({ id }) => id); + render(); + await waitFor(() => expect(fetchDashboard).toHaveBeenCalledTimes(2)); + expect(screen.getByTestId('navEmbeddable--panelEditor--title')).toHaveTextContent( + NavEmbeddableStrings.editor.panelEditor.getEditFlyoutTitle() + ); + const draggableLinks = screen.getAllByTestId('navEmbeddable--panelEditor--draggableLink'); + expect(draggableLinks.length).toEqual(4); + + draggableLinks.forEach((link, idx) => { + expect(link).toHaveAttribute('data-rfd-draggable-id', expectedLinkIds[idx]); + }); + }); + + test('saving by reference panels calls onSaveToLibrary', async () => { + const orderedLinks = [...someLinks].sort((a, b) => a.order - b.order); + render( + + ); + await waitFor(() => expect(fetchDashboard).toHaveBeenCalledTimes(2)); + const saveButton = screen.getByTestId('navEmbeddable--panelEditor--saveBtn'); + await userEvent.click(saveButton); + await waitFor(() => expect(defaultProps.onSaveToLibrary).toHaveBeenCalledTimes(1)); + expect(defaultProps.onSaveToLibrary).toHaveBeenCalledWith(orderedLinks, NAV_VERTICAL_LAYOUT); + }); + + test('saving by value panel calls onAddToDashboard', async () => { + const orderedLinks = [...someLinks].sort((a, b) => a.order - b.order); + render( + + ); + await waitFor(() => expect(fetchDashboard).toHaveBeenCalledTimes(2)); + const saveButton = screen.getByTestId('navEmbeddable--panelEditor--saveBtn'); + await userEvent.click(saveButton); + expect(defaultProps.onAddToDashboard).toHaveBeenCalledTimes(1); + expect(defaultProps.onAddToDashboard).toHaveBeenCalledWith(orderedLinks, NAV_VERTICAL_LAYOUT); + }); +}); diff --git a/src/plugins/navigation_embeddable/public/components/editor/navigation_embeddable_panel_editor.tsx b/src/plugins/navigation_embeddable/public/components/editor/navigation_embeddable_panel_editor.tsx index 5cac5674daf52..3028c22fa48b3 100644 --- a/src/plugins/navigation_embeddable/public/components/editor/navigation_embeddable_panel_editor.tsx +++ b/src/plugins/navigation_embeddable/public/components/editor/navigation_embeddable_panel_editor.tsx @@ -159,7 +159,7 @@ const NavigationEmbeddablePanelEditor = ({ <>
- +

{isEditingExisting ? NavEmbeddableStrings.editor.panelEditor.getEditFlyoutTitle() @@ -201,6 +201,7 @@ const NavigationEmbeddablePanelEditor = ({ draggableId={link.id} customDragHandle={true} hasInteractiveChildren={true} + data-test-subj={`navEmbeddable--panelEditor--draggableLink`} > {(provided) => ( - + {NavEmbeddableStrings.editor.getCancelButtonLabel()} @@ -243,12 +249,14 @@ const NavigationEmbeddablePanelEditor = ({ setSaveByReference(!saveByReference)} + data-test-subj="navEmbeddable--panelEditor--saveByReferenceSwitch" /> @@ -257,11 +265,13 @@ const NavigationEmbeddablePanelEditor = ({ { if (saveByReference) { setIsSaving(true); diff --git a/src/plugins/navigation_embeddable/public/components/editor/navigation_embeddable_panel_editor_empty_prompt.tsx b/src/plugins/navigation_embeddable/public/components/editor/navigation_embeddable_panel_editor_empty_prompt.tsx index 1e4d1a30589f8..1fd18f4730837 100644 --- a/src/plugins/navigation_embeddable/public/components/editor/navigation_embeddable_panel_editor_empty_prompt.tsx +++ b/src/plugins/navigation_embeddable/public/components/editor/navigation_embeddable_panel_editor_empty_prompt.tsx @@ -18,7 +18,7 @@ export const NavigationEmbeddablePanelEditorEmptyPrompt = ({ addLink: () => Promise; }) => { return ( - + diff --git a/src/plugins/navigation_embeddable/public/components/external_link/external_link_component.test.tsx b/src/plugins/navigation_embeddable/public/components/external_link/external_link_component.test.tsx new file mode 100644 index 0000000000000..1945c9e762a7d --- /dev/null +++ b/src/plugins/navigation_embeddable/public/components/external_link/external_link_component.test.tsx @@ -0,0 +1,106 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; + +import userEvent from '@testing-library/user-event'; +import { createEvent, fireEvent, render, screen } from '@testing-library/react'; +import { + NavigationEmbeddable, + NavigationEmbeddableContext, +} from '../../embeddable/navigation_embeddable'; +import { mockNavigationEmbeddable } from '../../../common/mocks'; +import { NAV_VERTICAL_LAYOUT } from '../../../common/content_management'; +import { ExternalLinkComponent } from './external_link_component'; +import { coreServices } from '../../services/kibana_services'; +import { DEFAULT_URL_DRILLDOWN_OPTIONS } from '@kbn/ui-actions-enhanced-plugin/public'; + +describe('external link component', () => { + const defaultLinkInfo = { + destination: 'https://example.com', + order: 1, + id: 'foo', + type: 'externalLink' as const, + }; + + let navEmbeddable: NavigationEmbeddable; + beforeEach(async () => { + window.open = jest.fn(); + navEmbeddable = await mockNavigationEmbeddable({}); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('by default opens in new tab', async () => { + render( + + + + ); + + const link = await screen.findByTestId('externalLink--foo'); + expect(link).toBeInTheDocument(); + await userEvent.click(link); + expect(window.open).toHaveBeenCalledWith('https://example.com', '_blank'); + }); + + test('modified click does not trigger event.preventDefault', async () => { + const linkInfo = { + ...defaultLinkInfo, + options: { ...DEFAULT_URL_DRILLDOWN_OPTIONS, openInNewTab: false }, + }; + render( + + + + ); + const link = await screen.findByTestId('externalLink--foo'); + expect(link).toHaveTextContent('https://example.com'); + const clickEvent = createEvent.click(link, { ctrlKey: true }); + const preventDefault = jest.spyOn(clickEvent, 'preventDefault'); + fireEvent(link, clickEvent); + expect(preventDefault).toHaveBeenCalledTimes(0); + }); + + test('uses navigateToUrl when openInNewTab is false', async () => { + const linkInfo = { + ...defaultLinkInfo, + options: { ...DEFAULT_URL_DRILLDOWN_OPTIONS, openInNewTab: false }, + }; + render( + + + + ); + const link = await screen.findByTestId('externalLink--foo'); + await userEvent.click(link); + expect(coreServices.application.navigateToUrl).toBeCalledTimes(1); + expect(coreServices.application.navigateToUrl).toBeCalledWith('https://example.com'); + }); + + test('disables link when url validation fails', async () => { + const linkInfo = { + ...defaultLinkInfo, + destination: 'file://buzz', + }; + render( + + + + ); + const link = await screen.findByTestId('externalLink--foo--error'); + expect(link).toBeDisabled(); + /** + * TODO: We should test the tooltip content, but the component is disabled + * so it has pointer-events: none. This means we can not use userEvent.hover(). + * See https://testing-library.com/docs/ecosystem-user-event#pointer-events-options + */ + }); +}); diff --git a/src/plugins/navigation_embeddable/public/components/external_link/external_link_component.tsx b/src/plugins/navigation_embeddable/public/components/external_link/external_link_component.tsx index cd637f6764247..1f48fe70f1620 100644 --- a/src/plugins/navigation_embeddable/public/components/external_link/external_link_component.tsx +++ b/src/plugins/navigation_embeddable/public/components/external_link/external_link_component.tsx @@ -63,10 +63,12 @@ export const ExternalLinkComponent = ({ position: layout === NAV_VERTICAL_LAYOUT ? 'right' : 'bottom', repositionOnScroll: true, delay: 'long', + 'data-test-subj': `externalLink--${link.id}--tooltip`, }} iconType={error ? 'warning' : undefined} id={`externalLink--${link.id}`} label={link.label || link.destination} + data-test-subj={error ? `externalLink--${link.id}--error` : `externalLink--${link.id}`} href={destination} onClick={async (event) => { if (!destination) return; diff --git a/src/plugins/navigation_embeddable/public/mocks.tsx b/src/plugins/navigation_embeddable/public/mocks.tsx new file mode 100644 index 0000000000000..7268893babcba --- /dev/null +++ b/src/plugins/navigation_embeddable/public/mocks.tsx @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { coreMock } from '@kbn/core/public/mocks'; +import { dashboardPluginMock } from '@kbn/dashboard-plugin/public/mocks'; +import { embeddablePluginMock } from '@kbn/embeddable-plugin/public/mocks'; +import { contentManagementMock } from '@kbn/content-management-plugin/public/mocks'; +import { setKibanaServices } from './services/kibana_services'; + +export const setStubKibanaServices = () => { + const core = coreMock.createStart(); + + setKibanaServices(core, { + dashboard: dashboardPluginMock.createStartContract(), + embeddable: embeddablePluginMock.createStartContract(), + contentManagement: contentManagementMock.createStartContract(), + }); +}; diff --git a/src/plugins/navigation_embeddable/tsconfig.json b/src/plugins/navigation_embeddable/tsconfig.json index e5c172f210048..8a0b7e037928c 100644 --- a/src/plugins/navigation_embeddable/tsconfig.json +++ b/src/plugins/navigation_embeddable/tsconfig.json @@ -3,7 +3,7 @@ "compilerOptions": { "outDir": "target/types" }, - "include": ["public/**/*", "common/**/*", "server/**/*", "public/**/*.json"], + "include": ["*.ts", "public/**/*", "common/**/*", "server/**/*", "public/**/*.json"], "kbn_references": [ "@kbn/core", "@kbn/i18n",