您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

ComponentContainer-test.tsx 23KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2024 SonarSource SA
  4. * mailto:info AT sonarsource DOT com
  5. *
  6. * This program is free software; you can redistribute it and/or
  7. * modify it under the terms of the GNU Lesser General Public
  8. * License as published by the Free Software Foundation; either
  9. * version 3 of the License, or (at your option) any later version.
  10. *
  11. * This program is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  14. * Lesser General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU Lesser General Public License
  17. * along with this program; if not, write to the Free Software Foundation,
  18. * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  19. */
  20. import { screen, waitFor } from '@testing-library/react';
  21. import userEvent from '@testing-library/user-event';
  22. import React, { useContext } from 'react';
  23. import { Route } from 'react-router-dom';
  24. import { validateProjectAlmBinding } from '../../../api/alm-settings';
  25. import { getTasksForComponent } from '../../../api/ce';
  26. import { getComponentData } from '../../../api/components';
  27. import { getComponentNavigation } from '../../../api/navigation';
  28. import * as withRouter from '../../../components/hoc/withRouter';
  29. import { mockProjectAlmBindingConfigurationErrors } from '../../../helpers/mocks/alm-settings';
  30. import { mockBranch, mockPullRequest } from '../../../helpers/mocks/branch-like';
  31. import { mockComponent } from '../../../helpers/mocks/component';
  32. import { mockTask } from '../../../helpers/mocks/tasks';
  33. import { HttpStatus } from '../../../helpers/request';
  34. import { renderAppRoutes, renderComponent } from '../../../helpers/testReactTestingUtils';
  35. import { byRole, byText } from '../../../helpers/testSelector';
  36. import { getProjectUrl, getPullRequestUrl } from '../../../helpers/urls';
  37. import { ComponentQualifier, Visibility } from '../../../types/component';
  38. import { TaskStatuses, TaskTypes } from '../../../types/tasks';
  39. import handleRequiredAuthorization from '../../utils/handleRequiredAuthorization';
  40. import ComponentContainer, { isSameBranch } from '../ComponentContainer';
  41. import { WithAvailableFeaturesProps } from '../available-features/withAvailableFeatures';
  42. import { ComponentContext } from '../componentContext/ComponentContext';
  43. jest.mock('../../../api/ce', () => ({
  44. getTasksForComponent: jest.fn().mockResolvedValue({ queue: [] }),
  45. }));
  46. jest.mock('../../../api/components', () => ({
  47. getComponentData: jest
  48. .fn()
  49. .mockResolvedValue({ component: { name: 'component name', analysisDate: '2018-07-30' } }),
  50. }));
  51. jest.mock('../../../api/navigation', () => ({
  52. getComponentNavigation: jest.fn().mockResolvedValue({
  53. breadcrumbs: [{ key: 'portfolioKey', name: 'portfolio', qualifier: 'VW' }],
  54. key: 'portfolioKey',
  55. }),
  56. }));
  57. jest.mock('../../../api/branches', () => ({
  58. getBranches: jest.fn().mockResolvedValue([mockBranch()]),
  59. getPullRequests: jest.fn().mockResolvedValue([mockPullRequest({ target: 'dropped-branch' })]),
  60. }));
  61. jest.mock('../../../api/alm-settings', () => ({
  62. validateProjectAlmBinding: jest.fn().mockResolvedValue(undefined),
  63. }));
  64. jest.mock('../../utils/handleRequiredAuthorization', () => ({
  65. __esModule: true,
  66. default: jest.fn(),
  67. }));
  68. jest.mock('../../../components/hoc/withRouter', () => ({
  69. __esModule: true,
  70. ...jest.requireActual('../../../components/hoc/withRouter'),
  71. }));
  72. const ui = {
  73. projectTitle: byRole('link', { name: 'Project' }),
  74. projectText: byText('project'),
  75. portfolioTitle: byRole('link', { name: 'portfolio' }),
  76. portfolioText: byText('portfolio'),
  77. overviewPageLink: byRole('link', { name: 'overview.page' }),
  78. issuesPageLink: byRole('link', { name: 'issues.page' }),
  79. hotspotsPageLink: byRole('link', { name: 'layout.security_hotspots' }),
  80. measuresPageLink: byRole('link', { name: 'layout.measures' }),
  81. codePageLink: byRole('link', { name: 'code.page' }),
  82. activityPageLink: byRole('link', { name: 'project_activity.page' }),
  83. projectInfoLink: byRole('link', { name: 'project.info.title' }),
  84. dashboardNotFound: byText('dashboard.project.not_found'),
  85. goBackToHomePageLink: byRole('link', { name: 'go_back_to_homepage' }),
  86. };
  87. afterEach(() => {
  88. jest.clearAllMocks();
  89. });
  90. it('should render the component nav correctly for portfolio', async () => {
  91. renderComponentContainerAsComponent();
  92. expect(await ui.portfolioTitle.find()).toHaveAttribute('href', '/portfolio?id=portfolioKey');
  93. expect(ui.issuesPageLink.get()).toHaveAttribute(
  94. 'href',
  95. '/project/issues?id=portfolioKey&issueStatuses=OPEN%2CCONFIRMED',
  96. );
  97. expect(ui.measuresPageLink.get()).toHaveAttribute('href', '/component_measures?id=portfolioKey');
  98. expect(ui.activityPageLink.get()).toHaveAttribute('href', '/project/activity?id=portfolioKey');
  99. await waitFor(() => {
  100. expect(getTasksForComponent).toHaveBeenCalledWith('portfolioKey');
  101. });
  102. });
  103. it('should render the component nav correctly for projects', async () => {
  104. const component = mockComponent({
  105. breadcrumbs: [{ key: 'project', name: 'Project', qualifier: ComponentQualifier.Project }],
  106. key: 'project-key',
  107. analysisDate: '2018-07-30',
  108. });
  109. jest
  110. .mocked(getComponentNavigation)
  111. .mockResolvedValueOnce({} as unknown as Awaited<ReturnType<typeof getComponentNavigation>>);
  112. jest
  113. .mocked(getComponentData)
  114. .mockResolvedValueOnce({ component } as unknown as Awaited<
  115. ReturnType<typeof getComponentData>
  116. >);
  117. renderComponentContainerAsComponent();
  118. expect(await ui.projectTitle.find()).toHaveAttribute('href', '/dashboard?id=project');
  119. expect(ui.overviewPageLink.get()).toHaveAttribute('href', '/dashboard?id=project-key');
  120. expect(ui.issuesPageLink.get()).toHaveAttribute(
  121. 'href',
  122. '/project/issues?id=project-key&issueStatuses=OPEN%2CCONFIRMED',
  123. );
  124. expect(ui.hotspotsPageLink.get()).toHaveAttribute('href', '/security_hotspots?id=project-key');
  125. expect(ui.measuresPageLink.get()).toHaveAttribute('href', '/component_measures?id=project-key');
  126. expect(ui.codePageLink.get()).toHaveAttribute('href', '/code?id=project-key');
  127. expect(ui.activityPageLink.get()).toHaveAttribute('href', '/project/activity?id=project-key');
  128. expect(ui.projectInfoLink.get()).toHaveAttribute('href', '/project/information?id=project-key');
  129. });
  130. it('should be able to change component', async () => {
  131. const user = userEvent.setup();
  132. renderComponentContainer();
  133. expect(await screen.findByText('This is a test component')).toBeInTheDocument();
  134. expect(screen.getByRole('button', { name: 'change component' })).toBeInTheDocument();
  135. expect(screen.getByText('component name')).toBeInTheDocument();
  136. await user.click(screen.getByRole('button', { name: 'change component' }));
  137. expect(screen.getByText('new component name')).toBeInTheDocument();
  138. });
  139. it('should show component not found if it does not exist', async () => {
  140. jest
  141. .mocked(getComponentNavigation)
  142. .mockRejectedValueOnce(new Response(null, { status: HttpStatus.NotFound }));
  143. renderComponentContainer();
  144. expect(await ui.dashboardNotFound.find()).toBeInTheDocument();
  145. expect(ui.goBackToHomePageLink.get()).toBeInTheDocument();
  146. });
  147. it('should show component not found if target branch is not found for fixing pull request', async () => {
  148. renderComponentContainer(
  149. { hasFeature: jest.fn().mockReturnValue(true) },
  150. '?id=foo&fixedInPullRequest=1001',
  151. );
  152. expect(await ui.dashboardNotFound.find()).toBeInTheDocument();
  153. });
  154. describe('getTasksForComponent', () => {
  155. it('reload component after task progress finished', async () => {
  156. jest
  157. .mocked(getTasksForComponent)
  158. .mockResolvedValueOnce({
  159. queue: [{ id: 'foo', status: TaskStatuses.InProgress, type: TaskTypes.ViewRefresh }],
  160. } as unknown as Awaited<ReturnType<typeof getTasksForComponent>>)
  161. .mockResolvedValueOnce({
  162. queue: [],
  163. } as unknown as Awaited<ReturnType<typeof getTasksForComponent>>);
  164. renderComponentContainer();
  165. jest.useFakeTimers();
  166. // First round, there's something in the queue, and component navigation was
  167. // not called again (it's called once at mount, hence the 1 times assertion
  168. // here).
  169. await waitFor(() => {
  170. expect(getComponentNavigation).toHaveBeenCalledTimes(1);
  171. });
  172. await waitFor(() => expect(getTasksForComponent).toHaveBeenCalledTimes(1));
  173. jest.runOnlyPendingTimers();
  174. jest.useRealTimers();
  175. // Second round, the queue is now empty, hence we assume the previous task
  176. // was done. We immediately load the component again.
  177. await waitFor(() => expect(getTasksForComponent).toHaveBeenCalledTimes(2));
  178. // Trigger the update.
  179. // The component was correctly re-loaded.
  180. await waitFor(() => {
  181. expect(getComponentNavigation).toHaveBeenCalledTimes(2);
  182. });
  183. // The status API call will be called 1 final time after the component is
  184. // fully loaded, so the total will be 3.
  185. expect(getTasksForComponent).toHaveBeenCalledTimes(3);
  186. // Make sure the timeout was cleared. It should not be called again.
  187. jest.useFakeTimers();
  188. jest.runAllTimers();
  189. // The number of calls haven't changed.
  190. await waitFor(() => {
  191. expect(getComponentNavigation).toHaveBeenCalledTimes(2);
  192. });
  193. expect(getTasksForComponent).toHaveBeenCalledTimes(3);
  194. jest.useRealTimers();
  195. });
  196. it('reloads component after task progress finished, and moves straight to current', async () => {
  197. jest.mocked(getComponentData).mockResolvedValueOnce({
  198. component: { key: 'bar' },
  199. } as unknown as Awaited<ReturnType<typeof getComponentData>>);
  200. jest
  201. .mocked(getTasksForComponent)
  202. .mockResolvedValueOnce({ queue: [] } as unknown as Awaited<
  203. ReturnType<typeof getTasksForComponent>
  204. >)
  205. .mockResolvedValueOnce({
  206. queue: [],
  207. current: { id: 'foo', status: TaskStatuses.Success, type: TaskTypes.AppRefresh },
  208. } as unknown as Awaited<ReturnType<typeof getTasksForComponent>>);
  209. renderComponentContainer();
  210. jest.useFakeTimers();
  211. // First round, nothing in the queue, and component navigation was not called
  212. // again (it's called once at mount, hence the 1 times assertion here).
  213. await waitFor(() => {
  214. expect(getComponentNavigation).toHaveBeenCalledTimes(1);
  215. });
  216. await waitFor(() => expect(getTasksForComponent).toHaveBeenCalledTimes(1));
  217. jest.runOnlyPendingTimers();
  218. jest.useRealTimers();
  219. // Second round, nothing in the queue, BUT a success task is current. This
  220. // means the queue was processed too quick for us to see, and we didn't see
  221. // any pending tasks in the queue. So we immediately load the component again.
  222. await waitFor(() => expect(getTasksForComponent).toHaveBeenCalledTimes(2));
  223. // Trigger the update.
  224. // The component was correctly re-loaded.
  225. await waitFor(() => {
  226. expect(getComponentNavigation).toHaveBeenCalledTimes(2);
  227. });
  228. // The status API call will be called 1 final time after the component is
  229. // fully loaded, so the total will be 3.
  230. expect(getTasksForComponent).toHaveBeenCalledTimes(3);
  231. });
  232. it('only fully loads a non-empty component once', async () => {
  233. jest.mocked(getComponentData).mockResolvedValueOnce({
  234. component: { key: 'bar', analysisDate: '2019-01-01' },
  235. } as unknown as Awaited<ReturnType<typeof getComponentData>>);
  236. jest.mocked(getTasksForComponent).mockResolvedValueOnce({
  237. queue: [],
  238. current: { id: 'foo', status: TaskStatuses.Success, type: TaskTypes.Report },
  239. } as unknown as Awaited<ReturnType<typeof getTasksForComponent>>);
  240. renderComponentContainer();
  241. await waitFor(() => {
  242. expect(getComponentNavigation).toHaveBeenCalledTimes(1);
  243. });
  244. await waitFor(() => expect(getTasksForComponent).toHaveBeenCalledTimes(1));
  245. });
  246. it('only fully reloads a non-empty component if there was previously some task in progress', async () => {
  247. jest.mocked(getComponentData).mockResolvedValueOnce({
  248. component: { key: 'bar', analysisDate: '2019-01-01' },
  249. } as unknown as Awaited<ReturnType<typeof getComponentData>>);
  250. jest
  251. .mocked(getTasksForComponent)
  252. .mockResolvedValueOnce({
  253. queue: [{ id: 'foo', status: TaskStatuses.InProgress, type: TaskTypes.AppRefresh }],
  254. } as unknown as Awaited<ReturnType<typeof getTasksForComponent>>)
  255. .mockResolvedValueOnce({
  256. queue: [],
  257. current: { id: 'foo', status: TaskStatuses.Success, type: TaskTypes.AppRefresh },
  258. } as unknown as Awaited<ReturnType<typeof getTasksForComponent>>);
  259. renderComponentContainer();
  260. jest.useFakeTimers();
  261. // First round, a pending task in the queue. This should trigger a reload of the
  262. // status endpoint.
  263. await waitFor(() => expect(getTasksForComponent).toHaveBeenCalledTimes(1));
  264. jest.runOnlyPendingTimers();
  265. jest.useRealTimers();
  266. // Second round, nothing in the queue, and a success task is current. This
  267. // implies the current task was updated, and previously we displayed some information
  268. // about a pending task. This new information must prompt the component to reload
  269. // all data.
  270. await waitFor(() => expect(getTasksForComponent).toHaveBeenCalledTimes(2));
  271. // The component was correctly re-loaded.
  272. await waitFor(() => {
  273. expect(getComponentNavigation).toHaveBeenCalledTimes(2);
  274. });
  275. // The status API call will be called 1 final time after the component is
  276. // fully loaded, so the total will be 3.
  277. expect(getTasksForComponent).toHaveBeenCalledTimes(3);
  278. });
  279. });
  280. describe('should correctly validate the project binding depending on the context', () => {
  281. const COMPONENT = mockComponent({
  282. breadcrumbs: [{ key: 'foo', name: 'Foo', qualifier: ComponentQualifier.Project }],
  283. });
  284. const PROJECT_BINDING_ERRORS = mockProjectAlmBindingConfigurationErrors();
  285. it.each([
  286. ["has an analysis; won't perform any check", { ...COMPONENT, analysisDate: '2020-01' }],
  287. ['has a project binding; check is OK', COMPONENT, undefined, 1],
  288. ['has a project binding; check is not OK', COMPONENT, PROJECT_BINDING_ERRORS, 1],
  289. ])('%s', async (_, component, projectBindingErrors = undefined, n = 0) => {
  290. jest
  291. .mocked(getComponentNavigation)
  292. .mockResolvedValueOnce({} as unknown as Awaited<ReturnType<typeof getComponentNavigation>>);
  293. jest
  294. .mocked(getComponentData)
  295. .mockResolvedValueOnce({ component } as unknown as Awaited<
  296. ReturnType<typeof getComponentData>
  297. >);
  298. if (n > 0) {
  299. jest.mocked(validateProjectAlmBinding).mockResolvedValueOnce(projectBindingErrors);
  300. }
  301. renderComponentContainer({ hasFeature: jest.fn().mockReturnValue(true) });
  302. await waitFor(() => {
  303. expect(validateProjectAlmBinding).toHaveBeenCalledTimes(n);
  304. });
  305. });
  306. it('should show error message when check is not OK', async () => {
  307. jest
  308. .mocked(getComponentNavigation)
  309. .mockResolvedValueOnce({} as unknown as Awaited<ReturnType<typeof getComponentNavigation>>);
  310. jest
  311. .mocked(getComponentData)
  312. .mockResolvedValueOnce({ component: COMPONENT } as unknown as Awaited<
  313. ReturnType<typeof getComponentData>
  314. >);
  315. jest.mocked(validateProjectAlmBinding).mockResolvedValueOnce(PROJECT_BINDING_ERRORS);
  316. renderComponentContainerAsComponent({ hasFeature: jest.fn().mockReturnValue(true) });
  317. expect(
  318. await screen.findByText('component_navigation.pr_deco.error_detected_X', { exact: false }),
  319. ).toBeInTheDocument();
  320. });
  321. });
  322. describe('redirects', () => {
  323. it('should redirect if the user has no access', async () => {
  324. jest
  325. .mocked(getComponentNavigation)
  326. .mockRejectedValueOnce(new Response(null, { status: HttpStatus.Forbidden }));
  327. renderComponentContainer();
  328. await waitFor(() => {
  329. expect(handleRequiredAuthorization).toHaveBeenCalled();
  330. });
  331. });
  332. it('should redirect to portfolio when using dashboard path', async () => {
  333. renderComponentContainer(
  334. { hasFeature: jest.fn().mockReturnValue(true) },
  335. 'dashboard?id=foo',
  336. '/dashboard',
  337. );
  338. expect(await ui.portfolioText.find()).toBeInTheDocument();
  339. });
  340. });
  341. it.each([
  342. [ComponentQualifier.Application],
  343. [ComponentQualifier.Portfolio],
  344. [ComponentQualifier.SubPortfolio],
  345. ])(
  346. 'should not care about PR decoration settings for %s',
  347. async (componentQualifier: ComponentQualifier) => {
  348. const component = mockComponent({
  349. breadcrumbs: [{ key: 'foo', name: 'Foo', qualifier: componentQualifier }],
  350. });
  351. jest
  352. .mocked(getComponentNavigation)
  353. .mockResolvedValueOnce({} as unknown as Awaited<ReturnType<typeof getComponentNavigation>>);
  354. jest
  355. .mocked(getComponentData)
  356. .mockResolvedValueOnce({ component } as unknown as Awaited<
  357. ReturnType<typeof getComponentData>
  358. >);
  359. renderComponentContainer({ hasFeature: jest.fn().mockReturnValue(true) });
  360. await waitFor(() => {
  361. expect(validateProjectAlmBinding).not.toHaveBeenCalled();
  362. });
  363. },
  364. );
  365. it('isSameBranch util returns expected result', () => {
  366. expect(isSameBranch(mockTask())).toBe(true);
  367. expect(isSameBranch(mockTask({ branch: 'branch' }), 'branch')).toBe(true);
  368. expect(isSameBranch(mockTask({ pullRequest: 'pr' }), undefined, 'pr')).toBe(true);
  369. });
  370. describe('tutorials', () => {
  371. it('should redirect to project main branch dashboard from tutorials when receiving new related scan report', async () => {
  372. const componentKey = 'foo-component';
  373. jest.mocked(getComponentData).mockResolvedValue({
  374. ancestors: [],
  375. component: {
  376. key: componentKey,
  377. name: 'component name',
  378. qualifier: ComponentQualifier.Project,
  379. visibility: Visibility.Public,
  380. },
  381. });
  382. jest
  383. .mocked(getTasksForComponent)
  384. .mockResolvedValueOnce({ queue: [] })
  385. .mockResolvedValue({
  386. queue: [{ status: TaskStatuses.InProgress, type: TaskTypes.Report }],
  387. } as unknown as Awaited<ReturnType<typeof getTasksForComponent>>);
  388. const mockedReplace = jest.fn();
  389. jest.spyOn(withRouter, 'useRouter').mockReturnValue({
  390. replace: mockedReplace,
  391. push: jest.fn(),
  392. });
  393. renderComponentContainer(
  394. { hasFeature: jest.fn().mockReturnValue(true) },
  395. `tutorials?id=${componentKey}`,
  396. '/',
  397. );
  398. jest.useFakeTimers();
  399. await waitFor(() => expect(getTasksForComponent).toHaveBeenCalledTimes(1));
  400. expect(mockedReplace).not.toHaveBeenCalled();
  401. jest.runOnlyPendingTimers();
  402. jest.useRealTimers();
  403. await waitFor(() => expect(getTasksForComponent).toHaveBeenCalledTimes(2));
  404. expect(mockedReplace).toHaveBeenCalledWith(getProjectUrl(componentKey));
  405. });
  406. it('should redirect to project branch dashboard from tutorials when receiving new related scan report', async () => {
  407. const componentKey = 'foo-component';
  408. const branchName = 'fooBranch';
  409. jest.mocked(getComponentData).mockResolvedValue({
  410. ancestors: [],
  411. component: {
  412. key: componentKey,
  413. name: 'component name',
  414. qualifier: ComponentQualifier.Project,
  415. visibility: Visibility.Public,
  416. },
  417. });
  418. jest
  419. .mocked(getTasksForComponent)
  420. .mockResolvedValueOnce({ queue: [] })
  421. .mockResolvedValue({
  422. queue: [{ branch: branchName, status: TaskStatuses.InProgress, type: TaskTypes.Report }],
  423. } as unknown as Awaited<ReturnType<typeof getTasksForComponent>>);
  424. const mockedReplace = jest.fn();
  425. jest.spyOn(withRouter, 'useRouter').mockReturnValue({
  426. replace: mockedReplace,
  427. push: jest.fn(),
  428. });
  429. renderComponentContainer(
  430. { hasFeature: jest.fn().mockReturnValue(true) },
  431. `tutorials?id=${componentKey}`,
  432. '/',
  433. );
  434. jest.useFakeTimers();
  435. await waitFor(() => expect(getTasksForComponent).toHaveBeenCalledTimes(1));
  436. expect(mockedReplace).not.toHaveBeenCalled();
  437. jest.runOnlyPendingTimers();
  438. jest.useRealTimers();
  439. await waitFor(() => expect(getTasksForComponent).toHaveBeenCalledTimes(2));
  440. expect(mockedReplace).toHaveBeenCalledWith(getProjectUrl(componentKey, branchName));
  441. });
  442. it('should redirect to project pull request dashboard from tutorials when receiving new related scan report', async () => {
  443. const componentKey = 'foo-component';
  444. const pullRequestKey = 'fooPR';
  445. jest.mocked(getComponentData).mockResolvedValue({
  446. ancestors: [],
  447. component: {
  448. key: componentKey,
  449. name: 'component name',
  450. qualifier: ComponentQualifier.Project,
  451. visibility: Visibility.Public,
  452. },
  453. });
  454. jest
  455. .mocked(getTasksForComponent)
  456. .mockResolvedValueOnce({ queue: [] })
  457. .mockResolvedValue({
  458. queue: [
  459. { pullRequest: pullRequestKey, status: TaskStatuses.InProgress, type: TaskTypes.Report },
  460. ],
  461. } as unknown as Awaited<ReturnType<typeof getTasksForComponent>>);
  462. const mockedReplace = jest.fn();
  463. jest.spyOn(withRouter, 'useRouter').mockReturnValue({
  464. replace: mockedReplace,
  465. push: jest.fn(),
  466. });
  467. renderComponentContainer(
  468. { hasFeature: jest.fn().mockReturnValue(true) },
  469. `tutorials?id=${componentKey}`,
  470. '/',
  471. );
  472. jest.useFakeTimers();
  473. await waitFor(() => expect(getTasksForComponent).toHaveBeenCalledTimes(1));
  474. expect(mockedReplace).not.toHaveBeenCalled();
  475. jest.runOnlyPendingTimers();
  476. jest.useRealTimers();
  477. await waitFor(() => expect(getTasksForComponent).toHaveBeenCalledTimes(2));
  478. expect(mockedReplace).toHaveBeenCalledWith(getPullRequestUrl(componentKey, pullRequestKey));
  479. });
  480. });
  481. function renderComponentContainerAsComponent(props: Partial<WithAvailableFeaturesProps> = {}) {
  482. return renderComponent(
  483. <>
  484. <div id="component-nav-portal" />
  485. <ComponentContainer {...props} />
  486. </>,
  487. '/?id=foo',
  488. );
  489. }
  490. function renderComponentContainer(
  491. props: Partial<WithAvailableFeaturesProps> = {},
  492. navigateTo = '?id=foo',
  493. path = '/',
  494. ) {
  495. renderAppRoutes(
  496. path,
  497. () => (
  498. <Route element={<ComponentContainer {...props} />}>
  499. <Route path="*" element={<TestComponent />} />
  500. <Route path="portfolio" element={<div>portfolio</div>} />
  501. <Route path="dashboard" element={<div>project</div>} />
  502. </Route>
  503. ),
  504. {
  505. navigateTo,
  506. },
  507. );
  508. }
  509. function TestComponent() {
  510. const { component, onComponentChange } = useContext(ComponentContext);
  511. return (
  512. <div>
  513. This is a test component
  514. <span>{component?.name}</span>
  515. <button
  516. onClick={() =>
  517. onComponentChange(
  518. mockComponent({
  519. name: 'new component name',
  520. breadcrumbs: [
  521. { key: 'portfolioKey', name: 'portfolio', qualifier: ComponentQualifier.Portfolio },
  522. ],
  523. }),
  524. )
  525. }
  526. type="button"
  527. >
  528. change component
  529. </button>
  530. </div>
  531. );
  532. }