From: Jay Date: Thu, 31 Aug 2023 13:52:25 +0000 (+0200) Subject: SONAR-20254 Migrate app-level component tests to RTL X-Git-Tag: 10.2.0.77647~10 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=759862025d953bcaeb3919b28ef393511564e924;p=sonarqube.git SONAR-20254 Migrate app-level component tests to RTL --- diff --git a/server/sonar-web/src/main/js/app/components/AdminContainer.tsx b/server/sonar-web/src/main/js/app/components/AdminContainer.tsx index 80db7939184..ab8cfaa9868 100644 --- a/server/sonar-web/src/main/js/app/components/AdminContainer.tsx +++ b/server/sonar-web/src/main/js/app/components/AdminContainer.tsx @@ -103,7 +103,7 @@ export class AdminContainer extends React.PureComponent { if (this.mounted) { this.setState({ systemStatus: status }); - document.location.reload(); + window.location.reload(); } }, () => {} diff --git a/server/sonar-web/src/main/js/app/components/__tests__/AdminContainer-test.tsx b/server/sonar-web/src/main/js/app/components/__tests__/AdminContainer-test.tsx index cd238b028f4..143cf6af20f 100644 --- a/server/sonar-web/src/main/js/app/components/__tests__/AdminContainer-test.tsx +++ b/server/sonar-web/src/main/js/app/components/__tests__/AdminContainer-test.tsx @@ -17,34 +17,152 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { shallow } from 'enzyme'; +import userEvent from '@testing-library/user-event'; import * as React from 'react'; +import { Route, useOutletContext } from 'react-router-dom'; +import { getSystemStatus, waitSystemUPStatus } from '../../../api/system'; import { mockAppState } from '../../../helpers/testMocks'; +import { renderAppRoutes } from '../../../helpers/testReactTestingUtils'; +import { byLabelText, byRole, byText } from '../../../helpers/testSelector'; +import { AdminPagesContext } from '../../../types/admin'; import { AdminContainer, AdminContainerProps } from '../AdminContainer'; +import AdminContext from '../AdminContext'; + +jest.mock('../../../api/navigation', () => ({ + getSettingsNavigation: jest + .fn() + .mockResolvedValue({ extensions: [{ key: 'asd', name: 'asdf' }] }), +})); jest.mock('../../../api/plugins', () => ({ - getSettingsNavigation: jest.fn().mockResolvedValue({}), - getPendingPlugins: jest.fn().mockResolvedValue({}), + getPendingPlugins: jest.fn().mockResolvedValue({ + installing: [{ key: '1', name: 'installing' }], + updating: [ + { key: '2', name: 'updating' }, + { key: '2b', name: 'update this too' }, + ], + removing: [{ key: '3', name: 'removing' }], + }), })); jest.mock('../../../api/system', () => ({ - getSystemStatus: jest.fn().mockResolvedValue({}), + getSystemStatus: jest.fn().mockResolvedValue({ status: 'DOWN' }), + waitSystemUPStatus: jest.fn().mockResolvedValue({ status: 'RESTARTING' }), })); -it('should render correctly', () => { - const wrapper = shallowRender(); - expect(wrapper).toMatchSnapshot(); +const originalLocation = window.location; +const reload = jest.fn(); + +beforeAll(() => { + Object.defineProperty(window, 'location', { + writable: true, + value: { reload }, + }); }); -function shallowRender(props: Partial = {}) { - return shallow( - { + Object.defineProperty(window, 'location', { + writable: true, + value: originalLocation, + }); +}); + +it('should render nav and provide context to children', async () => { + const user = userEvent.setup(); + renderAdminContainer(); + + expect(await ui.navHeader.find()).toBeInTheDocument(); + + expect(ui.pagesList.byRole('listitem').getAll()).toHaveLength(1); + expect(ui.pagesList.byText('asdf').get()).toBeInTheDocument(); + + expect(ui.pluginsInstallingList.byRole('listitem').getAll()).toHaveLength(1); + expect(ui.pluginsInstallingList.byText('installing').get()).toBeInTheDocument(); + + expect(ui.pluginsUpdatingList.byRole('listitem').getAll()).toHaveLength(2); + expect(ui.pluginsUpdatingList.byText('updating').get()).toBeInTheDocument(); + expect(ui.pluginsUpdatingList.byText('update this too').get()).toBeInTheDocument(); + + expect(ui.pluginsRemovingList.byRole('listitem').getAll()).toHaveLength(1); + expect(ui.pluginsRemovingList.byText('removing').get()).toBeInTheDocument(); + + expect(byText('DOWN').get()).toBeInTheDocument(); + + // Trigger a status update + jest.mocked(getSystemStatus).mockResolvedValueOnce({ id: '', version: '', status: 'RESTARTING' }); + jest.mocked(waitSystemUPStatus).mockResolvedValueOnce({ id: '', version: '', status: 'UP' }); + await user.click(ui.fetchStatusButton.get()); + + expect(await byText('UP').find()).toBeInTheDocument(); + expect(reload).toHaveBeenCalled(); +}); + +function renderAdminContainer(props: Partial = {}) { + return renderAppRoutes('admin', () => ( + + } > -
- + } /> + + )); +} + +function TestChildComponent() { + const { adminPages } = useOutletContext(); + + const { fetchPendingPlugins, fetchSystemStatus, pendingPlugins, systemStatus } = + React.useContext(AdminContext); + + return ( +
+
    + {adminPages.map((page) => ( +
  • {page.name}
  • + ))} +
+ +
    + {pendingPlugins.installing.map((p) => ( +
  • {p.name}
  • + ))} +
+
    + {pendingPlugins.removing.map((p) => ( +
  • {p.name}
  • + ))} +
+
    + {pendingPlugins.updating.map((p) => ( +
  • {p.name}
  • + ))} +
+ + + {systemStatus} + +
); } + +const ui = { + navHeader: byRole('heading', { name: 'layout.settings' }), + pagesList: byLabelText('pages'), + pluginsInstallingList: byLabelText('plugins - installing'), + pluginsUpdatingList: byLabelText('plugins - updating'), + pluginsRemovingList: byLabelText('plugins - removing'), + + fetchPluginsButton: byRole('button', { name: 'fetch plugins' }), + fetchStatusButton: byRole('button', { name: 'fetch status' }), +}; diff --git a/server/sonar-web/src/main/js/app/components/__tests__/App-test.tsx b/server/sonar-web/src/main/js/app/components/__tests__/App-test.tsx index a4726fa085b..e8f00fce9f9 100644 --- a/server/sonar-web/src/main/js/app/components/__tests__/App-test.tsx +++ b/server/sonar-web/src/main/js/app/components/__tests__/App-test.tsx @@ -17,32 +17,32 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { shallow } from 'enzyme'; import * as React from 'react'; import { mockAppState } from '../../../helpers/testMocks'; +import { renderComponent } from '../../../helpers/testReactTestingUtils'; import { App } from '../App'; -it('should render correctly', () => { - expect(shallowRender()).toMatchSnapshot('default'); - expect( - shallowRender({ - appState: mockAppState({ - settings: { - 'sonar.lf.enableGravatar': 'true', - 'sonar.lf.gravatarServerUrl': 'http://example.com', - }, - }), - }) - ).toMatchSnapshot('with gravatar'); +it('should render correctly with gravatar', () => { + renderApp({ + appState: mockAppState({ + settings: { + 'sonar.lf.enableGravatar': 'true', + 'sonar.lf.gravatarServerUrl': 'http://example.com', + }, + }), + }); + + // eslint-disable-next-line testing-library/no-node-access + expect(document.head.querySelector('link')).toHaveAttribute('href', 'http://example.com'); }); it('should correctly set the scrollbar width as a custom property', () => { - shallowRender(); + renderApp(); expect(document.body.style.getPropertyValue('--sbw')).toBe('0px'); }); -function shallowRender(props: Partial = {}) { - return shallow( +function renderApp(props: Partial = {}) { + return renderComponent( { - let close: () => void; - return { - ...jest.requireActual('react'), - useEffect: jest.fn().mockImplementation((f) => { - close = f(); - }), - clean: () => { - close(); - }, - }; -}); +it('should render correctly', async () => { + const user = userEvent.setup(); + renderKeyboardShortcutsModal(); -afterEach(() => { - if ((React as any).clean as () => void) { - (React as any).clean(); - } -}); + expect(ui.modalTitle.query()).not.toBeInTheDocument(); -it('should render correctly', () => { - const wrapper = shallowRender(); - expect(wrapper).toMatchSnapshot('hidden'); + await act(() => user.keyboard('?')); - document.dispatchEvent(new KeyboardEvent('keydown', { key: '?' })); + expect(ui.modalTitle.get()).toBeInTheDocument(); - expect(wrapper).toMatchSnapshot('visible'); -}); + await user.click(ui.closeButton.get()); -it('should close correctly', () => { - const wrapper = shallowRender(); - document.dispatchEvent(new KeyboardEvent('keydown', { key: '?' })); + expect(ui.modalTitle.query()).not.toBeInTheDocument(); +}); - wrapper.find(Modal).props().onRequestClose!(mockEvent()); +it('should ignore other keydownes', async () => { + const user = userEvent.setup(); + renderKeyboardShortcutsModal(); - expect(wrapper.type()).toBeNull(); -}); + await act(() => user.keyboard('!')); -it('should ignore other keydownes', () => { - const wrapper = shallowRender(); - document.dispatchEvent(new KeyboardEvent('keydown', { key: '!' })); - expect(wrapper.type()).toBeNull(); + expect(ui.modalTitle.query()).not.toBeInTheDocument(); }); -it.each([['input'], ['select'], ['textarea']])('should ignore events on a %s', (type) => { - const wrapper = shallowRender(); - - const fakeEvent = new KeyboardEvent('keydown', { key: '!' }); +it('should ignore events in an input', async () => { + const user = userEvent.setup(); - Object.defineProperty(fakeEvent, 'target', { - value: document.createElement(type), - }); + renderKeyboardShortcutsModal(); - document.dispatchEvent(fakeEvent); + await user.click(ui.textInput.get()); + await act(() => user.keyboard('?')); - expect(wrapper.type()).toBeNull(); + expect(ui.modalTitle.query()).not.toBeInTheDocument(); }); -function shallowRender() { - return shallow(); +function renderKeyboardShortcutsModal() { + return renderComponent( + <> + + + + ); } + +const ui = { + modalTitle: byRole('heading', { name: 'keyboard_shortcuts.title' }), + closeButton: byRole('button', { name: 'close' }), + + textInput: byRole('textbox'), +}; diff --git a/server/sonar-web/src/main/js/app/components/__tests__/MigrationContainer-test.tsx b/server/sonar-web/src/main/js/app/components/__tests__/MigrationContainer-test.tsx index affc20629ea..d04c07c03f2 100644 --- a/server/sonar-web/src/main/js/app/components/__tests__/MigrationContainer-test.tsx +++ b/server/sonar-web/src/main/js/app/components/__tests__/MigrationContainer-test.tsx @@ -17,9 +17,11 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { shallow } from 'enzyme'; import * as React from 'react'; +import { Route } from 'react-router-dom'; import { getSystemStatus } from '../../../helpers/system'; +import { renderAppRoutes } from '../../../helpers/testReactTestingUtils'; +import { byText } from '../../../helpers/testSelector'; import MigrationContainer from '../MigrationContainer'; jest.mock('../../../helpers/system', () => ({ @@ -49,14 +51,26 @@ afterAll(() => { it('should render correctly if system is up', () => { (getSystemStatus as jest.Mock).mockReturnValueOnce('UP'); - expect(shallowRender()).toMatchSnapshot(); + + renderMigrationContainer(); + + expect(byText('children').get()).toBeInTheDocument(); }); it('should render correctly if system is starting', () => { (getSystemStatus as jest.Mock).mockReturnValueOnce('STARTING'); - expect(shallowRender()).toMatchSnapshot(); + + renderMigrationContainer(); + + expect( + byText('/maintenance?return_to=%2Fprojects%3Fquery%3Dtoto%23hash').get() + ).toBeInTheDocument(); }); -function shallowRender() { - return shallow(); +function renderMigrationContainer() { + return renderAppRoutes('/', () => ( + }> + children
} /> +
+ )); } diff --git a/server/sonar-web/src/main/js/app/components/__tests__/PageTracker-test.tsx b/server/sonar-web/src/main/js/app/components/__tests__/PageTracker-test.tsx index 10504a10e74..f59f12f53bb 100644 --- a/server/sonar-web/src/main/js/app/components/__tests__/PageTracker-test.tsx +++ b/server/sonar-web/src/main/js/app/components/__tests__/PageTracker-test.tsx @@ -17,12 +17,15 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { shallow } from 'enzyme'; +import userEvent from '@testing-library/user-event'; import * as React from 'react'; +import { Link } from 'react-router-dom'; import { installScript } from '../../../helpers/extensions'; import { getWebAnalyticsPageHandlerFromCache } from '../../../helpers/extensionsHandler'; -import { mockAppState, mockLocation } from '../../../helpers/testMocks'; -import { PageTracker } from '../PageTracker'; +import { mockAppState } from '../../../helpers/testMocks'; +import { renderComponent } from '../../../helpers/testReactTestingUtils'; +import { byRole } from '../../../helpers/testSelector'; +import PageTracker from '../PageTracker'; jest.mock('../../../helpers/extensions', () => ({ installScript: jest.fn().mockResolvedValue({}), @@ -43,28 +46,41 @@ afterEach(() => { }); it('should not trigger if no analytics system is given', () => { - const wrapper = shallowRender(); - expect(wrapper).toMatchSnapshot(); + renderPageTracker(); + expect(installScript).not.toHaveBeenCalled(); }); -it('should work for WebAnalytics plugin', () => { +it('should work for WebAnalytics plugin', async () => { + const user = userEvent.setup({ delay: null }); const pageChange = jest.fn(); const webAnalyticsJsPath = '/static/pluginKey/web_analytics.js'; - const wrapper = shallowRender({ appState: mockAppState({ webAnalyticsJsPath }) }); + renderPageTracker(mockAppState({ webAnalyticsJsPath })); - expect(wrapper).toMatchSnapshot(); - expect(wrapper.find('Helmet').prop('onChangeClientState')).toBe(wrapper.instance().trackPage); expect(installScript).toHaveBeenCalledWith(webAnalyticsJsPath, 'head'); - (getWebAnalyticsPageHandlerFromCache as jest.Mock).mockReturnValueOnce(pageChange); - wrapper.instance().trackPage(); + jest.mocked(getWebAnalyticsPageHandlerFromCache).mockClear().mockReturnValueOnce(pageChange); + + // trigger trackPage + await user.click(byRole('link').get()); + jest.runAllTimers(); - expect(pageChange).toHaveBeenCalledWith('/path'); + expect(pageChange).toHaveBeenCalledWith('/newpath'); }); -function shallowRender(props: Partial = {}) { - return shallow( - +function renderPageTracker(appState = mockAppState()) { + return renderComponent(, '', { appState }); +} + +function WrappingComponent() { + const [metatag, setmetatag] = React.useState(null); + + return ( + <> + {metatag} + setmetatag()}> + trigger change + + ); } diff --git a/server/sonar-web/src/main/js/app/components/__tests__/ProjectAdminContainer-test.tsx b/server/sonar-web/src/main/js/app/components/__tests__/ProjectAdminContainer-test.tsx index d3ed20ab64f..b3acd752284 100644 --- a/server/sonar-web/src/main/js/app/components/__tests__/ProjectAdminContainer-test.tsx +++ b/server/sonar-web/src/main/js/app/components/__tests__/ProjectAdminContainer-test.tsx @@ -17,10 +17,12 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { mount, shallow } from 'enzyme'; import * as React from 'react'; +import { Route } from 'react-router-dom'; import handleRequiredAuthorization from '../../../app/utils/handleRequiredAuthorization'; import { mockComponent } from '../../../helpers/mocks/component'; +import { renderAppRoutes } from '../../../helpers/testReactTestingUtils'; +import { byText } from '../../../helpers/testSelector'; import { ProjectAdminContainer } from '../ProjectAdminContainer'; jest.mock('../../utils/handleRequiredAuthorization', () => { @@ -28,30 +30,33 @@ jest.mock('../../utils/handleRequiredAuthorization', () => { }); it('should render correctly', () => { - expect(shallowRender()).toMatchSnapshot(); + renderProjectAdminContainer(); + + expect(byText('children').get()).toBeInTheDocument(); }); it('should redirect for authorization if needed', () => { jest.useFakeTimers(); - mountRender({ component: mockComponent({ configuration: { showSettings: false } }) }); + renderProjectAdminContainer({ + component: mockComponent({ configuration: { showSettings: false } }), + }); jest.runAllTimers(); expect(handleRequiredAuthorization).toHaveBeenCalled(); jest.useRealTimers(); }); -function mountRender(props: Partial = {}) { - return mount(createComponent(props)); -} - -function shallowRender(props: Partial = {}) { - return shallow(createComponent(props)); -} - -function createComponent(props: Partial = {}) { - return ( - - ); +function renderProjectAdminContainer(props: Partial = {}) { + return renderAppRoutes('project/settings', () => ( + + } + > + children} /> + + )); } diff --git a/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/AdminContainer-test.tsx.snap b/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/AdminContainer-test.tsx.snap deleted file mode 100644 index 631a82fbf2e..00000000000 --- a/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/AdminContainer-test.tsx.snap +++ /dev/null @@ -1,47 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render correctly 1`] = ` - - - - - - - -`; diff --git a/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/App-test.tsx.snap b/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/App-test.tsx.snap deleted file mode 100644 index 1937a131c98..00000000000 --- a/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/App-test.tsx.snap +++ /dev/null @@ -1,22 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render correctly: default 1`] = ` - - - - - -`; - -exports[`should render correctly: with gravatar 1`] = ` - - - - - - - -`; diff --git a/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/KeyboardShortcutsModal-test.tsx.snap b/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/KeyboardShortcutsModal-test.tsx.snap deleted file mode 100644 index f63709831aa..00000000000 --- a/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/KeyboardShortcutsModal-test.tsx.snap +++ /dev/null @@ -1,583 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render correctly: hidden 1`] = `""`; - -exports[`should render correctly: visible 1`] = ` - -
-

- keyboard_shortcuts.title -

- - keyboard_shortcuts.disable_link - -
-
-
-
-

- keyboard_shortcuts.global.title -

- - - - - - - - - - - - - - - - - -
- keyboard_shortcuts.shortcut - - keyboard_shortcuts.action -
- - s - - - keyboard_shortcuts.global.search -
- - ? - - - keyboard_shortcuts.global.open_shortcuts -
-
-
-

- keyboard_shortcuts.issues_page.title -

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- keyboard_shortcuts.shortcut - - keyboard_shortcuts.action -
- - ↑ - - - ↓ - - - keyboard_shortcuts.issues_page.navigate -
- - → - - - keyboard_shortcuts.issues_page.source_code -
- - ← - - - keyboard_shortcuts.issues_page.back -
- - alt - - - + - - - ↑ - - - ↓ - - - keyboard_shortcuts.issues_page.navigate_locations -
- - alt - - - + - - - ← - - - → - - - keyboard_shortcuts.issues_page.switch_flows -
- - f - - - keyboard_shortcuts.issues_page.transition -
- - a - - - keyboard_shortcuts.issues_page.assign -
- - m - - - keyboard_shortcuts.issues_page.assign_to_me -
- - i - - - keyboard_shortcuts.issues_page.severity -
- - c - - - keyboard_shortcuts.issues_page.comment -
- - ctrl - - - + - - - enter - - - keyboard_shortcuts.issues_page.submit_comment -
- - t - - - keyboard_shortcuts.issues_page.tags -
-
-
-
-
-

- keyboard_shortcuts.code_page.title -

- - - - - - - - - - - - - - - - - - - - - -
- keyboard_shortcuts.shortcut - - keyboard_shortcuts.action -
- - ↑ - - - ↓ - - - keyboard_shortcuts.code_page.select_files -
- - → - - - keyboard_shortcuts.code_page.open_file -
- - ← - - - keyboard_shortcuts.code_page.back -
-
-
-

- keyboard_shortcuts.measures_page.title -

- - - - - - - - - - - - - - - - - - - - - -
- keyboard_shortcuts.shortcut - - keyboard_shortcuts.action -
- - ↑ - - - ↓ - - - keyboard_shortcuts.measures_page.select_files -
- - → - - - keyboard_shortcuts.measures_page.open_file -
- - ← - - - keyboard_shortcuts.measures_page.back -
-
-
-

- keyboard_shortcuts.rules_page.title -

- - - - - - - - - - - - - - - - - - - - - -
- keyboard_shortcuts.shortcut - - keyboard_shortcuts.action -
- - ↑ - - - ↓ - - - keyboard_shortcuts.rules_page.navigate -
- - → - - - keyboard_shortcuts.rules_page.rule_details -
- - ← - - - keyboard_shortcuts.rules_page.back -
-
-
-
-
- -
-
-`; diff --git a/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/MigrationContainer-test.tsx.snap b/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/MigrationContainer-test.tsx.snap deleted file mode 100644 index 304a45ecbda..00000000000 --- a/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/MigrationContainer-test.tsx.snap +++ /dev/null @@ -1,14 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render correctly if system is starting 1`] = ` - -`; - -exports[`should render correctly if system is up 1`] = ``; diff --git a/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/PageTracker-test.tsx.snap b/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/PageTracker-test.tsx.snap deleted file mode 100644 index e8b8c189454..00000000000 --- a/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/PageTracker-test.tsx.snap +++ /dev/null @@ -1,18 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should not trigger if no analytics system is given 1`] = ` - -`; - -exports[`should work for WebAnalytics plugin 1`] = ` - -`; diff --git a/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/ProjectAdminContainer-test.tsx.snap b/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/ProjectAdminContainer-test.tsx.snap deleted file mode 100644 index 0d01829fb3b..00000000000 --- a/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/ProjectAdminContainer-test.tsx.snap +++ /dev/null @@ -1,10 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render correctly 1`] = ` -
- - -
-`;