diff --git a/x-pack/platform/plugins/private/upgrade_assistant/__jest__/client_integration/kibana_deprecations/deprecation_details_flyout/deprecation_details_flyout.test.ts b/x-pack/platform/plugins/private/upgrade_assistant/__jest__/client_integration/kibana_deprecations/deprecation_details_flyout/deprecation_details_flyout.test.ts deleted file mode 100644 index 36c0744831053..0000000000000 --- a/x-pack/platform/plugins/private/upgrade_assistant/__jest__/client_integration/kibana_deprecations/deprecation_details_flyout/deprecation_details_flyout.test.ts +++ /dev/null @@ -1,156 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { act } from 'react-dom/test-utils'; -import { deprecationsServiceMock } from '@kbn/core/public/mocks'; - -import { setupEnvironment } from '../../helpers'; -import { kibanaDeprecationsServiceHelpers } from '../service.mock'; -import type { KibanaTestBed } from '../kibana_deprecations.helpers'; -import { setupKibanaPage } from '../kibana_deprecations.helpers'; - -describe('Kibana deprecations - Deprecation details flyout', () => { - let testBed: KibanaTestBed; - const { - defaultMockedResponses: { mockedKibanaDeprecations }, - } = kibanaDeprecationsServiceHelpers; - const deprecationService = deprecationsServiceMock.createStartContract(); - beforeEach(async () => { - await act(async () => { - kibanaDeprecationsServiceHelpers.setLoadDeprecations({ deprecationService }); - - testBed = await setupKibanaPage(setupEnvironment().httpSetup, { - services: { - core: { - deprecations: deprecationService, - }, - }, - }); - }); - - testBed.component.update(); - }); - - describe('Deprecation with manual steps', () => { - test('renders flyout with single manual step as a standalone paragraph', async () => { - const { find, exists, actions } = testBed; - const manualDeprecation = mockedKibanaDeprecations[1]; - - await actions.table.clickDeprecationAt(0); - - expect(exists('kibanaDeprecationDetails')).toBe(true); - expect(find('kibanaDeprecationDetails.flyoutTitle').text()).toBe(manualDeprecation.title); - expect(find('manualStep').length).toBe(1); - }); - - test('renders flyout with multiple manual steps as a list', async () => { - const { find, exists, actions } = testBed; - const manualDeprecation = mockedKibanaDeprecations[1]; - - await actions.table.clickDeprecationAt(1); - - expect(exists('kibanaDeprecationDetails')).toBe(true); - expect(find('kibanaDeprecationDetails.flyoutTitle').text()).toBe(manualDeprecation.title); - expect(find('manualStepsListItem').length).toBe(3); - }); - - test(`doesn't show corrective actions title and steps if there aren't any`, async () => { - const { find, exists, actions } = testBed; - const manualDeprecation = mockedKibanaDeprecations[2]; - - await actions.table.clickDeprecationAt(2); - - expect(exists('kibanaDeprecationDetails')).toBe(true); - expect(exists('kibanaDeprecationDetails.manualStepsTitle')).toBe(false); - expect(exists('manualStepsListItem')).toBe(false); - expect(find('kibanaDeprecationDetails.flyoutTitle').text()).toBe(manualDeprecation.title); - }); - }); - - test('Shows documentationUrl when present', async () => { - const { find, actions } = testBed; - const deprecation = mockedKibanaDeprecations[1]; - - await actions.table.clickDeprecationAt(1); - - expect(find('kibanaDeprecationDetails.documentationLink').props().href).toBe( - deprecation.documentationUrl - ); - }); - - describe('Deprecation with automatic resolution', () => { - test('resolves deprecation successfully', async () => { - const { find, exists, actions } = testBed; - const quickResolveDeprecation = mockedKibanaDeprecations[0]; - - await actions.table.clickDeprecationAt(0); - - expect(exists('kibanaDeprecationDetails')).toBe(true); - expect(exists('kibanaDeprecationDetails.criticalDeprecationBadge')).toBe(true); - expect(find('kibanaDeprecationDetails.flyoutTitle').text()).toBe( - quickResolveDeprecation.title - ); - - // Quick resolve callout and button should display - expect(exists('quickResolveCallout')).toBe(true); - expect(exists('resolveButton')).toBe(true); - - await actions.flyout.clickResolveButton(); - - // Flyout should close after button click - expect(exists('kibanaDeprecationDetails')).toBe(false); - - // Reopen the flyout - await actions.table.clickDeprecationAt(0); - - // Resolve information should not display and Quick resolve button should be disabled - expect(exists('resolveSection')).toBe(false); - expect(exists('resolveButton')).toBe(false); - // Badge should be updated in flyout title - expect(exists('kibanaDeprecationDetails.resolvedDeprecationBadge')).toBe(true); - }); - - test('handles resolve failure', async () => { - const { find, exists, actions } = testBed; - const quickResolveDeprecation = mockedKibanaDeprecations[0]; - - kibanaDeprecationsServiceHelpers.setResolveDeprecations({ - deprecationService, - status: 'fail', - }); - - await actions.table.clickDeprecationAt(0); - - expect(exists('kibanaDeprecationDetails')).toBe(true); - expect(exists('kibanaDeprecationDetails.criticalDeprecationBadge')).toBe(true); - expect(find('kibanaDeprecationDetails.flyoutTitle').text()).toBe( - quickResolveDeprecation.title - ); - - // Quick resolve callout and button should display - expect(exists('quickResolveCallout')).toBe(true); - expect(exists('resolveButton')).toBe(true); - - await actions.flyout.clickResolveButton(); - - // Flyout should close after button click - expect(exists('kibanaDeprecationDetails')).toBe(false); - - // Reopen the flyout - await actions.table.clickDeprecationAt(0); - - // Verify error displays - expect(exists('quickResolveError')).toBe(true); - // Resolve information should display and Quick resolve button should be enabled - expect(exists('resolveSection')).toBe(true); - // Badge should remain the same - expect(exists('kibanaDeprecationDetails.criticalDeprecationBadge')).toBe(true); - expect(find('resolveButton').props().disabled).toBe(false); - expect(find('resolveButton').text()).toContain('Try again'); - }); - }); -}); diff --git a/x-pack/platform/plugins/private/upgrade_assistant/__jest__/client_integration/kibana_deprecations/deprecations_table/deprecations_table.test.ts b/x-pack/platform/plugins/private/upgrade_assistant/__jest__/client_integration/kibana_deprecations/deprecations_table/deprecations_table.test.ts deleted file mode 100644 index 277a61cfd6817..0000000000000 --- a/x-pack/platform/plugins/private/upgrade_assistant/__jest__/client_integration/kibana_deprecations/deprecations_table/deprecations_table.test.ts +++ /dev/null @@ -1,128 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { act } from 'react-dom/test-utils'; -import { deprecationsServiceMock } from '@kbn/core/public/mocks'; -import type { DeprecationsServiceStart } from '@kbn/core/public'; - -import { setupEnvironment } from '../../helpers'; -import { kibanaDeprecationsServiceHelpers } from '../service.mock'; -import type { KibanaTestBed } from '../kibana_deprecations.helpers'; -import { setupKibanaPage } from '../kibana_deprecations.helpers'; - -describe('Kibana deprecations - Deprecations table', () => { - let testBed: KibanaTestBed; - let deprecationService: jest.Mocked; - - const { - mockedKibanaDeprecations, - mockedCriticalKibanaDeprecations, - mockedWarningKibanaDeprecations, - mockedConfigKibanaDeprecations, - } = kibanaDeprecationsServiceHelpers.defaultMockedResponses; - - let httpSetup: ReturnType['httpSetup']; - beforeEach(async () => { - const mockEnvironment = setupEnvironment(); - httpSetup = mockEnvironment.httpSetup; - deprecationService = deprecationsServiceMock.createStartContract(); - - await act(async () => { - kibanaDeprecationsServiceHelpers.setLoadDeprecations({ deprecationService }); - - testBed = await setupKibanaPage(httpSetup, { - services: { - core: { - deprecations: deprecationService, - }, - }, - }); - }); - - testBed.component.update(); - }); - - test('renders deprecations', () => { - const { exists, table } = testBed; - - expect(exists('kibanaDeprecations')).toBe(true); - - const { tableCellsValues } = table.getMetaData('kibanaDeprecationsTable'); - - expect(tableCellsValues.length).toEqual(mockedKibanaDeprecations.length); - }); - - it('refreshes deprecation data', async () => { - const { actions } = testBed; - - expect(deprecationService.getAllDeprecations).toHaveBeenCalledTimes(1); - - await actions.table.clickRefreshButton(); - - expect(deprecationService.getAllDeprecations).toHaveBeenCalledTimes(2); - }); - - it('shows critical and warning deprecations count', () => { - const { find } = testBed; - - expect(find('criticalDeprecationsCount').text()).toContain( - String(mockedCriticalKibanaDeprecations.length) - ); - expect(find('warningDeprecationsCount').text()).toContain( - String(mockedWarningKibanaDeprecations.length) - ); - }); - - describe('Search bar', () => { - it('filters by "critical" status', async () => { - const { actions, table } = testBed; - - // Show only critical deprecations - await actions.searchBar.openStatusFilterDropdown(); - await actions.searchBar.filterByTitle('Critical'); - const { rows: criticalRows } = table.getMetaData('kibanaDeprecationsTable'); - expect(criticalRows.length).toEqual(mockedCriticalKibanaDeprecations.length); - - // Show all deprecations - await actions.searchBar.openStatusFilterDropdown(); - await actions.searchBar.filterByTitle('Critical'); - const { rows: allRows } = table.getMetaData('kibanaDeprecationsTable'); - expect(allRows.length).toEqual(mockedKibanaDeprecations.length); - }); - - it('filters by type', async () => { - const { table, actions } = testBed; - - await actions.searchBar.openTypeFilterDropdown(); - await actions.searchBar.filterByTitle('Config'); - - const { rows: configRows } = table.getMetaData('kibanaDeprecationsTable'); - - expect(configRows.length).toEqual(mockedConfigKibanaDeprecations.length); - }); - }); - - describe('No deprecations', () => { - beforeEach(async () => { - await act(async () => { - testBed = await setupKibanaPage(httpSetup); - }); - - const { component } = testBed; - - component.update(); - }); - - test('renders prompt', () => { - const { exists, find } = testBed; - expect(exists('noDeprecationsPrompt')).toBe(true); - expect(find('noDeprecationsPrompt').text()).toContain( - 'Your Kibana configuration is up to date' - ); - }); - }); -}); diff --git a/x-pack/platform/plugins/private/upgrade_assistant/__jest__/client_integration/kibana_deprecations/deprecations_table/error_handling.test.ts b/x-pack/platform/plugins/private/upgrade_assistant/__jest__/client_integration/kibana_deprecations/deprecations_table/error_handling.test.ts deleted file mode 100644 index fa9fca1b8498f..0000000000000 --- a/x-pack/platform/plugins/private/upgrade_assistant/__jest__/client_integration/kibana_deprecations/deprecations_table/error_handling.test.ts +++ /dev/null @@ -1,116 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { act } from 'react-dom/test-utils'; -import { deprecationsServiceMock } from '@kbn/core/public/mocks'; - -import { APP_LOGS_COUNT_CLUSTER_PRIVILEGES } from '../../../../common/constants'; -import { setupEnvironment } from '../../helpers'; -import { kibanaDeprecationsServiceHelpers } from '../service.mock'; -import type { KibanaTestBed } from '../kibana_deprecations.helpers'; -import { setupKibanaPage } from '../kibana_deprecations.helpers'; - -describe('Kibana deprecations - Deprecations table - Error handling', () => { - let testBed: KibanaTestBed; - const deprecationService = deprecationsServiceMock.createStartContract(); - - let httpSetup: ReturnType['httpSetup']; - beforeEach(async () => { - const mockEnvironment = setupEnvironment(); - httpSetup = mockEnvironment.httpSetup; - }); - - test('handles plugin errors', async () => { - await act(async () => { - kibanaDeprecationsServiceHelpers.setLoadDeprecations({ - deprecationService, - response: [ - ...kibanaDeprecationsServiceHelpers.defaultMockedResponses.mockedKibanaDeprecations, - { - domainId: 'failed_plugin_id_1', - title: 'Failed to fetch deprecations for "failed_plugin_id"', - message: `Failed to get deprecations info for plugin "failed_plugin_id".`, - level: 'fetch_error', - correctiveActions: { - manualSteps: ['Check Kibana server logs for error message.'], - }, - }, - { - domainId: 'failed_plugin_id_1', - title: 'Failed to fetch deprecations for "failed_plugin_id"', - message: `Failed to get deprecations info for plugin "failed_plugin_id".`, - level: 'fetch_error', - correctiveActions: { - manualSteps: ['Check Kibana server logs for error message.'], - }, - }, - { - domainId: 'failed_plugin_id_2', - title: 'Failed to fetch deprecations for "failed_plugin_id"', - message: `Failed to get deprecations info for plugin "failed_plugin_id".`, - level: 'fetch_error', - correctiveActions: { - manualSteps: ['Check Kibana server logs for error message.'], - }, - }, - ], - }); - - testBed = await setupKibanaPage(httpSetup, { - services: { - core: { - deprecations: deprecationService, - }, - }, - privileges: { - hasAllPrivileges: true, - missingPrivileges: { - cluster: [...APP_LOGS_COUNT_CLUSTER_PRIVILEGES], - index: [], - }, - }, - }); - }); - - const { component, exists, find } = testBed; - - component.update(); - - expect(exists('kibanaDeprecationErrors')).toBe(true); - // Should contain error about failed deprecations - expect(find('kibanaDeprecationErrors').text()).toContain( - 'Failed to get deprecation issues for these plugins: failed_plugin_id_1, failed_plugin_id_2.' - ); - // Should contain error about missing privilege - expect(find('kibanaDeprecationErrors').text()).toContain( - 'Certain issues might be missing due to missing cluster privilege for: manage_security' - ); - }); - - test('handles request error', async () => { - await act(async () => { - kibanaDeprecationsServiceHelpers.setLoadDeprecations({ - deprecationService, - mockRequestErrorMessage: 'Internal Server Error', - }); - - testBed = await setupKibanaPage(httpSetup, { - services: { - core: { - deprecations: deprecationService, - }, - }, - }); - }); - - const { component, find } = testBed; - component.update(); - expect(find('deprecationsPageLoadingError').text()).toContain( - 'Could not retrieve Kibana deprecation issues' - ); - }); -}); diff --git a/x-pack/platform/plugins/private/upgrade_assistant/__jest__/client_integration/kibana_deprecations/kibana_deprecations.helpers.ts b/x-pack/platform/plugins/private/upgrade_assistant/__jest__/client_integration/kibana_deprecations/kibana_deprecations.helpers.ts deleted file mode 100644 index 45bde87df7881..0000000000000 --- a/x-pack/platform/plugins/private/upgrade_assistant/__jest__/client_integration/kibana_deprecations/kibana_deprecations.helpers.ts +++ /dev/null @@ -1,130 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import { act } from 'react-dom/test-utils'; -import type { TestBed, AsyncTestBedConfig } from '@kbn/test-jest-helpers'; -import { registerTestBed, findTestSubject } from '@kbn/test-jest-helpers'; -import type { HttpSetup } from '@kbn/core/public'; -import { KibanaDeprecations } from '../../../public/application/components'; -import { WithAppDependencies } from '../helpers'; - -const testBedConfig: AsyncTestBedConfig = { - memoryRouter: { - initialEntries: ['/kibana_deprecations'], - componentRoutePath: '/kibana_deprecations', - }, - doMountAsync: true, -}; - -export type KibanaTestBed = TestBed & { - actions: ReturnType; -}; - -const createActions = (testBed: TestBed) => { - const { component, find, table } = testBed; - - /** - * User Actions - */ - const tableActions = { - clickRefreshButton: async () => { - await act(async () => { - find('refreshButton').simulate('click'); - }); - - component.update(); - }, - - clickDeprecationAt: async (index: number) => { - const { rows } = table.getMetaData('kibanaDeprecationsTable'); - - const deprecationDetailsLink = findTestSubject( - rows[index].reactWrapper, - 'deprecationDetailsLink' - ); - - await act(async () => { - deprecationDetailsLink.simulate('click'); - }); - component.update(); - }, - }; - - const openFilterByIndex = async (index: number) => { - await act(async () => { - // EUI doesn't support data-test-subj's on the filter buttons, so we must access via CSS selector - find('kibanaDeprecations') - .find('.euiSearchBar__filtersHolder') - .find('.euiPopover') - .find('button.euiFilterButton') - .at(index) - .simulate('click'); - }); - - component.update(); - - // Wait for the filter dropdown to be displayed - await new Promise(requestAnimationFrame); - }; - - const searchBarActions = { - openTypeFilterDropdown: async () => { - await openFilterByIndex(1); - }, - - openStatusFilterDropdown: async () => { - await openFilterByIndex(0); - }, - - filterByTitle: async (title: string) => { - // We need to read the document "body" as the filter dropdown (an EuiSelectable) - // is added in a portalled popover and not inside the component DOM tree. - const filterButton: HTMLButtonElement | null = document.body.querySelector( - `.euiSelectableListItem[title=${title}]` - ); - - expect(filterButton).not.toBeNull(); - - await act(async () => { - filterButton!.click(); - }); - - component.update(); - }, - }; - - const flyoutActions = { - clickResolveButton: async () => { - await act(async () => { - find('resolveButton').simulate('click'); - }); - - component.update(); - }, - }; - - return { - table: tableActions, - flyout: flyoutActions, - searchBar: searchBarActions, - }; -}; - -export const setupKibanaPage = async ( - httpSetup: HttpSetup, - overrides?: Record -): Promise => { - const initTestBed = registerTestBed( - WithAppDependencies(KibanaDeprecations, httpSetup, overrides), - testBedConfig - ); - const testBed = await initTestBed(); - - return { - ...testBed, - actions: createActions(testBed), - }; -}; diff --git a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/kibana_deprecations/deprecation_details_flyout.test.tsx b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/kibana_deprecations/deprecation_details_flyout.test.tsx new file mode 100644 index 0000000000000..53d0967a17a93 --- /dev/null +++ b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/kibana_deprecations/deprecation_details_flyout.test.tsx @@ -0,0 +1,293 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { I18nProvider } from '@kbn/i18n-react'; + +import type { KibanaDeprecationDetails } from './kibana_deprecations'; +import type { DeprecationResolutionState } from './kibana_deprecations'; +import { DeprecationDetailsFlyout } from './deprecation_details_flyout'; + +const renderWithProviders = (ui: React.ReactElement) => render({ui}); + +const createFeatureDeprecation = ( + overrides: Partial< + Omit + > = {} +): KibanaDeprecationDetails => ({ + id: 'test-id', + domainId: 'test_domain', + level: 'warning', + title: 'Test deprecation title', + message: 'Test deprecation message', + correctiveActions: { manualSteps: [] }, + ...overrides, + deprecationType: 'feature', + filterType: 'feature', +}); + +const mockCloseFlyout = jest.fn(); +const mockResolveDeprecation = jest.fn().mockResolvedValue(undefined); + +describe('DeprecationDetailsFlyout', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('WHEN deprecation has a single manual step', () => { + it('SHOULD render the step as a standalone paragraph', () => { + const deprecation = createFeatureDeprecation({ + id: 'dep-1', + level: 'critical', + title: 'Critical deprecation', + correctiveActions: { + manualSteps: ['Step 1'], + api: { method: 'POST' as const, path: '/test' }, + }, + }); + + renderWithProviders( + + ); + + expect(screen.getByTestId('flyoutTitle')).toHaveTextContent(deprecation.title); + expect(screen.getAllByTestId('manualStep')).toHaveLength(1); + }); + }); + + describe('WHEN deprecation has multiple manual steps', () => { + it('SHOULD render steps as a list', () => { + const deprecation = createFeatureDeprecation({ + id: 'dep-2', + title: 'Multi-step deprecation', + correctiveActions: { + manualSteps: ['Step 1', 'Step 2', 'Step 3'], + }, + }); + + renderWithProviders( + + ); + + expect(screen.getByTestId('flyoutTitle')).toHaveTextContent(deprecation.title); + expect(screen.getAllByTestId('manualStepsListItem')).toHaveLength(3); + }); + }); + + describe('WHEN deprecation has no manual steps', () => { + it('SHOULD not show corrective actions title and steps', () => { + const deprecation = createFeatureDeprecation({ + id: 'dep-3', + title: 'No-steps deprecation', + correctiveActions: { + manualSteps: [], + }, + }); + + renderWithProviders( + + ); + + expect(screen.getByTestId('flyoutTitle')).toHaveTextContent(deprecation.title); + expect(screen.queryByTestId('manualStepsTitle')).not.toBeInTheDocument(); + expect(screen.queryByTestId('manualStepsListItem')).not.toBeInTheDocument(); + }); + }); + + describe('WHEN deprecation has a documentation URL', () => { + it('SHOULD render the documentation link', () => { + const deprecation = createFeatureDeprecation({ + id: 'dep-doc', + documentationUrl: 'https://example.com/docs', + correctiveActions: { manualSteps: ['Step 1', 'Step 2'] }, + }); + + renderWithProviders( + + ); + + const docLink = screen.getByTestId('documentationLink') as HTMLAnchorElement; + expect(docLink.getAttribute('href')).toBe('https://example.com/docs'); + }); + }); + + describe('WHEN deprecation supports automatic resolution', () => { + it('SHOULD show quick resolve callout and button', () => { + const deprecation = createFeatureDeprecation({ + id: 'dep-auto', + level: 'critical', + correctiveActions: { + manualSteps: ['Step 1'], + api: { method: 'POST' as const, path: '/test' }, + }, + }); + + renderWithProviders( + + ); + + expect(screen.getByTestId('criticalDeprecationBadge')).toBeInTheDocument(); + expect(screen.getByTestId('quickResolveCallout')).toBeInTheDocument(); + expect(screen.getByTestId('resolveButton')).toBeInTheDocument(); + }); + + it('SHOULD show loading/disabled resolve button when resolution is in progress', () => { + const deprecation = createFeatureDeprecation({ + id: 'dep-in-progress', + level: 'critical', + correctiveActions: { + manualSteps: ['Step 1'], + api: { method: 'POST' as const, path: '/test' }, + }, + }); + + const resolutionState: DeprecationResolutionState = { + id: 'dep-in-progress', + resolveDeprecationStatus: 'in_progress', + }; + + renderWithProviders( + + ); + + expect(screen.getByTestId('resolveButton')).toBeDisabled(); + expect(screen.getByTestId('resolveButton')).toHaveTextContent('Resolution in progress'); + }); + + it('SHOULD call resolveDeprecation when resolve button is clicked', () => { + const deprecation = createFeatureDeprecation({ + id: 'dep-resolve', + level: 'critical', + correctiveActions: { + manualSteps: ['Step 1'], + api: { method: 'POST' as const, path: '/test' }, + }, + }); + + renderWithProviders( + + ); + + fireEvent.click(screen.getByTestId('resolveButton')); + expect(mockResolveDeprecation).toHaveBeenCalledTimes(1); + expect(mockResolveDeprecation).toHaveBeenCalledWith(deprecation); + }); + + it('SHOULD show resolved badge and hide resolve controls after successful resolution', () => { + const deprecation = createFeatureDeprecation({ + id: 'dep-resolved', + level: 'critical', + correctiveActions: { + manualSteps: ['Step 1'], + api: { method: 'POST' as const, path: '/test' }, + }, + }); + + const resolutionState: DeprecationResolutionState = { + id: 'dep-resolved', + resolveDeprecationStatus: 'ok', + }; + + renderWithProviders( + + ); + + expect(screen.queryByTestId('resolveSection')).not.toBeInTheDocument(); + expect(screen.queryByTestId('resolveButton')).not.toBeInTheDocument(); + expect(screen.getByTestId('resolvedDeprecationBadge')).toBeInTheDocument(); + }); + + it('SHOULD show error and retry button after failed resolution', () => { + const deprecation = createFeatureDeprecation({ + id: 'dep-failed', + level: 'critical', + correctiveActions: { + manualSteps: ['Step 1'], + api: { method: 'POST' as const, path: '/test' }, + }, + }); + + const resolutionState: DeprecationResolutionState = { + id: 'dep-failed', + resolveDeprecationStatus: 'fail', + resolveDeprecationError: 'resolve failed', + }; + + renderWithProviders( + + ); + + expect(screen.getByTestId('quickResolveError')).toBeInTheDocument(); + expect(screen.getByTestId('resolveSection')).toBeInTheDocument(); + expect(screen.getByTestId('criticalDeprecationBadge')).toBeInTheDocument(); + expect(screen.getByTestId('resolveButton')).toBeEnabled(); + expect(screen.getByTestId('resolveButton')).toHaveTextContent('Try again'); + }); + }); + + describe('WHEN deprecation does not support automatic resolution', () => { + it('SHOULD not show resolve button', () => { + const deprecation = createFeatureDeprecation({ + id: 'dep-manual', + correctiveActions: { + manualSteps: ['Step 1'], + }, + }); + + renderWithProviders( + + ); + + expect(screen.queryByTestId('resolveButton')).not.toBeInTheDocument(); + }); + }); +}); diff --git a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/kibana_deprecations/kibana_deprecations.test.tsx b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/kibana_deprecations/kibana_deprecations.test.tsx new file mode 100644 index 0000000000000..c435a70f2bda3 --- /dev/null +++ b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/kibana_deprecations/kibana_deprecations.test.tsx @@ -0,0 +1,342 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { type ReactNode } from 'react'; +import { render, screen, waitFor, fireEvent, act } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { I18nProvider } from '@kbn/i18n-react'; +import { createMemoryHistory } from 'history'; + +const mockGetAllDeprecations = jest.fn(); +const mockResolveDeprecation = jest.fn(); +const mockSetBreadcrumbs = jest.fn(); +const mockAddContent = jest.fn(); +const mockRemoveContent = jest.fn(); + +interface PrivilegesCheckResult { + hasPrivileges: boolean; + isLoading: boolean; + privilegesMissing: { cluster?: string[] }; +} + +jest.mock('@kbn/es-ui-shared-plugin/public', () => ({ + ...jest.requireActual('@kbn/es-ui-shared-plugin/public'), + SectionLoading: ({ children }: { children: ReactNode }) => ( +
{children}
+ ), + GlobalFlyout: { + useGlobalFlyout: () => ({ + addContent: mockAddContent, + removeContent: mockRemoveContent, + }), + }, + WithPrivileges: ({ children }: { children: (result: PrivilegesCheckResult) => ReactNode }) => + children({ hasPrivileges: true, isLoading: false, privilegesMissing: {} }), +})); + +const mockServices = { + core: { + deprecations: { + getAllDeprecations: mockGetAllDeprecations, + resolveDeprecation: mockResolveDeprecation, + }, + }, + breadcrumbs: { + setBreadcrumbs: mockSetBreadcrumbs, + }, +}; + +jest.mock('../../app_context', () => ({ + ...jest.requireActual('../../app_context'), + useAppContext: () => ({ + services: mockServices, + }), +})); + +import { KibanaDeprecationsList } from './kibana_deprecations'; + +const renderWithProviders = (ui: React.ReactElement) => render({ui}); + +const kibanaDeprecations = [ + { + correctiveActions: { + manualSteps: ['Step 1'], + api: { method: 'POST' as const, path: '/test' }, + }, + domainId: 'test_domain_1', + level: 'critical', + title: 'Test deprecation title 1', + message: 'Test deprecation message 1', + deprecationType: 'config', + configPath: 'test', + }, + { + correctiveActions: { + manualSteps: ['Step 1', 'Step 2', 'Step 3'], + }, + domainId: 'test_domain_2', + level: 'warning', + title: 'Test deprecation title 2', + documentationUrl: 'https://', + message: 'Test deprecation message 2', + deprecationType: 'feature', + }, + { + correctiveActions: { + manualSteps: [], + }, + domainId: 'test_domain_3', + level: 'warning', + title: 'Test deprecation title 3', + message: 'Test deprecation message 3', + deprecationType: 'feature', + }, +]; + +const mockHistory = createMemoryHistory({ initialEntries: ['/kibana_deprecations'] }); + +describe('KibanaDeprecationsList', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockGetAllDeprecations.mockResolvedValue(kibanaDeprecations); + }); + + describe('WHEN deprecations load successfully', () => { + it('SHOULD render deprecations table with correct items', async () => { + renderWithProviders( + + ); + + await waitFor(() => { + expect(screen.getByTestId('kibanaDeprecations')).toBeInTheDocument(); + }); + + expect(screen.getByTestId('kibanaDeprecationsTable')).toBeInTheDocument(); + expect(screen.getAllByTestId('row')).toHaveLength(kibanaDeprecations.length); + }); + + it('SHOULD show critical and warning deprecation counts', async () => { + renderWithProviders( + + ); + + await waitFor(() => { + expect(screen.getByTestId('kibanaDeprecations')).toBeInTheDocument(); + }); + + expect(screen.getByTestId('criticalDeprecationsCount')).toHaveTextContent('Critical: 1'); + expect(screen.getByTestId('warningDeprecationsCount')).toHaveTextContent('Warning: 2'); + }); + }); + + describe('WHEN deprecations service returns an error', () => { + it('SHOULD render the loading error prompt', async () => { + mockGetAllDeprecations.mockRejectedValue(new Error('Internal Server Error')); + + renderWithProviders( + + ); + + await waitFor(() => { + expect(screen.getByTestId('deprecationsPageLoadingError')).toBeInTheDocument(); + }); + + expect(screen.getByTestId('deprecationsPageLoadingError')).toHaveTextContent( + 'Could not retrieve Kibana deprecation issues' + ); + }); + }); + + describe('WHEN there are no deprecations', () => { + it('SHOULD render the no deprecations prompt', async () => { + mockGetAllDeprecations.mockResolvedValue([]); + + renderWithProviders( + + ); + + await waitFor(() => { + expect(screen.getByTestId('noDeprecationsPrompt')).toBeInTheDocument(); + }); + + expect(screen.getByTestId('noDeprecationsPrompt')).toHaveTextContent( + 'Your Kibana configuration is up to date' + ); + }); + }); + + describe('WHEN there are plugin fetch errors', () => { + it('SHOULD show the deprecation errors callout', async () => { + mockGetAllDeprecations.mockResolvedValue([ + ...kibanaDeprecations, + { + domainId: 'failed_plugin_id_1', + title: 'Failed to fetch deprecations for "failed_plugin_id"', + message: 'Failed to get deprecations info for plugin "failed_plugin_id".', + level: 'fetch_error', + correctiveActions: { + manualSteps: ['Check Kibana server logs for error message.'], + }, + }, + { + domainId: 'failed_plugin_id_1', + title: 'Failed to fetch deprecations for "failed_plugin_id"', + message: 'Failed to get deprecations info for plugin "failed_plugin_id".', + level: 'fetch_error', + correctiveActions: { + manualSteps: ['Check Kibana server logs for error message.'], + }, + }, + { + domainId: 'failed_plugin_id_2', + title: 'Failed to fetch deprecations for "failed_plugin_id"', + message: 'Failed to get deprecations info for plugin "failed_plugin_id".', + level: 'fetch_error', + correctiveActions: { + manualSteps: ['Check Kibana server logs for error message.'], + }, + }, + ]); + + renderWithProviders( + + ); + + await waitFor(() => { + expect(screen.getByTestId('kibanaDeprecationErrors')).toBeInTheDocument(); + }); + + expect(screen.getByTestId('kibanaDeprecationErrors')).toHaveTextContent( + 'Failed to get deprecation issues for these plugins: failed_plugin_id_1, failed_plugin_id_2.' + ); + }); + + it('SHOULD show missing privileges warning when hasPrivileges is false', async () => { + mockGetAllDeprecations.mockResolvedValue(kibanaDeprecations); + + renderWithProviders( + + ); + + await waitFor(() => { + expect(screen.getByTestId('kibanaDeprecationErrors')).toBeInTheDocument(); + }); + + expect(screen.getByTestId('kibanaDeprecationErrors')).toHaveTextContent( + 'Certain issues might be missing due to missing cluster privilege for: manage_security' + ); + }); + }); + + describe('WHEN refresh is triggered', () => { + it('SHOULD call getAllDeprecations again', async () => { + renderWithProviders( + + ); + + await waitFor(() => { + expect(screen.getByTestId('kibanaDeprecations')).toBeInTheDocument(); + }); + + expect(mockGetAllDeprecations).toHaveBeenCalledTimes(1); + + fireEvent.click(screen.getByTestId('refreshButton')); + + await waitFor(() => { + expect(mockGetAllDeprecations).toHaveBeenCalledTimes(2); + }); + }); + }); + + describe('WHEN a deprecation is opened and resolved', () => { + it('SHOULD add flyout content and resolve successfully', async () => { + mockResolveDeprecation.mockResolvedValue({ status: 'ok' }); + + renderWithProviders( + + ); + + await waitFor(() => { + expect(screen.getByTestId('kibanaDeprecations')).toBeInTheDocument(); + }); + + fireEvent.click(screen.getAllByTestId('row')[0]); + + await waitFor(() => { + expect(mockAddContent).toHaveBeenCalledTimes(1); + }); + + const flyoutConfig = mockAddContent.mock.calls[0][0]; + await act(async () => { + await flyoutConfig.props.resolveDeprecation(flyoutConfig.props.deprecation); + }); + + expect(mockResolveDeprecation).toHaveBeenCalledTimes(1); + expect(mockResolveDeprecation).toHaveBeenCalledWith(flyoutConfig.props.deprecation); + + await waitFor(() => { + expect(mockRemoveContent).toHaveBeenCalledTimes(1); + }); + + fireEvent.click(screen.getAllByTestId('row')[0]); + + await waitFor(() => { + expect(mockAddContent).toHaveBeenCalledTimes(2); + }); + + const reopenedFlyoutConfig = mockAddContent.mock.calls[1][0]; + expect(reopenedFlyoutConfig.props.deprecationResolutionState).toEqual( + expect.objectContaining({ resolveDeprecationStatus: 'ok' }) + ); + }); + + it('SHOULD store failure state and expose it when reopened', async () => { + mockResolveDeprecation.mockResolvedValue({ status: 'fail', reason: 'resolve failed' }); + + renderWithProviders( + + ); + + await waitFor(() => { + expect(screen.getByTestId('kibanaDeprecations')).toBeInTheDocument(); + }); + + fireEvent.click(screen.getAllByTestId('row')[0]); + + await waitFor(() => { + expect(mockAddContent).toHaveBeenCalledTimes(1); + }); + + const firstFlyoutConfig = mockAddContent.mock.calls[0][0]; + await act(async () => { + await firstFlyoutConfig.props.resolveDeprecation(firstFlyoutConfig.props.deprecation); + }); + + await waitFor(() => { + expect(mockRemoveContent).toHaveBeenCalledTimes(1); + }); + + fireEvent.click(screen.getAllByTestId('row')[0]); + + await waitFor(() => { + expect(mockAddContent).toHaveBeenCalledTimes(2); + }); + + const secondFlyoutConfig = mockAddContent.mock.calls[1][0]; + expect(secondFlyoutConfig.props.deprecationResolutionState).toEqual( + expect.objectContaining({ + resolveDeprecationStatus: 'fail', + resolveDeprecationError: 'resolve failed', + }) + ); + }); + }); +}); diff --git a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/kibana_deprecations/kibana_deprecations.tsx b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/kibana_deprecations/kibana_deprecations.tsx index 94e714ec6eacd..9cbb462527509 100644 --- a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/kibana_deprecations/kibana_deprecations.tsx +++ b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/kibana_deprecations/kibana_deprecations.tsx @@ -15,8 +15,12 @@ import { METRIC_TYPE } from '@kbn/analytics'; import { FormattedMessage } from '@kbn/i18n-react'; import type { DomainDeprecationDetails } from '@kbn/core/public'; -import type { MissingPrivileges } from '../../../shared_imports'; -import { WithPrivileges, SectionLoading, GlobalFlyout } from '../../../shared_imports'; +import { + type MissingPrivileges, + WithPrivileges, + SectionLoading, + GlobalFlyout, +} from '@kbn/es-ui-shared-plugin/public'; import { APP_LOGS_COUNT_CLUSTER_PRIVILEGES } from '../../../../common/constants'; import { useAppContext } from '../../app_context'; import { uiMetricService, UIM_KIBANA_DEPRECATIONS_PAGE_LOAD } from '../../lib/ui_metric'; diff --git a/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/kibana_deprecations/kibana_deprecations_table.test.tsx b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/kibana_deprecations/kibana_deprecations_table.test.tsx new file mode 100644 index 0000000000000..ce57d9fee010e --- /dev/null +++ b/x-pack/platform/plugins/private/upgrade_assistant/public/application/components/kibana_deprecations/kibana_deprecations_table.test.tsx @@ -0,0 +1,205 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { render, screen, fireEvent, waitFor, within } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { I18nProvider } from '@kbn/i18n-react'; + +import type { KibanaDeprecationDetails } from './kibana_deprecations'; +import { KibanaDeprecationsTable } from './kibana_deprecations_table'; + +const renderWithProviders = (ui: React.ReactElement) => render({ui}); + +interface ConfigDeprecationOverrides + extends Partial> { + configPath: string; +} + +const createFeatureDeprecation = ( + overrides: Partial< + Omit + > = {} +): KibanaDeprecationDetails => ({ + id: 'test-id', + domainId: 'test_domain', + level: 'warning', + title: 'Test deprecation', + message: 'Test message', + correctiveActions: { manualSteps: ['Step 1'] }, + ...overrides, + deprecationType: 'feature', + filterType: 'feature', +}); + +const createConfigDeprecation = ({ + configPath, + ...overrides +}: ConfigDeprecationOverrides): KibanaDeprecationDetails => ({ + id: 'test-id', + domainId: 'test_domain', + level: 'warning', + title: 'Test deprecation', + message: 'Test message', + correctiveActions: { manualSteps: ['Step 1'] }, + ...overrides, + deprecationType: 'config', + filterType: 'config', + configPath, +}); + +const mockDeprecations: KibanaDeprecationDetails[] = [ + createConfigDeprecation({ + id: 'dep-1', + domainId: 'test_domain_1', + level: 'critical', + title: 'Critical config deprecation', + configPath: 'test', + correctiveActions: { + manualSteps: ['Step 1'], + api: { method: 'POST' as const, path: '/test' }, + }, + }), + createFeatureDeprecation({ + id: 'dep-2', + domainId: 'test_domain_2', + level: 'warning', + title: 'Warning feature deprecation', + }), +]; + +const mockReload = jest.fn(); +const mockToggleFlyout = jest.fn(); + +describe('KibanaDeprecationsTable', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('SHOULD render all deprecations as rows', () => { + renderWithProviders( + + ); + + expect(screen.getByTestId('kibanaDeprecationsTable')).toBeInTheDocument(); + expect(screen.getAllByTestId('row')).toHaveLength(mockDeprecations.length); + }); + + it('SHOULD call reload when refresh button is clicked', () => { + renderWithProviders( + + ); + + fireEvent.click(screen.getByTestId('refreshButton')); + expect(mockReload).toHaveBeenCalledTimes(1); + }); + + it('SHOULD call toggleFlyout when deprecation link is clicked', () => { + renderWithProviders( + + ); + + fireEvent.click(screen.getAllByTestId('deprecationDetailsLink')[0]); + expect(mockToggleFlyout).toHaveBeenCalledWith(mockDeprecations[0]); + }); + + describe('Search bar', () => { + it('SHOULD filter by "critical" status', async () => { + renderWithProviders( + + ); + + fireEvent.click(screen.getByLabelText('Status Selection')); + await waitFor(() => { + const option = document.body.querySelector( + '.euiSelectableListItem[title="Critical"]' + ); + expect(option).not.toBeNull(); + option!.click(); + }); + + await waitFor(() => { + expect( + within(screen.getByTestId('kibanaDeprecationsTable')).getAllByTestId('row') + ).toHaveLength(1); + }); + + // Clear (restore all rows) + const clearButton = document.body.querySelector( + '[data-test-subj="clearSearchButton"]' + ); + expect(clearButton).not.toBeNull(); + fireEvent.click(clearButton!); + + await waitFor(() => { + expect( + within(screen.getByTestId('kibanaDeprecationsTable')).getAllByTestId('row') + ).toHaveLength(mockDeprecations.length); + }); + }); + + it('SHOULD filter by type', async () => { + renderWithProviders( + + ); + + fireEvent.click(screen.getByLabelText('Type Selection')); + await waitFor(() => { + const option = document.body.querySelector( + '.euiSelectableListItem[title="Config"]' + ); + expect(option).not.toBeNull(); + option!.click(); + }); + + await waitFor(() => { + expect( + within(screen.getByTestId('kibanaDeprecationsTable')).getAllByTestId('row') + ).toHaveLength(1); + }); + }); + }); + + it('SHOULD render empty table when no deprecations', () => { + renderWithProviders( + + ); + + expect(screen.getByTestId('kibanaDeprecationsTable')).toBeInTheDocument(); + expect(screen.queryAllByTestId('row')).toHaveLength(0); + }); +});