]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-20254 Using RTL for component container
authorRevanshu Paliwal <revanshu.paliwal@sonarsource.com>
Tue, 29 Aug 2023 15:13:39 +0000 (17:13 +0200)
committersonartech <sonartech@sonarsource.com>
Thu, 31 Aug 2023 20:02:56 +0000 (20:02 +0000)
server/sonar-web/src/main/js/app/components/ComponentContainer.tsx
server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx
server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/ComponentContainer-test.tsx.snap [deleted file]

index 60d59d84e5307a26c6091a843904fc17bf1b8ef7..920eaee6ac6b3204100b47153e700ead724dc0fb 100644 (file)
@@ -44,7 +44,7 @@ import withAvailableFeatures, {
 import { ComponentContext } from './componentContext/ComponentContext';
 import ComponentNav from './nav/component/ComponentNav';
 
-interface Props extends WithAvailableFeaturesProps {
+export interface Props extends WithAvailableFeaturesProps {
   location: Location;
   router: Router;
 }
index 6b53e8980df8cd737cc7be24320751f14225a89b..d3ba659c70d1b472529bc9d89a36f3541db85482 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 
-import { shallow } from 'enzyme';
-import * as React from 'react';
-import { getProjectAlmBinding, validateProjectAlmBinding } from '../../../api/alm-settings';
-import { getBranches, getPullRequests } from '../../../api/branches';
+import { screen, waitFor } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import React, { useContext } from 'react';
+import { Route } from 'react-router-dom';
+import { validateProjectAlmBinding } from '../../../api/alm-settings';
 import { getTasksForComponent } from '../../../api/ce';
 import { getComponentData } from '../../../api/components';
 import { getComponentNavigation } from '../../../api/navigation';
 import { mockProjectAlmBindingConfigurationErrors } from '../../../helpers/mocks/alm-settings';
-import { mockBranch, mockMainBranch, mockPullRequest } from '../../../helpers/mocks/branch-like';
 import { mockComponent } from '../../../helpers/mocks/component';
-import { mockTask } from '../../../helpers/mocks/tasks';
 import { HttpStatus } from '../../../helpers/request';
 import { mockLocation, mockRouter } from '../../../helpers/testMocks';
-import { waitAndUpdate } from '../../../helpers/testUtils';
-import { ComponentQualifier, Visibility } from '../../../types/component';
+import { renderAppRoutes, renderComponent } from '../../../helpers/testReactTestingUtils';
+import { byRole, byText } from '../../../helpers/testSelector';
+import { ComponentQualifier } from '../../../types/component';
 import { TaskStatuses, TaskTypes } from '../../../types/tasks';
-import { Component } from '../../../types/types';
 import handleRequiredAuthorization from '../../utils/handleRequiredAuthorization';
-import { ComponentContainer } from '../ComponentContainer';
-
-jest.mock('../../../api/branches', () => {
-  const { mockMainBranch, mockPullRequest } = jest.requireActual(
-    '../../../helpers/mocks/branch-like'
-  );
-
-  return {
-    getBranches: jest
-      .fn()
-      .mockResolvedValue([mockMainBranch({ status: { qualityGateStatus: 'OK' } })]),
-    getPullRequests: jest
-      .fn()
-      .mockResolvedValue([
-        mockPullRequest({ key: 'pr-89', status: { qualityGateStatus: 'ERROR' } }),
-        mockPullRequest({ key: 'pr-90', title: 'PR Feature 2' }),
-      ]),
-  };
-});
+import ComponentContainer, { Props } from '../ComponentContainer';
+import { ComponentContext } from '../componentContext/ComponentContext';
 
 jest.mock('../../../api/ce', () => ({
-  getAnalysisStatus: jest.fn().mockResolvedValue({ component: { warnings: [] } }),
   getTasksForComponent: jest.fn().mockResolvedValue({ queue: [] }),
 }));
 
 jest.mock('../../../api/components', () => ({
-  getComponentData: jest.fn().mockResolvedValue({ component: { analysisDate: '2018-07-30' } }),
+  getComponentData: jest
+    .fn()
+    .mockResolvedValue({ component: { name: 'component name', analysisDate: '2018-07-30' } }),
 }));
 
 jest.mock('../../../api/navigation', () => ({
@@ -73,7 +56,6 @@ jest.mock('../../../api/navigation', () => ({
 }));
 
 jest.mock('../../../api/alm-settings', () => ({
-  getProjectAlmBinding: jest.fn().mockResolvedValue(undefined),
   validateProjectAlmBinding: jest.fn().mockResolvedValue(undefined),
 }));
 
@@ -82,251 +64,242 @@ jest.mock('../../utils/handleRequiredAuthorization', () => ({
   default: jest.fn(),
 }));
 
-// eslint-disable-next-line react/function-component-definition
-const Inner = () => <div />;
-
-beforeEach(() => {
-  jest.useFakeTimers();
-  jest.clearAllMocks();
-});
+const ui = {
+  projectTitle: byRole('link', { name: 'Project' }),
+  portfolioTitle: byRole('link', { name: 'portfolio' }),
+  overviewPageLink: byRole('link', { name: 'overview.page' }),
+  issuesPageLink: byRole('link', { name: 'issues.page' }),
+  hotspotsPageLink: byRole('link', { name: 'layout.security_hotspots' }),
+  measuresPageLink: byRole('link', { name: 'layout.measures' }),
+  codePageLink: byRole('link', { name: 'code.page' }),
+  activityPageLink: byRole('link', { name: 'project_activity.page' }),
+  projectInfoLink: byRole('link', { name: 'project.info.title' }),
+  dashboardNotFound: byText('dashboard.project.not_found'),
+  goBackToHomePageLink: byRole('link', { name: 'go_back_to_homepage' }),
+};
 
 afterEach(() => {
-  jest.runOnlyPendingTimers();
-  jest.useRealTimers();
+  jest.clearAllMocks();
 });
 
-it('changes component', () => {
-  const wrapper = shallowRender();
-
-  wrapper.setState({
-    component: {
-      qualifier: ComponentQualifier.Project,
-      visibility: Visibility.Public,
-    } as Component,
-    loading: false,
-  });
-
-  wrapper.instance().handleComponentChange({ visibility: Visibility.Private });
+it('should render the component nav correctly for portfolio', async () => {
+  renderComponentContainerAsComponent();
+  expect(await ui.portfolioTitle.find()).toHaveAttribute('href', '/portfolio?id=portfolioKey');
+  expect(ui.issuesPageLink.get()).toHaveAttribute(
+    'href',
+    '/project/issues?id=portfolioKey&resolved=false'
+  );
+  expect(ui.measuresPageLink.get()).toHaveAttribute('href', '/component_measures?id=portfolioKey');
+  expect(ui.activityPageLink.get()).toHaveAttribute('href', '/project/activity?id=portfolioKey');
 
-  expect(wrapper.state().component).toEqual({
-    qualifier: ComponentQualifier.Project,
-    visibility: Visibility.Private,
+  await waitFor(() => {
+    expect(getTasksForComponent).toHaveBeenCalledWith('portfolioKey');
   });
 });
 
-it("doesn't load branches portfolio", async () => {
-  const wrapper = shallowRender({ location: mockLocation({ query: { id: 'portfolioKey' } }) });
-  await waitAndUpdate(wrapper);
-  expect(getBranches).not.toHaveBeenCalled();
-  expect(getPullRequests).not.toHaveBeenCalled();
-  expect(getComponentData).toHaveBeenCalledWith({ component: 'portfolioKey', branch: undefined });
-  expect(getComponentNavigation).toHaveBeenCalledWith({
-    component: 'portfolioKey',
-    branch: undefined,
+it('should render the component nav correctly for projects', async () => {
+  const component = mockComponent({
+    breadcrumbs: [{ key: 'project', name: 'Project', qualifier: ComponentQualifier.Project }],
+    key: 'project-key',
+    analysisDate: '2018-07-30',
   });
-});
 
-it('fetches status', async () => {
-  jest.mocked(getComponentData).mockResolvedValueOnce({
-    component: {},
-  } as unknown as Awaited<ReturnType<typeof getComponentData>>);
+  jest
+    .mocked(getComponentNavigation)
+    .mockResolvedValueOnce({} as unknown as Awaited<ReturnType<typeof getComponentNavigation>>);
 
-  const wrapper = shallowRender();
-  await waitAndUpdate(wrapper);
-  expect(getTasksForComponent).toHaveBeenCalledWith('portfolioKey');
+  jest
+    .mocked(getComponentData)
+    .mockResolvedValueOnce({ component } as unknown as Awaited<
+      ReturnType<typeof getComponentData>
+    >);
+
+  renderComponentContainerAsComponent();
+  expect(await ui.projectTitle.find()).toHaveAttribute('href', '/dashboard?id=project');
+  expect(ui.overviewPageLink.get()).toHaveAttribute('href', '/dashboard?id=project-key');
+  expect(ui.issuesPageLink.get()).toHaveAttribute(
+    'href',
+    '/project/issues?id=project-key&resolved=false'
+  );
+  expect(ui.hotspotsPageLink.get()).toHaveAttribute('href', '/security_hotspots?id=project-key');
+  expect(ui.measuresPageLink.get()).toHaveAttribute('href', '/component_measures?id=project-key');
+  expect(ui.codePageLink.get()).toHaveAttribute('href', '/code?id=project-key');
+  expect(ui.activityPageLink.get()).toHaveAttribute('href', '/project/activity?id=project-key');
+  expect(ui.projectInfoLink.get()).toHaveAttribute('href', '/project/information?id=project-key');
 });
 
-it('filters correctly the pending tasks for a main branch', () => {
-  const wrapper = shallowRender();
-  const component = wrapper.instance();
-  const mainBranch = mockMainBranch();
-  const branch3 = mockBranch({ name: 'branch-3' });
-  const pullRequest = mockPullRequest();
-
-  expect(component.isSameBranch({})).toBe(true);
-  wrapper.setProps({ location: mockLocation({ query: { branch: mainBranch.name } }) });
-  expect(component.isSameBranch({ branch: mainBranch.name })).toBe(true);
-  expect(component.isSameBranch({})).toBe(false);
-  wrapper.setProps({ location: mockLocation({ query: { branch: branch3.name } }) });
-  expect(component.isSameBranch({ branch: branch3.name })).toBe(true);
-  wrapper.setProps({ location: mockLocation({ query: { pullRequest: pullRequest.key } }) });
-  expect(component.isSameBranch({ pullRequest: pullRequest.key })).toBe(true);
-
-  const currentTask = mockTask({ pullRequest: pullRequest.key, status: TaskStatuses.InProgress });
-  const failedTask = { ...currentTask, status: TaskStatuses.Failed };
-  const pendingTasks = [currentTask, mockTask({ branch: branch3.name }), mockTask()];
-  expect(component.getCurrentTask(failedTask)).toBe(failedTask);
-  wrapper.setProps({ location: mockLocation({ query: {} }) });
-  expect(component.getCurrentTask(currentTask)).toBeUndefined();
-  wrapper.setProps({ location: mockLocation({ query: { pullRequest: pullRequest.key } }) });
-  expect(component.getCurrentTask(currentTask)).toMatchObject(currentTask);
-
-  expect(component.getPendingTasksForBranchLike(pendingTasks)).toMatchObject([currentTask]);
-  wrapper.setProps({ location: mockLocation({ query: {} }) });
-  expect(component.getPendingTasksForBranchLike(pendingTasks)).toMatchObject([{}]);
+it('should be able to change component', async () => {
+  const user = userEvent.setup();
+  renderComponentContainer();
+  expect(await screen.findByText('This is a test component')).toBeInTheDocument();
+  expect(screen.getByRole('button', { name: 'change component' })).toBeInTheDocument();
+  expect(screen.getByText('component name')).toBeInTheDocument();
+  await user.click(screen.getByRole('button', { name: 'change component' }));
+  expect(screen.getByText('new component name')).toBeInTheDocument();
 });
 
-it('reload component after task progress finished', async () => {
+it('should show component not found if it does not exist', async () => {
   jest
-    .mocked(getTasksForComponent)
-    .mockResolvedValueOnce({
-      queue: [{ id: 'foo', status: TaskStatuses.InProgress, type: TaskTypes.ViewRefresh }],
-    } as unknown as Awaited<ReturnType<typeof getTasksForComponent>>)
-    .mockResolvedValueOnce({
-      queue: [],
-    } as unknown as Awaited<ReturnType<typeof getTasksForComponent>>);
-
-  const wrapper = shallowRender();
-
-  // First round, there's something in the queue, and component navigation was
-  // not called again (it's called once at mount, hence the 1 times assertion
-  // here).
-  await waitAndUpdate(wrapper);
-  expect(getComponentNavigation).toHaveBeenCalledTimes(1);
-  expect(getTasksForComponent).toHaveBeenCalledTimes(1);
-
-  jest.runOnlyPendingTimers();
-
-  // Second round, the queue is now empty, hence we assume the previous task
-  // was done. We immediately load the component again.
-  expect(getTasksForComponent).toHaveBeenCalledTimes(2);
-
-  // Trigger the update.
-  await waitAndUpdate(wrapper);
-  // The component was correctly re-loaded.
-  expect(getComponentNavigation).toHaveBeenCalledTimes(2);
-  // The status API call will be called 1 final time after the component is
-  // fully loaded, so the total will be 3.
-  expect(getTasksForComponent).toHaveBeenCalledTimes(3);
-
-  // Make sure the timeout was cleared. It should not be called again.
-  jest.runAllTimers();
-  await waitAndUpdate(wrapper);
-  // The number of calls haven't changed.
-  expect(getComponentNavigation).toHaveBeenCalledTimes(2);
-  expect(getTasksForComponent).toHaveBeenCalledTimes(3);
-});
+    .mocked(getComponentNavigation)
+    .mockRejectedValueOnce(new Response(null, { status: HttpStatus.NotFound }));
 
-it('reloads component after task progress finished, and moves straight to current', async () => {
-  jest.mocked(getComponentData).mockResolvedValueOnce({
-    component: { key: 'bar' },
-  } as unknown as Awaited<ReturnType<typeof getComponentData>>);
+  renderComponentContainer();
 
-  jest
-    .mocked(getTasksForComponent)
-    .mockResolvedValueOnce({ queue: [] } as unknown as Awaited<
-      ReturnType<typeof getTasksForComponent>
-    >)
-    .mockResolvedValueOnce({
-      queue: [],
-      current: { id: 'foo', status: TaskStatuses.Success, type: TaskTypes.AppRefresh },
-    } as unknown as Awaited<ReturnType<typeof getTasksForComponent>>);
+  expect(await ui.dashboardNotFound.find()).toBeInTheDocument();
+  expect(ui.goBackToHomePageLink.get()).toBeInTheDocument();
+});
 
-  const wrapper = shallowRender();
+describe('getTasksForComponent', () => {
+  beforeEach(() => {
+    jest.useFakeTimers();
+  });
 
-  // First round, nothing in the queue, and component navigation was not called
-  // again (it's called once at mount, hence the 1 times assertion here).
-  await waitAndUpdate(wrapper);
-  expect(getComponentNavigation).toHaveBeenCalledTimes(1);
-  expect(getTasksForComponent).toHaveBeenCalledTimes(1);
+  afterEach(() => {
+    jest.useRealTimers();
+  });
 
-  jest.runOnlyPendingTimers();
+  it('reload component after task progress finished', async () => {
+    jest
+      .mocked(getTasksForComponent)
+      .mockResolvedValueOnce({
+        queue: [{ id: 'foo', status: TaskStatuses.InProgress, type: TaskTypes.ViewRefresh }],
+      } as unknown as Awaited<ReturnType<typeof getTasksForComponent>>)
+      .mockResolvedValueOnce({
+        queue: [],
+      } as unknown as Awaited<ReturnType<typeof getTasksForComponent>>);
+
+    renderComponentContainer();
+
+    // First round, there's something in the queue, and component navigation was
+    // not called again (it's called once at mount, hence the 1 times assertion
+    // here).
+    await waitFor(() => {
+      expect(getComponentNavigation).toHaveBeenCalledTimes(1);
+    });
+    expect(getComponentNavigation).toHaveBeenCalledTimes(1);
+    expect(getTasksForComponent).toHaveBeenCalledTimes(1);
 
-  // Second round, nothing in the queue, BUT a success task is current. This
-  // means the queue was processed too quick for us to see, and we didn't see
-  // any pending tasks in the queue. So we immediately load the component again.
-  expect(getTasksForComponent).toHaveBeenCalledTimes(2);
+    jest.runOnlyPendingTimers();
 
-  // Trigger the update.
-  await waitAndUpdate(wrapper);
-  // The component was correctly re-loaded.
-  expect(getComponentNavigation).toHaveBeenCalledTimes(2);
-  // The status API call will be called 1 final time after the component is
-  // fully loaded, so the total will be 3.
-  expect(getTasksForComponent).toHaveBeenCalledTimes(3);
-});
+    // Second round, the queue is now empty, hence we assume the previous task
+    // was done. We immediately load the component again.
+    expect(getTasksForComponent).toHaveBeenCalledTimes(2);
 
-it('only fully loads a non-empty component once', async () => {
-  jest.mocked(getComponentData).mockResolvedValueOnce({
-    component: { key: 'bar', analysisDate: '2019-01-01' },
-  } as unknown as Awaited<ReturnType<typeof getComponentData>>);
+    // Trigger the update.
+    // The component was correctly re-loaded.
+    await waitFor(() => {
+      expect(getComponentNavigation).toHaveBeenCalledTimes(2);
+    });
+    // The status API call will be called 1 final time after the component is
+    // fully loaded, so the total will be 3.
+    expect(getTasksForComponent).toHaveBeenCalledTimes(3);
+
+    // Make sure the timeout was cleared. It should not be called again.
+    jest.runAllTimers();
+    // The number of calls haven't changed.
+    await waitFor(() => {
+      expect(getComponentNavigation).toHaveBeenCalledTimes(2);
+    });
+    expect(getTasksForComponent).toHaveBeenCalledTimes(3);
+  });
 
-  jest.mocked(getTasksForComponent).mockResolvedValueOnce({
-    queue: [],
-    current: { id: 'foo', status: TaskStatuses.Success, type: TaskTypes.Report },
-  } as unknown as Awaited<ReturnType<typeof getTasksForComponent>>);
+  it('reloads component after task progress finished, and moves straight to current', async () => {
+    jest.mocked(getComponentData).mockResolvedValueOnce({
+      component: { key: 'bar' },
+    } as unknown as Awaited<ReturnType<typeof getComponentData>>);
 
-  const wrapper = shallowRender();
+    jest
+      .mocked(getTasksForComponent)
+      .mockResolvedValueOnce({ queue: [] } as unknown as Awaited<
+        ReturnType<typeof getTasksForComponent>
+      >)
+      .mockResolvedValueOnce({
+        queue: [],
+        current: { id: 'foo', status: TaskStatuses.Success, type: TaskTypes.AppRefresh },
+      } as unknown as Awaited<ReturnType<typeof getTasksForComponent>>);
+
+    renderComponentContainer();
+
+    // First round, nothing in the queue, and component navigation was not called
+    // again (it's called once at mount, hence the 1 times assertion here).
+    await waitFor(() => {
+      expect(getComponentNavigation).toHaveBeenCalledTimes(1);
+    });
+    expect(getTasksForComponent).toHaveBeenCalledTimes(1);
 
-  await waitAndUpdate(wrapper);
-  expect(getComponentNavigation).toHaveBeenCalledTimes(1);
-  expect(getTasksForComponent).toHaveBeenCalledTimes(1);
-});
+    jest.runOnlyPendingTimers();
 
-it('should redirect if in tutorials and ends the first analyses', async () => {
-  (getComponentData as jest.Mock<any>).mockResolvedValueOnce({
-    component: { key: 'bar', analysisDate: undefined },
-  });
-  (getTasksForComponent as jest.Mock<any>).mockResolvedValueOnce({
-    queue: [],
-    current: { id: 'foo', status: TaskStatuses.Success, type: TaskTypes.Report },
-  });
+    // Second round, nothing in the queue, BUT a success task is current. This
+    // means the queue was processed too quick for us to see, and we didn't see
+    // any pending tasks in the queue. So we immediately load the component again.
+    expect(getTasksForComponent).toHaveBeenCalledTimes(2);
 
-  const replace = jest.fn();
-  const wrapper = shallowRender({
-    location: mockLocation({ pathname: '/tutorials' }),
-    router: mockRouter({ replace }),
+    // Trigger the update.
+    // The component was correctly re-loaded.
+    await waitFor(() => {
+      expect(getComponentNavigation).toHaveBeenCalledTimes(2);
+    });
+    // The status API call will be called 1 final time after the component is
+    // fully loaded, so the total will be 3.
+    expect(getTasksForComponent).toHaveBeenCalledTimes(3);
   });
 
-  await waitAndUpdate(wrapper);
-  expect(replace).toHaveBeenCalledTimes(1);
-});
-
-it('only fully reloads a non-empty component if there was previously some task in progress', async () => {
-  jest.mocked(getComponentData).mockResolvedValueOnce({
-    component: { key: 'bar', analysisDate: '2019-01-01' },
-  } as unknown as Awaited<ReturnType<typeof getComponentData>>);
+  it('only fully loads a non-empty component once', async () => {
+    jest.mocked(getComponentData).mockResolvedValueOnce({
+      component: { key: 'bar', analysisDate: '2019-01-01' },
+    } as unknown as Awaited<ReturnType<typeof getComponentData>>);
 
-  jest
-    .mocked(getTasksForComponent)
-    .mockResolvedValueOnce({
-      queue: [{ id: 'foo', status: TaskStatuses.InProgress, type: TaskTypes.AppRefresh }],
-    } as unknown as Awaited<ReturnType<typeof getTasksForComponent>>)
-    .mockResolvedValueOnce({
+    jest.mocked(getTasksForComponent).mockResolvedValueOnce({
       queue: [],
-      current: { id: 'foo', status: TaskStatuses.Success, type: TaskTypes.AppRefresh },
+      current: { id: 'foo', status: TaskStatuses.Success, type: TaskTypes.Report },
     } as unknown as Awaited<ReturnType<typeof getTasksForComponent>>);
 
-  const wrapper = shallowRender();
-
-  // First round, a pending task in the queue. This should trigger a reload of the
-  // status endpoint.
-  await waitAndUpdate(wrapper);
-  jest.runOnlyPendingTimers();
-
-  // Second round, nothing in the queue, and a success task is current. This
-  // implies the current task was updated, and previously we displayed some information
-  // about a pending task. This new information must prompt the component to reload
-  // all data.
-  expect(getTasksForComponent).toHaveBeenCalledTimes(2);
-
-  // Trigger the update.
-  await waitAndUpdate(wrapper);
-  // The component was correctly re-loaded.
-  expect(getComponentNavigation).toHaveBeenCalledTimes(2);
-  // The status API call will be called 1 final time after the component is
-  // fully loaded, so the total will be 3.
-  expect(getTasksForComponent).toHaveBeenCalledTimes(3);
-});
+    renderComponentContainer();
+    await waitFor(() => {
+      expect(getComponentNavigation).toHaveBeenCalledTimes(1);
+    });
 
-it('should show component not found if it does not exist', async () => {
-  jest
-    .mocked(getComponentNavigation)
-    .mockRejectedValueOnce(new Response(null, { status: HttpStatus.NotFound }));
+    expect(getTasksForComponent).toHaveBeenCalledTimes(1);
+  });
+
+  it('only fully reloads a non-empty component if there was previously some task in progress', async () => {
+    jest.mocked(getComponentData).mockResolvedValueOnce({
+      component: { key: 'bar', analysisDate: '2019-01-01' },
+    } as unknown as Awaited<ReturnType<typeof getComponentData>>);
 
-  const wrapper = shallowRender();
-  await waitAndUpdate(wrapper);
-  expect(wrapper).toMatchSnapshot();
+    jest
+      .mocked(getTasksForComponent)
+      .mockResolvedValueOnce({
+        queue: [{ id: 'foo', status: TaskStatuses.InProgress, type: TaskTypes.AppRefresh }],
+      } as unknown as Awaited<ReturnType<typeof getTasksForComponent>>)
+      .mockResolvedValueOnce({
+        queue: [],
+        current: { id: 'foo', status: TaskStatuses.Success, type: TaskTypes.AppRefresh },
+      } as unknown as Awaited<ReturnType<typeof getTasksForComponent>>);
+
+    renderComponentContainer();
+
+    // First round, a pending task in the queue. This should trigger a reload of the
+    // status endpoint.
+    await waitFor(() => {
+      expect(getTasksForComponent).toHaveBeenCalledTimes(1);
+    });
+    jest.runOnlyPendingTimers();
+
+    // Second round, nothing in the queue, and a success task is current. This
+    // implies the current task was updated, and previously we displayed some information
+    // about a pending task. This new information must prompt the component to reload
+    // all data.
+    expect(getTasksForComponent).toHaveBeenCalledTimes(2);
+
+    // The component was correctly re-loaded.
+    await waitFor(() => {
+      expect(getComponentNavigation).toHaveBeenCalledTimes(2);
+    });
+    // The status API call will be called 1 final time after the component is
+    // fully loaded, so the total will be 3.
+    expect(getTasksForComponent).toHaveBeenCalledTimes(3);
+  });
 });
 
 it('should redirect if the user has no access', async () => {
@@ -334,27 +307,10 @@ it('should redirect if the user has no access', async () => {
     .mocked(getComponentNavigation)
     .mockRejectedValueOnce(new Response(null, { status: HttpStatus.Forbidden }));
 
-  const wrapper = shallowRender();
-  await waitAndUpdate(wrapper);
-  expect(handleRequiredAuthorization).toHaveBeenCalled();
-});
-
-it('should redirect if the component is a portfolio', async () => {
-  const componentKey = 'comp-key';
-
-  jest.mocked(getComponentData).mockResolvedValueOnce({
-    component: { key: componentKey, breadcrumbs: [{ qualifier: ComponentQualifier.Portfolio }] },
-  } as unknown as Awaited<ReturnType<typeof getComponentData>>);
-
-  const replace = jest.fn();
-
-  const wrapper = shallowRender({
-    location: mockLocation({ pathname: '/dashboard' }),
-    router: mockRouter({ replace }),
+  renderComponentContainer();
+  await waitFor(() => {
+    expect(handleRequiredAuthorization).toHaveBeenCalled();
   });
-
-  await waitAndUpdate(wrapper);
-  expect(replace).toHaveBeenCalledWith({ pathname: '/portfolio', search: `?id=${componentKey}` });
 });
 
 describe('should correctly validate the project binding depending on the context', () => {
@@ -382,11 +338,29 @@ describe('should correctly validate the project binding depending on the context
       jest.mocked(validateProjectAlmBinding).mockResolvedValueOnce(projectBindingErrors);
     }
 
-    const wrapper = shallowRender({ hasFeature: () => true });
-    await waitAndUpdate(wrapper);
-    expect(wrapper.state().projectBindingErrors).toBe(projectBindingErrors);
+    renderComponentContainer({ hasFeature: jest.fn().mockReturnValue(true) });
+    await waitFor(() => {
+      expect(validateProjectAlmBinding).toHaveBeenCalledTimes(n);
+    });
+  });
 
-    expect(validateProjectAlmBinding).toHaveBeenCalledTimes(n);
+  it('should show error message when check is not OK', async () => {
+    jest
+      .mocked(getComponentNavigation)
+      .mockResolvedValueOnce({} as unknown as Awaited<ReturnType<typeof getComponentNavigation>>);
+
+    jest
+      .mocked(getComponentData)
+      .mockResolvedValueOnce({ component: COMPONENT } as unknown as Awaited<
+        ReturnType<typeof getComponentData>
+      >);
+
+    jest.mocked(validateProjectAlmBinding).mockResolvedValueOnce(PROJECT_BINDING_ERRORS);
+
+    renderComponentContainerAsComponent({ hasFeature: jest.fn().mockReturnValue(true) });
+    expect(
+      await screen.findByText('component_navigation.pr_deco.error_detected_X', { exact: false })
+    ).toBeInTheDocument();
   });
 });
 
@@ -411,23 +385,66 @@ it.each([
         ReturnType<typeof getComponentData>
       >);
 
-    const wrapper = shallowRender();
-    await waitAndUpdate(wrapper);
-
-    expect(getProjectAlmBinding).not.toHaveBeenCalled();
-    expect(validateProjectAlmBinding).not.toHaveBeenCalled();
+    renderComponentContainer({ hasFeature: jest.fn().mockReturnValue(true) });
+    await waitFor(() => {
+      expect(validateProjectAlmBinding).not.toHaveBeenCalled();
+    });
   }
 );
 
-function shallowRender(props: Partial<ComponentContainer['props']> = {}) {
-  return shallow<ComponentContainer>(
-    <ComponentContainer
-      hasFeature={jest.fn().mockReturnValue(false)}
-      location={mockLocation({ query: { id: 'foo' } })}
-      router={mockRouter()}
-      {...props}
+function renderComponentContainerAsComponent(props: Partial<Props> = {}) {
+  return renderComponent(
+    <>
+      <div id="component-nav-portal" />{' '}
+      <ComponentContainer
+        hasFeature={jest.fn().mockReturnValue(false)}
+        location={mockLocation({ query: { id: 'foo' } })}
+        router={mockRouter()}
+        {...props}
+      />
+    </>
+  );
+}
+
+function renderComponentContainer(props: Partial<Props> = {}, pathName: string = '/') {
+  renderAppRoutes(pathName, () => (
+    <Route
+      element={
+        <ComponentContainer
+          hasFeature={jest.fn().mockReturnValue(false)}
+          location={mockLocation({ query: { id: 'foo' } })}
+          router={mockRouter()}
+          {...props}
+        />
+      }
     >
-      <Inner />
-    </ComponentContainer>
+      <Route path="*" element={<TestComponent />} />
+    </Route>
+  ));
+}
+
+function TestComponent() {
+  const { component, onComponentChange } = useContext(ComponentContext);
+
+  return (
+    <div>
+      This is a test component
+      <span>{component?.name}</span>
+      <button
+        onClick={() =>
+          onComponentChange(
+            mockComponent({
+              name: 'new component name',
+              breadcrumbs: [
+                { key: 'portfolioKey', name: 'portfolio', qualifier: ComponentQualifier.Portfolio },
+              ],
+            })
+          )
+        }
+        type="button"
+      >
+        change component
+      </button>
+    </div>
   );
 }
diff --git a/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/ComponentContainer-test.tsx.snap b/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/ComponentContainer-test.tsx.snap
deleted file mode 100644 (file)
index a2eba82..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should show component not found if it does not exist 1`] = `
-<ComponentContainerNotFound
-  isPortfolioLike={false}
-/>
-`;