]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-18552 Migrate issues app components to RTL
author7PH <benjamin.raymond@sonarsource.com>
Fri, 24 Feb 2023 10:38:09 +0000 (11:38 +0100)
committersonartech <sonartech@sonarsource.com>
Thu, 2 Mar 2023 20:03:50 +0000 (20:03 +0000)
25 files changed:
server/sonar-web/src/main/js/api/mocks/IssuesServiceMock.ts
server/sonar-web/src/main/js/apps/issues/__tests__/IssuesApp-it.tsx
server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.tsx
server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx
server/sonar-web/src/main/js/apps/issues/components/__tests__/AssigneeSelect-test.tsx
server/sonar-web/src/main/js/apps/issues/components/__tests__/BulkChangeModal-it.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/issues/components/__tests__/BulkChangeModal-test.tsx [deleted file]
server/sonar-web/src/main/js/apps/issues/components/__tests__/ComponentBreadcrumbs-test.tsx
server/sonar-web/src/main/js/apps/issues/components/__tests__/IssuesApp-test.tsx [deleted file]
server/sonar-web/src/main/js/apps/issues/components/__tests__/IssuesContainer-test.tsx [deleted file]
server/sonar-web/src/main/js/apps/issues/components/__tests__/IssuesList-test.tsx [deleted file]
server/sonar-web/src/main/js/apps/issues/components/__tests__/IssuesSourceViewer-test.tsx [deleted file]
server/sonar-web/src/main/js/apps/issues/components/__tests__/ListItem-test.tsx [deleted file]
server/sonar-web/src/main/js/apps/issues/components/__tests__/PageActions-test.tsx [deleted file]
server/sonar-web/src/main/js/apps/issues/components/__tests__/TotalEffort-test.tsx [deleted file]
server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/AssigneeSelect-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/BulkChangeModal-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/ComponentBreadcrumbs-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/IssuesContainer-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/IssuesList-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/IssuesSourceViewer-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/ListItem-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/PageActions-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/TotalEffort-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/types/issues.ts

index 5f1cfdeff520c0db64fb4770d078cc1acc73a784..ec195133c9a967812224ec9b1b0eee4e29896584 100644 (file)
@@ -30,6 +30,7 @@ import {
   mockRuleDetails,
 } from '../../helpers/testMocks';
 import {
+  ASSIGNEE_ME,
   IssueType,
   RawFacet,
   RawIssue,
@@ -103,6 +104,7 @@ export default class IssuesServiceMock {
           key: 'issue101',
           component: 'foo:test1.js',
           message: 'Issue with no location message',
+          type: IssueType.Vulnerability,
           rule: 'simpleRuleId',
           textRange: {
             startLine: 10,
@@ -160,6 +162,7 @@ export default class IssuesServiceMock {
           key: 'issue11',
           component: 'foo:test1.js',
           message: 'FlowIssue',
+          type: IssueType.CodeSmell,
           rule: 'simpleRuleId',
           textRange: {
             startLine: 10,
@@ -252,6 +255,8 @@ export default class IssuesServiceMock {
           key: 'issue0',
           component: 'foo:test1.js',
           message: 'Issue on file',
+          assignee: mockLoggedInUser().login,
+          type: IssueType.Vulnerability,
           rule: 'simpleRuleId',
           textRange: undefined,
           line: undefined,
@@ -263,6 +268,7 @@ export default class IssuesServiceMock {
           key: 'issue1',
           component: 'foo:test1.js',
           message: 'Fix this',
+          type: IssueType.Vulnerability,
           rule: 'simpleRuleId',
           textRange: {
             startLine: 10,
@@ -397,6 +403,17 @@ export default class IssuesServiceMock {
           'component.key'
         ),
       },
+      {
+        issue: mockRawIssue(false, {
+          key: 'issue1101',
+          component: 'foo:test5.js',
+          message: 'Issue on page 2',
+          rule: 'simpleRuleId',
+          textRange: undefined,
+          line: undefined,
+        }),
+        snippets: {},
+      },
     ];
 
     this.list = cloneDeep(this.defaultList);
@@ -524,18 +541,60 @@ export default class IssuesServiceMock {
       if (name === 'owaspTop10-2021') {
         return this.owasp2021FacetList();
       }
+      if (name === 'languages') {
+        return {
+          property: name,
+          values: [
+            {
+              val: 'java',
+              count: 25211,
+            },
+            {
+              val: 'ts',
+              count: 3174,
+            },
+          ],
+        };
+      }
       return {
         property: name,
         values: [],
       };
     });
+
+    // Filter list (only supports assignee, type and severity)
+    const filteredList = this.list
+      .filter((item) => {
+        if (!query.assignees) {
+          return true;
+        }
+        if (query.assignees === ASSIGNEE_ME) {
+          return item.issue.assignee === mockLoggedInUser().login;
+        }
+        return query.assignees.split(',').includes(item.issue.assignee);
+      })
+      .filter((item) => !query.types || query.types.split(',').includes(item.issue.type))
+      .filter(
+        (item) => !query.severities || query.severities.split(',').includes(item.issue.severity)
+      );
+
+    // Splice list items according to paging using a fixed page size
+    const pageIndex = query.p || 1;
+    const pageSize = 7;
+    const listItems = filteredList.slice((pageIndex - 1) * pageSize, pageIndex * pageSize);
+
+    // Generate response
     return this.reply({
-      components: generateReferenceComponentsForIssues(this.list),
+      components: generateReferenceComponentsForIssues(filteredList),
       effortTotal: 199629,
       facets,
-      issues: this.list.map((line) => line.issue),
+      issues: listItems.map((line) => line.issue),
       languages: [],
-      paging: mockPaging(),
+      paging: mockPaging({
+        pageIndex,
+        pageSize,
+        total: filteredList.length,
+      }),
     });
   };
 
index d21fe46459b6d52f48f6abcdb76d24a3dff268a7..8507717caa227c9ee5b3482367fd172a8b90f0cb 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-import { act, screen, within } from '@testing-library/react';
+import { act, screen, waitFor, within } from '@testing-library/react';
 import userEvent from '@testing-library/user-event';
 import React from 'react';
 import selectEvent from 'react-select-event';
+import { byLabelText, byRole } from 'testing-library-selector';
 import ComponentsServiceMock from '../../../api/mocks/ComponentsServiceMock';
 import IssuesServiceMock from '../../../api/mocks/IssuesServiceMock';
 import { TabKeys } from '../../../components/rules/RuleTabViewer';
 import { mockComponent } from '../../../helpers/mocks/component';
 import { renderOwaspTop102021Category } from '../../../helpers/security-standard';
-import { mockCurrentUser } from '../../../helpers/testMocks';
+import { mockCurrentUser, mockLoggedInUser } from '../../../helpers/testMocks';
 import { renderApp, renderAppWithComponentContext } from '../../../helpers/testReactTestingUtils';
 import { ComponentQualifier } from '../../../types/component';
 import { IssueType } from '../../../types/issues';
@@ -50,583 +51,699 @@ beforeEach(() => {
   window.HTMLElement.prototype.scrollIntoView = jest.fn();
 });
 
-it('should navigate to Why is this an issue tab', async () => {
-  renderProjectIssuesApp('project/issues?issues=issue2&open=issue2&id=myproject&why=1');
-  expect(
-    await screen.findByRole('tab', {
-      name: `coding_rules.description_section.title.root_cause`,
-      selected: true,
-    })
-  ).toBeInTheDocument();
-});
-
-//Improve this to include all the bulk change fonctionality
-it('should be able to bulk change', async () => {
-  const user = userEvent.setup();
-  issuesHandler.setIsAdmin(true);
-  renderIssueApp(mockCurrentUser({ isLoggedIn: true }));
-
-  // Check that the bulk button has correct behavior
-  expect(await screen.findByRole('button', { name: 'bulk_change' })).toBeDisabled();
-  await user.click(screen.getByRole('checkbox', { name: 'issues.select_all_issues' }));
-  expect(
-    screen.getByRole('button', { name: 'issues.bulk_change_X_issues.500' })
-  ).toBeInTheDocument();
-  await user.click(screen.getByRole('button', { name: 'issues.bulk_change_X_issues.500' }));
-  await user.click(screen.getByRole('button', { name: 'cancel' }));
-  expect(screen.getByRole('button', { name: 'issues.bulk_change_X_issues.500' })).toHaveFocus();
-  await user.click(screen.getByRole('checkbox', { name: 'issues.select_all_issues' }));
-
-  // Check that we bulk change the selected issue
-  const issueBoxFixThat = within(await screen.findByRole('region', { name: 'Fix that' }));
-
-  expect(
-    issueBoxFixThat.getByRole('button', {
-      name: 'issue.type.type_x_click_to_change.issue.type.CODE_SMELL',
-    })
-  ).toBeInTheDocument();
-
-  await user.click(screen.getByRole('checkbox', { name: 'issues.action_select.label.Fix that' }));
-  expect(screen.getByRole('button', { name: 'issues.bulk_change_X_issues.1' })).toBeInTheDocument();
-  await user.click(screen.getByRole('button', { name: 'issues.bulk_change_X_issues.1' }));
-
-  await user.click(screen.getByRole('textbox', { name: 'issue.comment.formlink' }));
-  await user.keyboard('New Comment');
-  expect(screen.getByRole('button', { name: 'apply' })).toBeDisabled();
-
-  await selectEvent.select(screen.getByRole('combobox', { name: 'issue.set_type' }), [
-    'issue.type.BUG',
-  ]);
-  await user.click(screen.getByRole('button', { name: 'apply' }));
-
-  expect(
-    issueBoxFixThat.getByRole('button', {
-      name: 'issue.type.type_x_click_to_change.issue.type.BUG',
-    })
-  ).toBeInTheDocument();
-});
-
-it('should show warning when not all issues are accessible', async () => {
-  const user = userEvent.setup();
-  renderProjectIssuesApp('project/issues?id=myproject', {
-    canBrowseAllChildProjects: false,
-    qualifier: ComponentQualifier.Portfolio,
+const ui = {
+  loading: byLabelText('loading'),
+  issueItems: byRole('region'),
+
+  issueItem1: byRole('region', { name: 'Issue with no location message' }),
+  issueItem2: byRole('region', { name: 'FlowIssue' }),
+  issueItem3: byRole('region', { name: 'Issue on file' }),
+  issueItem4: byRole('region', { name: 'Fix this' }),
+  issueItem5: byRole('region', { name: 'Fix that' }),
+  issueItem6: byRole('region', { name: 'Second issue' }),
+  issueItem7: byRole('region', { name: 'Issue with tags' }),
+  issueItem8: byRole('region', { name: 'Issue on page 2' }),
+
+  codeSmellIssueTypeFilter: byRole('checkbox', { name: 'issue.type.CODE_SMELL' }),
+  clearAllFilters: byRole('button', { name: 'clear_all_filters' }),
+};
+
+async function waitOnDataLoaded() {
+  await waitFor(() => {
+    expect(ui.loading.query()).not.toBeInTheDocument();
   });
-  expect(await screen.findByRole('alert', { name: 'alert.tooltip.warning' })).toBeInTheDocument();
+}
 
-  await act(async () => {
-    await user.keyboard('{ArrowRight}');
+describe('issues app', () => {
+  describe('rendering', () => {
+    it('should show warning when not all issues are accessible', async () => {
+      const user = userEvent.setup();
+      renderProjectIssuesApp('project/issues?id=myproject', {
+        canBrowseAllChildProjects: false,
+        qualifier: ComponentQualifier.Portfolio,
+      });
+      expect(screen.getByRole('alert', { name: 'alert.tooltip.warning' })).toBeInTheDocument();
+
+      await act(async () => {
+        await user.keyboard('{ArrowRight}');
+      });
+
+      expect(screen.getByRole('alert', { name: 'alert.tooltip.warning' })).toBeInTheDocument();
+    });
+
+    it('should support OWASP Top 10 version 2021', async () => {
+      const user = userEvent.setup();
+      renderIssueApp();
+      await user.click(screen.getByRole('button', { name: 'issues.facet.standards' }));
+      const owaspTop102021 = screen.getByRole('button', { name: 'issues.facet.owaspTop10_2021' });
+      expect(owaspTop102021).toBeInTheDocument();
+
+      await user.click(owaspTop102021);
+      await Promise.all(
+        issuesHandler.owasp2021FacetList().values.map(async ({ val }) => {
+          const standard = await issuesHandler.getStandards();
+          /* eslint-disable-next-line testing-library/render-result-naming-convention */
+          const linkName = renderOwaspTop102021Category(standard, val);
+          expect(screen.getByRole('checkbox', { name: linkName })).toBeInTheDocument();
+        })
+      );
+    });
   });
 
-  expect(await screen.findByRole('alert', { name: 'alert.tooltip.warning' })).toBeInTheDocument();
+  describe('navigation', () => {
+    it('should handle keyboard navigation in list and open / close issues', async () => {
+      const user = userEvent.setup();
+      renderIssueApp();
+
+      // Navigate to 2nd issue
+      await user.keyboard('{ArrowDown}');
+
+      // Select it
+      await act(async () => {
+        await user.keyboard('{ArrowRight}');
+      });
+      expect(
+        screen.getByRole('heading', { name: issuesHandler.list[1].issue.message })
+      ).toBeInTheDocument();
+
+      // Go back
+      await act(async () => {
+        await user.keyboard('{ArrowLeft}');
+      });
+      expect(
+        screen.queryByRole('heading', { name: issuesHandler.list[1].issue.message })
+      ).not.toBeInTheDocument();
+
+      // Navigate to 1st issue and select it
+      await user.keyboard('{ArrowUp}');
+      await user.keyboard('{ArrowUp}');
+      await act(async () => {
+        await user.keyboard('{ArrowRight}');
+      });
+      expect(
+        screen.getByRole('heading', { name: issuesHandler.list[0].issue.message })
+      ).toBeInTheDocument();
+    });
+
+    it('should open issue and navigate', async () => {
+      const user = userEvent.setup();
+      renderIssueApp();
+
+      // Select an issue with an advanced rule
+      await user.click(await screen.findByRole('region', { name: 'Fix that' }));
+      expect(screen.getByRole('tab', { name: 'issue.tabs.code' })).toBeInTheDocument();
+
+      // Are rule headers present?
+      expect(screen.getByRole('heading', { level: 1, name: 'Fix that' })).toBeInTheDocument();
+      expect(screen.getByRole('link', { name: 'advancedRuleId' })).toBeInTheDocument();
+
+      // Select the "why is this an issue" tab and check its content
+      await user.click(
+        screen.getByRole('tab', { name: `coding_rules.description_section.title.root_cause` })
+      );
+      expect(screen.getByRole('heading', { name: 'Because' })).toBeInTheDocument();
+
+      // Select the "how to fix it" tab
+      await user.click(
+        screen.getByRole('tab', { name: `coding_rules.description_section.title.how_to_fix` })
+      );
+
+      // Is the context selector present with the expected values and default selection?
+      expect(screen.getByRole('button', { name: 'Context 2' })).toBeInTheDocument();
+      expect(screen.getByRole('button', { name: 'Context 3' })).toBeInTheDocument();
+      expect(screen.getByRole('button', { name: 'Spring' })).toBeInTheDocument();
+      expect(
+        screen.getByRole('button', { name: 'coding_rules.description_context.other' })
+      ).toBeInTheDocument();
+      expect(screen.getByRole('button', { name: 'Spring' })).toHaveClass('selected');
+
+      // Select context 2 and check tab content
+      await user.click(screen.getByRole('button', { name: 'Context 2' }));
+      expect(screen.getByText('Context 2 content')).toBeInTheDocument();
+
+      // Select the "other" context and check tab content
+      await user.click(
+        screen.getByRole('button', { name: 'coding_rules.description_context.other' })
+      );
+      expect(screen.getByText('coding_rules.context.others.title')).toBeInTheDocument();
+      expect(screen.getByText('coding_rules.context.others.description.first')).toBeInTheDocument();
+      expect(
+        screen.getByText('coding_rules.context.others.description.second')
+      ).toBeInTheDocument();
+
+      // Select the main info tab and check its content
+      await user.click(
+        screen.getByRole('tab', { name: `coding_rules.description_section.title.more_info` })
+      );
+      expect(screen.getByRole('heading', { name: 'Link' })).toBeInTheDocument();
+
+      // Check for extended description (eslint FP)
+      // eslint-disable-next-line jest-dom/prefer-in-document
+      expect(screen.getAllByText('Extended Description')).toHaveLength(1);
+
+      // Select the previous issue (with a simple rule) through keyboard shortcut
+      await act(async () => {
+        await user.keyboard('{ArrowUp}');
+      });
+
+      // Are rule headers present?
+      expect(screen.getByRole('heading', { level: 1, name: 'Fix this' })).toBeInTheDocument();
+      expect(screen.getByRole('link', { name: 'simpleRuleId' })).toBeInTheDocument();
+
+      // Select the "why is this an issue tab" and check its content
+      await user.click(
+        screen.getByRole('tab', { name: `coding_rules.description_section.title.root_cause` })
+      );
+      expect(screen.getByRole('heading', { name: 'Default' })).toBeInTheDocument();
+
+      // Select the previous issue (with a simple rule) through keyboard shortcut
+      await act(async () => {
+        await user.keyboard('{ArrowUp}');
+      });
+
+      // Are rule headers present?
+      expect(screen.getByRole('heading', { level: 1, name: 'Issue on file' })).toBeInTheDocument();
+      expect(screen.getByRole('link', { name: 'simpleRuleId' })).toBeInTheDocument();
+
+      // The "Where is the issue" tab should be selected by default. Check its content
+      expect(screen.getByRole('region', { name: 'Issue on file' })).toBeInTheDocument();
+      expect(
+        screen.getByRole('row', {
+          name: '2 * SonarQube',
+        })
+      ).toBeInTheDocument();
+    });
+
+    it('should be able to navigate to other issue located in the same file', async () => {
+      const user = userEvent.setup();
+      renderIssueApp();
+
+      await user.click(await ui.issueItem5.find());
+      expect(ui.issueItem6.get()).toBeInTheDocument();
+
+      await user.click(ui.issueItem6.get());
+      expect(screen.getByRole('heading', { level: 1, name: 'Second issue' })).toBeInTheDocument();
+    });
+
+    it('should be able to show more issues', async () => {
+      const user = userEvent.setup();
+      renderIssueApp();
+
+      expect(await ui.issueItems.findAll()).toHaveLength(7);
+      expect(ui.issueItem8.query()).not.toBeInTheDocument();
+
+      await user.click(screen.getByRole('button', { name: 'show_more' }));
+      expect(ui.issueItems.getAll()).toHaveLength(8);
+      expect(ui.issueItem8.get()).toBeInTheDocument();
+    });
+
+    // Improve this to include all the bulk change fonctionality
+    it('should be able to bulk change', async () => {
+      const user = userEvent.setup();
+      issuesHandler.setIsAdmin(true);
+      renderIssueApp(mockLoggedInUser());
+
+      // Check that the bulk button has correct behavior
+      expect(screen.getByRole('button', { name: 'bulk_change' })).toBeDisabled();
+      await user.click(screen.getByRole('checkbox', { name: 'issues.select_all_issues' }));
+      expect(
+        screen.getByRole('button', { name: 'issues.bulk_change_X_issues.8' })
+      ).toBeInTheDocument();
+      await user.click(screen.getByRole('button', { name: 'issues.bulk_change_X_issues.8' }));
+      await user.click(screen.getByRole('button', { name: 'cancel' }));
+      expect(screen.getByRole('button', { name: 'issues.bulk_change_X_issues.8' })).toHaveFocus();
+      await user.click(screen.getByRole('checkbox', { name: 'issues.select_all_issues' }));
+
+      // Check that we bulk change the selected issue
+      const issueBoxFixThat = within(screen.getByRole('region', { name: 'Fix that' }));
+
+      expect(
+        issueBoxFixThat.getByRole('button', {
+          name: 'issue.type.type_x_click_to_change.issue.type.CODE_SMELL',
+        })
+      ).toBeInTheDocument();
+
+      await user.click(
+        screen.getByRole('checkbox', { name: 'issues.action_select.label.Fix that' })
+      );
+      await user.click(screen.getByRole('button', { name: 'issues.bulk_change_X_issues.1' }));
+
+      await user.click(screen.getByRole('textbox', { name: 'issue.comment.formlink' }));
+      await user.keyboard('New Comment');
+      expect(screen.getByRole('button', { name: 'apply' })).toBeDisabled();
+
+      await selectEvent.select(screen.getByRole('combobox', { name: 'issue.set_type' }), [
+        'issue.type.BUG',
+      ]);
+      await user.click(screen.getByRole('button', { name: 'apply' }));
+
+      expect(
+        issueBoxFixThat.getByRole('button', {
+          name: 'issue.type.type_x_click_to_change.issue.type.BUG',
+        })
+      ).toBeInTheDocument();
+    });
+  });
+  describe('filtering', () => {
+    it('should allow to reset all facets', async () => {
+      const user = userEvent.setup();
+      renderIssueApp();
+
+      await user.click(ui.codeSmellIssueTypeFilter.get());
+      expect(ui.codeSmellIssueTypeFilter.get()).toBeChecked();
+      expect(ui.issueItem4.query()).not.toBeInTheDocument();
+
+      await user.click(ui.clearAllFilters.get());
+      expect(ui.codeSmellIssueTypeFilter.get()).not.toBeChecked();
+      expect(ui.issueItem4.get()).toBeInTheDocument();
+    });
+
+    it('should handle filtering from a specific issue properly', async () => {
+      const user = userEvent.setup();
+      renderIssueApp();
+
+      // Get first issue list item
+      const issueItem = await ui.issueItem2.find();
+
+      // Ensure issue type filter is unchecked
+      expect(ui.codeSmellIssueTypeFilter.get()).not.toBeChecked();
+      expect(ui.issueItem2.get()).toBeInTheDocument();
+      expect(ui.issueItem3.get()).toBeInTheDocument();
+
+      // Open filter similar issue dropdown
+      await user.click(
+        await within(issueItem).findByRole('button', { name: 'issue.filter_similar_issues' })
+      );
+
+      // Select type
+      await user.click(
+        await within(issueItem).findByRole('button', { name: 'issue.type.CODE_SMELL' })
+      );
+
+      // Ensure issue type filter is now checked
+      expect(ui.codeSmellIssueTypeFilter.get()).toBeChecked();
+      expect(ui.issueItem2.get()).toBeInTheDocument();
+      expect(ui.issueItem3.query()).not.toBeInTheDocument();
+    });
+
+    it('should allow to only show my issues', async () => {
+      const user = userEvent.setup();
+      renderIssueApp(mockLoggedInUser());
+      await waitOnDataLoaded();
+
+      // By default, it should show all issues
+      expect(ui.issueItem2.get()).toBeInTheDocument();
+      expect(ui.issueItem3.get()).toBeInTheDocument();
+
+      // Only show my issues
+      await user.click(screen.getByRole('button', { name: 'issues.my_issues' }));
+      expect(ui.issueItem2.query()).not.toBeInTheDocument();
+      expect(ui.issueItem3.get()).toBeInTheDocument();
+
+      // Show all issues again
+      await user.click(screen.getByRole('button', { name: 'all' }));
+      expect(ui.issueItem2.get()).toBeInTheDocument();
+      expect(ui.issueItem3.get()).toBeInTheDocument();
+    });
+  });
 });
 
-it('should show secondary location even when no message is present', async () => {
-  renderProjectIssuesApp('project/issues?issues=issue101&open=issue101&id=myproject');
+describe('issues item', () => {
+  it('should navigate to Why is this an issue tab', async () => {
+    renderProjectIssuesApp('project/issues?issues=issue2&open=issue2&id=myproject&why=1');
 
-  expect(await screen.findByRole('button', { name: '1 issue.location_x.1' })).toBeInTheDocument();
-  expect(screen.getByRole('button', { name: '2 issue.location_x.2' })).toBeInTheDocument();
-});
+    expect(
+      await screen.findByRole('tab', {
+        name: `coding_rules.description_section.title.root_cause`,
+        selected: true,
+      })
+    ).toBeInTheDocument();
+  });
 
-it('should interact with flows and locations', async () => {
-  const user = userEvent.setup();
-  renderProjectIssuesApp('project/issues?issues=issue11&open=issue11&id=myproject');
-  const dataFlowButton = await screen.findByRole('button', { name: 'Backtracking 1' });
-  const exectionFlowButton = await screen.findByRole('button', { name: 'issue.execution_flow' });
-
-  let dataLocation1Button = screen.getByRole('button', { name: '1 Data location 1' });
-  let dataLocation2Button = screen.getByRole('button', { name: '2 Data location 2' });
-
-  expect(dataFlowButton).toBeInTheDocument();
-  expect(dataLocation1Button).toBeInTheDocument();
-  expect(dataLocation2Button).toBeInTheDocument();
-
-  await user.click(dataFlowButton);
-  // Colapsing flow
-  expect(dataLocation1Button).not.toBeInTheDocument();
-  expect(dataLocation2Button).not.toBeInTheDocument();
-
-  await user.click(exectionFlowButton);
-  expect(screen.getByRole('button', { name: '1 Execution location 1' })).toBeInTheDocument();
-  expect(screen.getByRole('button', { name: '2 Execution location 2' })).toBeInTheDocument();
-  expect(screen.getByRole('button', { name: '3 Execution location 3' })).toBeInTheDocument();
-
-  // Keyboard interaction
-  await user.click(dataFlowButton);
-  dataLocation1Button = screen.getByRole('button', { name: '1 Data location 1' });
-  dataLocation2Button = screen.getByRole('button', { name: '2 Data location 2' });
-
-  //Location navigation
-  await user.keyboard('{Alt>}{ArrowDown}{/Alt}');
-  expect(dataLocation1Button).toHaveClass('selected');
-  await user.keyboard('{Alt>}{ArrowDown}{/Alt}');
-  expect(dataLocation1Button).not.toHaveClass('selected');
-  expect(dataLocation2Button).toHaveClass('selected');
-  await user.keyboard('{Alt>}{ArrowDown}{/Alt}');
-  expect(dataLocation1Button).not.toHaveClass('selected');
-  expect(dataLocation2Button).not.toHaveClass('selected');
-  await user.keyboard('{Alt>}{ArrowUp}{/Alt}');
-  expect(dataLocation1Button).not.toHaveClass('selected');
-  expect(dataLocation2Button).toHaveClass('selected');
-
-  //Flow navigation
-  await user.keyboard('{Alt>}{ArrowRight}{/Alt}');
-  expect(screen.getByRole('button', { name: '1 Execution location 1' })).toHaveClass('selected');
-  await user.keyboard('{Alt>}{ArrowLeft}{/Alt}');
-  expect(screen.getByRole('button', { name: '1 Data location 1' })).toHaveClass('selected');
-});
+  it('should show secondary location even when no message is present', async () => {
+    renderProjectIssuesApp('project/issues?issues=issue101&open=issue101&id=myproject');
 
-it('should show education principles', async () => {
-  const user = userEvent.setup();
-  renderProjectIssuesApp('project/issues?issues=issue2&open=issue2&id=myproject');
-  await user.click(
-    await screen.findByRole('tab', { name: `coding_rules.description_section.title.more_info` })
-  );
-  expect(screen.getByRole('heading', { name: 'Defense-In-Depth', level: 3 })).toBeInTheDocument();
-});
+    expect(await screen.findByRole('button', { name: '1 issue.location_x.1' })).toBeInTheDocument();
+    expect(screen.getByRole('button', { name: '2 issue.location_x.2' })).toBeInTheDocument();
+  });
 
-it('should open issue and navigate', async () => {
-  const user = userEvent.setup();
+  it('should interact with flows and locations', async () => {
+    const user = userEvent.setup();
+    renderProjectIssuesApp('project/issues?issues=issue11&open=issue11&id=myproject');
+    const dataFlowButton = await screen.findByRole('button', { name: 'Backtracking 1' });
+    const exectionFlowButton = screen.getByRole('button', { name: 'issue.execution_flow' });
+
+    let dataLocation1Button = screen.getByRole('button', { name: '1 Data location 1' });
+    let dataLocation2Button = screen.getByRole('button', { name: '2 Data location 2' });
+
+    expect(dataFlowButton).toBeInTheDocument();
+    expect(dataLocation1Button).toBeInTheDocument();
+    expect(dataLocation2Button).toBeInTheDocument();
+
+    await user.click(dataFlowButton);
+    // Colapsing flow
+    expect(dataLocation1Button).not.toBeInTheDocument();
+    expect(dataLocation2Button).not.toBeInTheDocument();
+
+    await user.click(exectionFlowButton);
+    expect(screen.getByRole('button', { name: '1 Execution location 1' })).toBeInTheDocument();
+    expect(screen.getByRole('button', { name: '2 Execution location 2' })).toBeInTheDocument();
+    expect(screen.getByRole('button', { name: '3 Execution location 3' })).toBeInTheDocument();
+
+    // Keyboard interaction
+    await user.click(dataFlowButton);
+    dataLocation1Button = screen.getByRole('button', { name: '1 Data location 1' });
+    dataLocation2Button = screen.getByRole('button', { name: '2 Data location 2' });
+
+    // Location navigation
+    await user.keyboard('{Alt>}{ArrowDown}{/Alt}');
+    expect(dataLocation1Button).toHaveClass('selected');
+    await user.keyboard('{Alt>}{ArrowDown}{/Alt}');
+    expect(dataLocation1Button).not.toHaveClass('selected');
+    expect(dataLocation2Button).toHaveClass('selected');
+    await user.keyboard('{Alt>}{ArrowDown}{/Alt}');
+    expect(dataLocation1Button).not.toHaveClass('selected');
+    expect(dataLocation2Button).not.toHaveClass('selected');
+    await user.keyboard('{Alt>}{ArrowUp}{/Alt}');
+    expect(dataLocation1Button).not.toHaveClass('selected');
+    expect(dataLocation2Button).toHaveClass('selected');
+
+    // Flow navigation
+    await user.keyboard('{Alt>}{ArrowRight}{/Alt}');
+    expect(screen.getByRole('button', { name: '1 Execution location 1' })).toHaveClass('selected');
+    await user.keyboard('{Alt>}{ArrowLeft}{/Alt}');
+    expect(screen.getByRole('button', { name: '1 Data location 1' })).toHaveClass('selected');
+  });
 
-  renderIssueApp(mockCurrentUser());
+  it('should show education principles', async () => {
+    const user = userEvent.setup();
+    renderProjectIssuesApp('project/issues?issues=issue2&open=issue2&id=myproject');
+    await user.click(
+      await screen.findByRole('tab', { name: `coding_rules.description_section.title.more_info` })
+    );
+    expect(screen.getByRole('heading', { name: 'Defense-In-Depth', level: 3 })).toBeInTheDocument();
+  });
 
-  // Select an issue with an advanced rule
-  expect(await screen.findByRole('region', { name: 'Fix that' })).toBeInTheDocument();
-  await user.click(screen.getByRole('region', { name: 'Fix that' }));
-  expect(screen.getByRole('tab', { name: 'issue.tabs.code' })).toBeInTheDocument();
+  it('should be able to perform action on issues', async () => {
+    const user = userEvent.setup();
+    issuesHandler.setIsAdmin(true);
+    renderIssueApp();
 
-  // Are rule headers present?
-  expect(screen.getByRole('heading', { level: 1, name: 'Fix that' })).toBeInTheDocument();
-  expect(screen.getByRole('link', { name: 'advancedRuleId' })).toBeInTheDocument();
+    // Get 'Fix that' issue list item
+    const listItem = within(await screen.findByRole('region', { name: 'Fix that' }));
 
-  // Select the "why is this an issue" tab and check its content
-  expect(
-    screen.getByRole('tab', { name: `coding_rules.description_section.title.root_cause` })
-  ).toBeInTheDocument();
-  await user.click(
-    screen.getByRole('tab', { name: `coding_rules.description_section.title.root_cause` })
-  );
-  expect(screen.getByRole('heading', { name: 'Because' })).toBeInTheDocument();
-
-  // Select the "how to fix it" tab
-  expect(
-    screen.getByRole('tab', { name: `coding_rules.description_section.title.how_to_fix` })
-  ).toBeInTheDocument();
-  await user.click(
-    screen.getByRole('tab', { name: `coding_rules.description_section.title.how_to_fix` })
-  );
+    // Change issue type
+    await user.click(
+      listItem.getByRole('button', {
+        name: `issue.type.type_x_click_to_change.issue.type.CODE_SMELL`,
+      })
+    );
+    expect(listItem.getByText('issue.type.BUG')).toBeInTheDocument();
+    expect(listItem.getByText('issue.type.VULNERABILITY')).toBeInTheDocument();
 
-  // Is the context selector present with the expected values and default selection?
-  expect(screen.getByRole('button', { name: 'Context 2' })).toBeInTheDocument();
-  expect(screen.getByRole('button', { name: 'Context 3' })).toBeInTheDocument();
-  expect(screen.getByRole('button', { name: 'Spring' })).toBeInTheDocument();
-  expect(
-    screen.getByRole('button', { name: 'coding_rules.description_context.other' })
-  ).toBeInTheDocument();
-  expect(screen.getByRole('button', { name: 'Spring' })).toHaveClass('selected');
-
-  // Select context 2 and check tab content
-  await user.click(screen.getByRole('button', { name: 'Context 2' }));
-  expect(screen.getByText('Context 2 content')).toBeInTheDocument();
-
-  // Select the "other" context and check tab content
-  await user.click(screen.getByRole('button', { name: 'coding_rules.description_context.other' }));
-  expect(screen.getByText('coding_rules.context.others.title')).toBeInTheDocument();
-  expect(screen.getByText('coding_rules.context.others.description.first')).toBeInTheDocument();
-  expect(screen.getByText('coding_rules.context.others.description.second')).toBeInTheDocument();
-
-  // Select the main info tab and check its content
-  expect(
-    screen.getByRole('tab', { name: `coding_rules.description_section.title.more_info` })
-  ).toBeInTheDocument();
-  await user.click(
-    screen.getByRole('tab', { name: `coding_rules.description_section.title.more_info` })
-  );
-  expect(screen.getByRole('heading', { name: 'Link' })).toBeInTheDocument();
+    await user.click(listItem.getByText('issue.type.VULNERABILITY'));
+    expect(
+      listItem.getByRole('button', {
+        name: `issue.type.type_x_click_to_change.issue.type.VULNERABILITY`,
+      })
+    ).toBeInTheDocument();
 
-  // check for extended description
-  const extendedDescriptions = screen.getAllByText('Extended Description');
+    // Change issue severity
+    expect(listItem.getByText('severity.MAJOR')).toBeInTheDocument();
 
-  // FP
-  // eslint-disable-next-line jest-dom/prefer-in-document
-  expect(extendedDescriptions).toHaveLength(1);
+    await user.click(
+      listItem.getByRole('button', {
+        name: `issue.severity.severity_x_click_to_change.severity.MAJOR`,
+      })
+    );
+    expect(listItem.getByText('severity.MINOR')).toBeInTheDocument();
+    expect(listItem.getByText('severity.INFO')).toBeInTheDocument();
+    await user.click(listItem.getByText('severity.MINOR'));
+    expect(
+      listItem.getByRole('button', {
+        name: `issue.severity.severity_x_click_to_change.severity.MINOR`,
+      })
+    ).toBeInTheDocument();
 
-  // Select the previous issue (with a simple rule) through keyboard shortcut
-  await act(async () => {
-    await user.keyboard('{ArrowUp}');
-  });
+    // Change issue status
+    expect(listItem.getByText('issue.status.OPEN')).toBeInTheDocument();
 
-  // Are rule headers present?
-  expect(screen.getByRole('heading', { level: 1, name: 'Fix this' })).toBeInTheDocument();
-  expect(screen.getByRole('link', { name: 'simpleRuleId' })).toBeInTheDocument();
+    await user.click(listItem.getByText('issue.status.OPEN'));
+    expect(listItem.getByText('issue.transition.confirm')).toBeInTheDocument();
+    expect(listItem.getByText('issue.transition.resolve')).toBeInTheDocument();
 
-  // Select the "why is this an issue tab" and check its content
-  expect(
-    screen.getByRole('tab', { name: `coding_rules.description_section.title.root_cause` })
-  ).toBeInTheDocument();
-  await user.click(
-    screen.getByRole('tab', { name: `coding_rules.description_section.title.root_cause` })
-  );
-  expect(screen.getByRole('heading', { name: 'Default' })).toBeInTheDocument();
+    await user.click(listItem.getByText('issue.transition.confirm'));
+    expect(
+      listItem.getByRole('button', {
+        name: `issue.transition.status_x_click_to_change.issue.status.CONFIRMED`,
+      })
+    ).toBeInTheDocument();
 
-  // Select the previous issue (with a simple rule) through keyboard shortcut
-  await act(async () => {
-    await user.keyboard('{ArrowUp}');
+    // As won't fix
+    await user.click(listItem.getByText('issue.status.CONFIRMED'));
+    await user.click(listItem.getByText('issue.transition.wontfix'));
+    // Comment should open and close
+    expect(listItem.getByRole('button', { name: 'issue.comment.formlink' })).toBeInTheDocument();
+    await user.keyboard('test');
+    await user.click(listItem.getByRole('button', { name: 'issue.comment.formlink' }));
+    expect(
+      listItem.queryByRole('button', { name: 'issue.comment.submit' })
+    ).not.toBeInTheDocument();
+
+    // Assign issue to a different user
+    await user.click(
+      listItem.getByRole('button', {
+        name: `issue.assign.unassigned_click_to_assign`,
+      })
+    );
+    await user.click(listItem.getByRole('searchbox', { name: 'search.search_for_users' }));
+    await user.keyboard('luke');
+    expect(listItem.getByText('Skywalker')).toBeInTheDocument();
+    await user.keyboard('{ArrowUp}{enter}');
+    expect(
+      listItem.getByRole('button', {
+        name: 'issue.assign.assigned_to_x_click_to_change.luke',
+      })
+    ).toBeInTheDocument();
+
+    // Add comment to the issue
+    await user.click(
+      listItem.getByRole('button', {
+        name: `issue.comment.add_comment`,
+      })
+    );
+    await user.keyboard('comment');
+    await user.click(listItem.getByRole('button', { name: 'issue.comment.formlink' }));
+    expect(listItem.getByText('comment')).toBeInTheDocument();
+
+    // Cancel editing the comment
+    await user.click(listItem.getByRole('button', { name: 'issue.comment.edit' }));
+    await user.keyboard('New ');
+    await user.click(listItem.getByRole('button', { name: 'issue.comment.edit.cancel' }));
+    expect(listItem.queryByText('New comment')).not.toBeInTheDocument();
+
+    // Edit the comment
+    await user.click(listItem.getByRole('button', { name: 'issue.comment.edit' }));
+    await user.keyboard('New ');
+    await user.click(listItem.getByText('save'));
+    expect(listItem.getByText('New comment')).toBeInTheDocument();
+
+    // Delete the comment
+    await user.click(listItem.getByRole('button', { name: 'issue.comment.delete' }));
+    await user.click(listItem.getByRole('button', { name: 'delete' })); // Confirm button
+    expect(listItem.queryByText('New comment')).not.toBeInTheDocument();
+
+    // Add comment using keyboard
+    await user.click(
+      listItem.getByRole('button', {
+        name: `issue.comment.add_comment`,
+      })
+    );
+    await user.keyboard('comment');
+    await user.keyboard('{Control>}{enter}{/Control}');
+    expect(listItem.getByText('comment')).toBeInTheDocument();
+
+    // Edit the comment using keyboard
+    await user.click(listItem.getByRole('button', { name: 'issue.comment.edit' }));
+    await user.keyboard('New ');
+    await user.keyboard('{Control>}{enter}{/Control}');
+    expect(listItem.getByText('New comment')).toBeInTheDocument();
+    await user.keyboard('{Escape}');
+
+    // Change tags
+    expect(listItem.getByText('issue.no_tag')).toBeInTheDocument();
+    await user.click(listItem.getByText('issue.no_tag'));
+    expect(listItem.getByRole('searchbox', { name: 'search.search_for_tags' })).toBeInTheDocument();
+    expect(listItem.getByText('android')).toBeInTheDocument();
+    expect(listItem.getByText('accessibility')).toBeInTheDocument();
+
+    await user.click(listItem.getByText('accessibility'));
+    await user.click(listItem.getByText('android'));
+    expect(listItem.getByTitle('accessibility, android')).toBeInTheDocument();
+
+    // Unselect
+    await user.click(screen.getByText('accessibility'));
+    expect(screen.getByTitle('android')).toBeInTheDocument();
+
+    await user.click(screen.getByRole('searchbox', { name: 'search.search_for_tags' }));
+    await user.keyboard('addNewTag');
+    expect(
+      screen.getByRole('checkbox', { name: 'create_new_element: addnewtag' })
+    ).toBeInTheDocument();
   });
 
-  // Are rule headers present?
-  expect(screen.getByRole('heading', { level: 1, name: 'Issue on file' })).toBeInTheDocument();
-  expect(screen.getByRole('link', { name: 'simpleRuleId' })).toBeInTheDocument();
-
-  // The "Where is the issue" tab should be selected by default. Check its content
-  expect(screen.getByRole('region', { name: 'Issue on file' })).toBeInTheDocument();
-  expect(
-    screen.getByRole('row', {
-      name: '2 * SonarQube',
-    })
-  ).toBeInTheDocument();
-});
+  it('should not allow performing actions when user does not have permission', async () => {
+    const user = userEvent.setup();
+    renderIssueApp();
 
-it('should be able to navigate to other issue located in the same file', async () => {
-  const user = userEvent.setup();
-  renderIssueApp();
-  await user.click(await screen.findByRole('region', { name: 'Fix that' }));
-  expect(await screen.findByRole('region', { name: 'Second issue' })).toBeInTheDocument();
-  await user.click(await screen.findByRole('region', { name: 'Second issue' }));
-  expect(screen.getByRole('heading', { level: 1, name: 'Second issue' })).toBeInTheDocument();
-});
+    await user.click(await ui.issueItem4.find());
 
-it('should support OWASP Top 10 version 2021', async () => {
-  const user = userEvent.setup();
-  renderIssueApp();
-  await user.click(await screen.findByRole('button', { name: 'issues.facet.standards' }));
-  const owaspTop102021 = screen.getByRole('button', { name: 'issues.facet.owaspTop10_2021' });
-  expect(owaspTop102021).toBeInTheDocument();
-
-  await user.click(owaspTop102021);
-  await Promise.all(
-    issuesHandler.owasp2021FacetList().values.map(async ({ val }) => {
-      const standard = await issuesHandler.getStandards();
-      /* eslint-disable-next-line testing-library/render-result-naming-convention */
-      const linkName = renderOwaspTop102021Category(standard, val);
-      expect(await screen.findByRole('checkbox', { name: linkName })).toBeInTheDocument();
-    })
-  );
-});
+    expect(
+      screen.queryByRole('button', {
+        name: `issue.assign.unassigned_click_to_assign`,
+      })
+    ).not.toBeInTheDocument();
+    expect(
+      screen.queryByRole('button', {
+        name: `issue.type.type_x_click_to_change.issue.type.CODE_SMELL`,
+      })
+    ).not.toBeInTheDocument();
+
+    await user.click(
+      screen.getByRole('button', {
+        name: `issue.comment.add_comment`,
+      })
+    );
+    expect(screen.queryByRole('button', { name: 'issue.comment.submit' })).not.toBeInTheDocument();
+    expect(
+      screen.queryByRole('button', {
+        name: `issue.transition.status_x_click_to_change.issue.status.OPEN`,
+      })
+    ).not.toBeInTheDocument();
+    expect(
+      screen.queryByRole('button', {
+        name: `issue.severity.severity_x_click_to_change.severity.MAJOR`,
+      })
+    ).not.toBeInTheDocument();
+  });
 
-it('should be able to perform action on issues', async () => {
-  const user = userEvent.setup();
-  issuesHandler.setIsAdmin(true);
-  renderIssueApp();
-
-  // Select an issue with an advanced rule
-  await user.click(await screen.findByRole('region', { name: 'Fix that' }));
-
-  // changing issue type
-  expect(
-    screen.getByRole('button', {
-      name: `issue.type.type_x_click_to_change.issue.type.CODE_SMELL`,
-    })
-  ).toBeInTheDocument();
-  await user.click(
-    screen.getByRole('button', {
-      name: `issue.type.type_x_click_to_change.issue.type.CODE_SMELL`,
-    })
-  );
-  expect(screen.getByText('issue.type.BUG')).toBeInTheDocument();
-  expect(screen.getByText('issue.type.VULNERABILITY')).toBeInTheDocument();
-
-  await user.click(screen.getByText('issue.type.VULNERABILITY'));
-  expect(
-    screen.getByRole('button', {
-      name: `issue.type.type_x_click_to_change.issue.type.VULNERABILITY`,
-    })
-  ).toBeInTheDocument();
-
-  // changing issue severity
-  expect(screen.getByText('severity.MAJOR')).toBeInTheDocument();
-
-  await user.click(
-    screen.getByRole('button', {
-      name: `issue.severity.severity_x_click_to_change.severity.MAJOR`,
-    })
-  );
-  expect(screen.getByText('severity.MINOR')).toBeInTheDocument();
-  expect(screen.getByText('severity.INFO')).toBeInTheDocument();
-  await user.click(screen.getByText('severity.MINOR'));
-  expect(
-    screen.getByRole('button', {
-      name: `issue.severity.severity_x_click_to_change.severity.MINOR`,
-    })
-  ).toBeInTheDocument();
-
-  // changing issue status
-  expect(screen.getByText('issue.status.OPEN')).toBeInTheDocument();
-
-  await user.click(screen.getByText('issue.status.OPEN'));
-  expect(screen.getByText('issue.transition.confirm')).toBeInTheDocument();
-  expect(screen.getByText('issue.transition.resolve')).toBeInTheDocument();
-
-  await user.click(screen.getByText('issue.transition.confirm'));
-  expect(
-    screen.getByRole('button', {
-      name: `issue.transition.status_x_click_to_change.issue.status.CONFIRMED`,
-    })
-  ).toBeInTheDocument();
-
-  // As won't fix
-  await user.click(screen.getByText('issue.status.CONFIRMED'));
-  await user.click(screen.getByText('issue.transition.wontfix'));
-  // Comment should open and close
-  expect(screen.getByRole('button', { name: 'issue.comment.formlink' })).toBeInTheDocument();
-  await user.keyboard('test');
-  await user.click(screen.getByRole('button', { name: 'issue.comment.formlink' }));
-  expect(screen.queryByRole('button', { name: 'issue.comment.submit' })).not.toBeInTheDocument();
-
-  // assigning issue to a different user
-  expect(
-    screen.getByRole('button', {
-      name: `issue.assign.unassigned_click_to_assign`,
-    })
-  ).toBeInTheDocument();
-
-  await user.click(
-    screen.getByRole('button', {
-      name: `issue.assign.unassigned_click_to_assign`,
-    })
-  );
-  expect(screen.getByRole('searchbox', { name: 'search.search_for_users' })).toBeInTheDocument();
-
-  await user.click(screen.getByRole('searchbox', { name: 'search.search_for_users' }));
-  await user.keyboard('luke');
-  expect(screen.getByText('Skywalker')).toBeInTheDocument();
-  await user.keyboard('{ArrowUp}{enter}');
-  expect(
-    screen.getByRole('button', { name: 'issue.assign.assigned_to_x_click_to_change.luke' })
-  ).toBeInTheDocument();
-
-  // adding comment to the issue
-  expect(
-    screen.getByRole('button', {
-      name: `issue.comment.add_comment`,
-    })
-  ).toBeInTheDocument();
-
-  await user.click(
-    screen.getByRole('button', {
-      name: `issue.comment.add_comment`,
-    })
-  );
-  expect(screen.getByText('issue.comment.formlink')).toBeInTheDocument();
-  await user.keyboard('comment');
-  await user.click(screen.getByText('issue.comment.formlink'));
-  expect(screen.getByText('comment')).toBeInTheDocument();
-
-  // Cancel editing the comment
-  expect(screen.getByRole('button', { name: 'issue.comment.edit' })).toBeInTheDocument();
-  await user.click(screen.getByRole('button', { name: 'issue.comment.edit' }));
-  await user.keyboard('New ');
-  await user.click(screen.getByRole('button', { name: 'issue.comment.edit.cancel' }));
-  expect(screen.queryByText('New comment')).not.toBeInTheDocument();
-
-  // editing the comment
-  expect(screen.getByRole('button', { name: 'issue.comment.edit' })).toBeInTheDocument();
-  await user.click(screen.getByRole('button', { name: 'issue.comment.edit' }));
-  await user.keyboard('New ');
-  await user.click(screen.getByText('save'));
-  expect(screen.getByText('New comment')).toBeInTheDocument();
-
-  // deleting the comment
-  expect(screen.getByRole('button', { name: 'issue.comment.delete' })).toBeInTheDocument();
-  await user.click(screen.getByRole('button', { name: 'issue.comment.delete' }));
-  expect(screen.queryByText('New comment')).not.toBeInTheDocument();
-
-  // adding comment using keyboard
-  await user.click(screen.getByRole('textbox'));
-  await user.keyboard('comment');
-  await user.keyboard('{Control>}{enter}{/Control}');
-  expect(screen.getByText('comment')).toBeInTheDocument();
-
-  // editing the comment using keyboard
-  await user.click(screen.getByRole('button', { name: 'issue.comment.edit' }));
-  await user.keyboard('New ');
-  await user.keyboard('{Control>}{enter}{/Control}');
-  expect(screen.getByText('New comment')).toBeInTheDocument();
-  await user.keyboard('{Escape}');
-
-  // changing tags
-  expect(screen.getByText('issue.no_tag')).toBeInTheDocument();
-  await user.click(screen.getByText('issue.no_tag'));
-  expect(screen.getByRole('searchbox', { name: 'search.search_for_tags' })).toBeInTheDocument();
-  expect(screen.getByText('android')).toBeInTheDocument();
-  expect(screen.getByText('accessibility')).toBeInTheDocument();
-
-  await user.click(screen.getByText('accessibility'));
-  await user.click(screen.getByText('android'));
-  expect(screen.getByTitle('accessibility, android')).toBeInTheDocument();
-
-  // Unslect
-  await user.click(screen.getByText('accessibility'));
-  expect(screen.getByTitle('android')).toBeInTheDocument();
-
-  await user.click(screen.getByRole('searchbox', { name: 'search.search_for_tags' }));
-  await user.keyboard('addNewTag');
-  expect(
-    screen.getByRole('checkbox', { name: 'create_new_element: addnewtag' })
-  ).toBeInTheDocument();
-});
+  it('should open the actions popup using keyboard shortcut', async () => {
+    const user = userEvent.setup();
+    issuesHandler.setIsAdmin(true);
+    renderIssueApp();
+
+    // Select an issue with an advanced rule
+    await user.click(await ui.issueItem5.find());
+
+    // open severity popup on key press 'i'
+    await user.keyboard('i');
+    expect(screen.getByText('severity.MINOR')).toBeInTheDocument();
+    expect(screen.getByText('severity.INFO')).toBeInTheDocument();
+
+    // open status popup on key press 'f'
+    await user.keyboard('f');
+    expect(screen.getByText('issue.transition.confirm')).toBeInTheDocument();
+    expect(screen.getByText('issue.transition.resolve')).toBeInTheDocument();
+
+    // open comment popup on key press 'c'
+    await user.keyboard('c');
+    expect(screen.getByText('issue.comment.formlink')).toBeInTheDocument();
+    await user.keyboard('{Escape}');
+
+    // open tags popup on key press 't'
+    await user.keyboard('t');
+    expect(screen.getByRole('searchbox', { name: 'search.search_for_tags' })).toBeInTheDocument();
+    expect(screen.getByText('android')).toBeInTheDocument();
+    expect(screen.getByText('accessibility')).toBeInTheDocument();
+    // closing tags popup
+    await user.click(screen.getByText('issue.no_tag'));
+
+    // open assign popup on key press 'a'
+    await user.keyboard('a');
+    expect(screen.getByRole('searchbox', { name: 'search.search_for_tags' })).toBeInTheDocument();
+  });
 
-it('should not allow performing actions when user does not have permission', async () => {
-  const user = userEvent.setup();
-  renderIssueApp();
-
-  await user.click(await screen.findByRole('region', { name: 'Fix this' }));
-
-  expect(
-    screen.queryByRole('button', {
-      name: `issue.assign.unassigned_click_to_assign`,
-    })
-  ).not.toBeInTheDocument();
-  expect(
-    screen.queryByRole('button', {
-      name: `issue.type.type_x_click_to_change.issue.type.CODE_SMELL`,
-    })
-  ).not.toBeInTheDocument();
-
-  await user.click(
-    screen.getByRole('button', {
-      name: `issue.comment.add_comment`,
-    })
-  );
-  expect(screen.queryByRole('button', { name: 'issue.comment.submit' })).not.toBeInTheDocument();
-  expect(
-    screen.queryByRole('button', {
-      name: `issue.transition.status_x_click_to_change.issue.status.OPEN`,
-    })
-  ).not.toBeInTheDocument();
-  expect(
-    screen.queryByRole('button', {
-      name: `issue.severity.severity_x_click_to_change.severity.MAJOR`,
-    })
-  ).not.toBeInTheDocument();
-});
+  it('should not open the actions popup using keyboard shortcut when keyboard shortcut flag is disabled', async () => {
+    localStorage.setItem('sonarqube.preferences.keyboard_shortcuts_enabled', 'false');
+    const user = userEvent.setup();
+    issuesHandler.setIsAdmin(true);
+    renderIssueApp();
 
-it('should open the actions popup using keyboard shortcut', async () => {
-  const user = userEvent.setup();
-  issuesHandler.setIsAdmin(true);
-  renderIssueApp();
-
-  // Select an issue with an advanced rule
-  await user.click(await screen.findByRole('region', { name: 'Fix that' }));
-
-  // open severity popup on key press 'i'
-  await user.keyboard('i');
-  expect(screen.getByText('severity.MINOR')).toBeInTheDocument();
-  expect(screen.getByText('severity.INFO')).toBeInTheDocument();
-
-  // open status popup on key press 'f'
-  await user.keyboard('f');
-  expect(screen.getByText('issue.transition.confirm')).toBeInTheDocument();
-  expect(screen.getByText('issue.transition.resolve')).toBeInTheDocument();
-
-  // open comment popup on key press 'c'
-  await user.keyboard('c');
-  expect(screen.getByText('issue.comment.formlink')).toBeInTheDocument();
-  await user.keyboard('{Escape}');
-
-  // open tags popup on key press 't'
-  await user.keyboard('t');
-  expect(screen.getByRole('searchbox', { name: 'search.search_for_tags' })).toBeInTheDocument();
-  expect(screen.getByText('android')).toBeInTheDocument();
-  expect(screen.getByText('accessibility')).toBeInTheDocument();
-  // closing tags popup
-  await user.click(screen.getByText('issue.no_tag'));
-
-  // open assign popup on key press 'a'
-  await user.keyboard('a');
-  expect(screen.getByRole('searchbox', { name: 'search.search_for_tags' })).toBeInTheDocument();
-});
+    // Select an issue with an advanced rule
+    await user.click(await ui.issueItem5.find());
 
-it('should not open the actions popup using keyboard shortcut when keyboard shortcut flag is disabled', async () => {
-  localStorage.setItem('sonarqube.preferences.keyboard_shortcuts_enabled', 'false');
-  const user = userEvent.setup();
-  issuesHandler.setIsAdmin(true);
-  renderIssueApp();
+    // open status popup on key press 'f'
+    await user.keyboard('f');
+    expect(screen.queryByText('issue.transition.confirm')).not.toBeInTheDocument();
+    expect(screen.queryByText('issue.transition.resolve')).not.toBeInTheDocument();
 
-  // Select an issue with an advanced rule
-  await user.click(await screen.findByRole('region', { name: 'Fix that' }));
+    // open comment popup on key press 'c'
+    await user.keyboard('c');
+    expect(screen.queryByText('issue.comment.submit')).not.toBeInTheDocument();
+    localStorage.setItem('sonarqube.preferences.keyboard_shortcuts_enabled', 'true');
+  });
 
-  // open status popup on key press 'f'
-  await user.keyboard('f');
-  expect(screen.queryByText('issue.transition.confirm')).not.toBeInTheDocument();
-  expect(screen.queryByText('issue.transition.resolve')).not.toBeInTheDocument();
+  it('should show code tabs when any secondary location is selected', async () => {
+    const user = userEvent.setup();
+    renderIssueApp();
 
-  // open comment popup on key press 'c'
-  await user.keyboard('c');
-  expect(screen.queryByText('issue.comment.submit')).not.toBeInTheDocument();
-  localStorage.setItem('sonarqube.preferences.keyboard_shortcuts_enabled', 'true');
-});
+    await user.click(await ui.issueItem4.find());
+    expect(screen.getByRole('button', { name: '1 location 1' })).toBeInTheDocument();
+    expect(screen.getByRole('button', { name: '2 location 2' })).toBeInTheDocument();
 
-it('should show code tabs when any secondary location is selected', async () => {
-  const user = userEvent.setup();
-  renderIssueApp();
+    // Select the "why is this an issue" tab
+    await user.click(
+      screen.getByRole('tab', { name: 'coding_rules.description_section.title.root_cause' })
+    );
+    expect(
+      screen.queryByRole('tab', {
+        name: `issue.tabs.${TabKeys.Code}`,
+        selected: true,
+      })
+    ).not.toBeInTheDocument();
 
-  await user.click(await screen.findByRole('region', { name: 'Fix this' }));
-  expect(screen.getByRole('button', { name: '1 location 1' })).toBeInTheDocument();
-  expect(screen.getByRole('button', { name: '2 location 2' })).toBeInTheDocument();
+    await user.click(screen.getByRole('button', { name: '1 location 1' }));
+    expect(
+      screen.getByRole('tab', {
+        name: `issue.tabs.${TabKeys.Code}`,
+        selected: true,
+      })
+    ).toBeInTheDocument();
 
-  // Select the "why is this an issue" tab
-  await user.click(
-    screen.getByRole('tab', { name: 'coding_rules.description_section.title.root_cause' })
-  );
-  expect(
-    screen.queryByRole('tab', {
-      name: `issue.tabs.${TabKeys.Code}`,
-      selected: true,
-    })
-  ).not.toBeInTheDocument();
-
-  await user.click(screen.getByRole('button', { name: '1 location 1' }));
-  expect(
-    screen.getByRole('tab', {
-      name: `issue.tabs.${TabKeys.Code}`,
-      selected: true,
-    })
-  ).toBeInTheDocument();
-
-  // selecting the same selected hotspot location should also navigate back to code page
-  await user.click(
-    screen.getByRole('tab', { name: 'coding_rules.description_section.title.root_cause' })
-  );
-  expect(
-    screen.queryByRole('tab', {
-      name: `issue.tabs.${TabKeys.Code}`,
-      selected: true,
-    })
-  ).not.toBeInTheDocument();
-
-  await user.click(screen.getByRole('button', { name: '1 location 1' }));
-  expect(
-    screen.getByRole('tab', {
-      name: `issue.tabs.${TabKeys.Code}`,
-      selected: true,
-    })
-  ).toBeInTheDocument();
-});
+    // Select the same selected hotspot location should also navigate back to code page
+    await user.click(
+      screen.getByRole('tab', { name: 'coding_rules.description_section.title.root_cause' })
+    );
+    expect(
+      screen.queryByRole('tab', {
+        name: `issue.tabs.${TabKeys.Code}`,
+        selected: true,
+      })
+    ).not.toBeInTheDocument();
 
-it('should show issue tags if applicable', async () => {
-  const user = userEvent.setup();
-  issuesHandler.setIsAdmin(true);
-  renderIssueApp();
+    await user.click(screen.getByRole('button', { name: '1 location 1' }));
+    expect(
+      screen.getByRole('tab', {
+        name: `issue.tabs.${TabKeys.Code}`,
+        selected: true,
+      })
+    ).toBeInTheDocument();
+  });
 
-  // Select an issue with an advanced rule
-  await user.click(await screen.findByRole('region', { name: 'Issue with tags' }));
+  it('should show issue tags if applicable', async () => {
+    const user = userEvent.setup();
+    issuesHandler.setIsAdmin(true);
+    renderIssueApp();
 
-  expect(
-    screen.getByRole('heading', {
-      name: 'Issue with tags sonar-lint-icon issue.resolution.badge.DEPRECATED',
-    })
-  ).toBeInTheDocument();
+    // Select an issue with an advanced rule
+    await user.click(await ui.issueItem7.find());
+
+    expect(
+      screen.getByRole('heading', {
+        name: 'Issue with tags sonar-lint-icon issue.resolution.badge.DEPRECATED',
+      })
+    ).toBeInTheDocument();
+  });
 });
 
 describe('redirects', () => {
@@ -636,13 +753,13 @@ describe('redirects', () => {
     expect(screen.getByText('/security_hotspots?assignedToMe=false')).toBeInTheDocument();
   });
 
-  it('should filter out hotspots', async () => {
+  it('should filter out hotspots', () => {
     renderProjectIssuesApp(
       `project/issues?types=${IssueType.SecurityHotspot},${IssueType.CodeSmell}`
     );
 
     expect(
-      await screen.findByRole('checkbox', { name: `issue.type.${IssueType.CodeSmell}` })
+      screen.getByRole('checkbox', { name: `issue.type.${IssueType.CodeSmell}` })
     ).toBeInTheDocument();
   });
 });
index 220f5f173d95036db9806e779003ad79dbce9c18..1a31ccfb7fffd2cb93f0e0112f042d45b2c87ec9 100644 (file)
@@ -37,6 +37,7 @@ import IssueTypeIcon from '../../../components/icons/IssueTypeIcon';
 import SeverityHelper from '../../../components/shared/SeverityHelper';
 import { Alert } from '../../../components/ui/Alert';
 import DeferredSpinner from '../../../components/ui/DeferredSpinner';
+import { SEVERITIES } from '../../../helpers/constants';
 import { throwGlobalError } from '../../../helpers/error';
 import { translate, translateWithParameters } from '../../../helpers/l10n';
 import { Component, Dict, Issue, IssueType, Paging } from '../../../types/types';
@@ -370,8 +371,7 @@ export default class BulkChangeModal extends React.PureComponent<Props, State> {
       return null;
     }
 
-    const severities = ['BLOCKER', 'CRITICAL', 'MAJOR', 'MINOR', 'INFO'];
-    const options: LabelValueSelectOption[] = severities.map((severity) => ({
+    const options: LabelValueSelectOption[] = SEVERITIES.map((severity) => ({
       label: translate('severity', severity),
       value: severity,
     }));
index b9429272f573eb452b4d0c0c6eb50b0b63bb0457..99904430b65944d1458f0c3750b1caf666b3effc 100644 (file)
@@ -66,6 +66,7 @@ import { serializeDate } from '../../../helpers/query';
 import { BranchLike } from '../../../types/branch-like';
 import { ComponentQualifier, isPortfolioLike } from '../../../types/component';
 import {
+  ASSIGNEE_ME,
   Facet,
   FetchIssuesPromise,
   ReferencedComponent,
@@ -469,7 +470,7 @@ export class App extends React.PureComponent<Props, State> {
     }
 
     if (myIssues) {
-      Object.assign(parameters, { assignees: '__me__' });
+      Object.assign(parameters, { assignees: ASSIGNEE_ME });
     }
 
     return this.fetchIssuesHelper(parameters);
@@ -688,7 +689,7 @@ export class App extends React.PureComponent<Props, State> {
     };
 
     if (myIssues) {
-      Object.assign(parameters, { assignees: '__me__' });
+      Object.assign(parameters, { assignees: ASSIGNEE_ME });
     }
 
     return this.fetchIssuesHelper(parameters).then(({ facets }) => parseFacets(facets)[property]);
index af11d1b37f6b9ea726d790707be70a24792d14c8..48474e56cf37ee27e72a5600762e9cc6622225fc 100644 (file)
  * 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 { screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
 import * as React from 'react';
-import { GroupBase } from 'react-select';
-import { AsyncProps } from 'react-select/async';
-import { SearchSelect } from '../../../../components/controls/Select';
-import Avatar from '../../../../components/ui/Avatar';
+import { act } from 'react-dom/test-utils';
+import { byRole } from 'testing-library-selector';
+import { mockUserBase } from '../../../../helpers/mocks/users';
 import { mockCurrentUser, mockIssue, mockLoggedInUser } from '../../../../helpers/testMocks';
-import { searchAssignees } from '../../utils';
-import AssigneeSelect, {
-  AssigneeOption,
-  AssigneeSelectProps,
-  MIN_QUERY_LENGTH,
-} from '../AssigneeSelect';
+import { renderComponent } from '../../../../helpers/testReactTestingUtils';
+import AssigneeSelect, { AssigneeSelectProps, MIN_QUERY_LENGTH } from '../AssigneeSelect';
 
 jest.mock('../../utils', () => ({
   searchAssignees: jest.fn().mockResolvedValue({
     results: [
-      {
+      mockUserBase({
         active: true,
-        avatar: '##avatar1',
+        avatar: 'avatar1',
         login: 'toto@toto',
         name: 'toto',
-      },
-      {
+      }),
+      mockUserBase({
         active: false,
-        avatar: '##avatar2',
+        avatar: 'avatar2',
         login: 'tata@tata',
         name: 'tata',
-      },
-      {
+      }),
+      mockUserBase({
         active: true,
-        avatar: '##avatar3',
+        avatar: 'avatar3',
         login: 'titi@titi',
-      },
+      }),
     ],
   }),
 }));
 
-it('should render correctly', () => {
-  expect(shallowRender()).toMatchSnapshot('default');
-  expect(shallowRender({ currentUser: mockLoggedInUser(), issues: [mockIssue()] })).toMatchSnapshot(
-    'logged in & assignable issues'
-  );
-  expect(shallowRender({ currentUser: mockLoggedInUser() })).toMatchSnapshot(
-    'logged in & no assignable issues'
-  );
-  expect(shallowRender({ issues: [mockIssue(false, { assignee: 'someone' })] })).toMatchSnapshot(
-    'unassignable issues'
-  );
+const ui = {
+  combobox: byRole('combobox'),
+};
+
+it('should show correct suggestions when there is assignable issue for the current user', async () => {
+  const user = userEvent.setup();
+  renderAssigneeSelect({
+    currentUser: mockLoggedInUser({ name: 'Skywalker' }),
+    issues: [mockIssue(false, { assignee: 'someone' })],
+  });
+
+  await user.click(ui.combobox.get());
+  expect(await screen.findByText('Skywalker')).toBeInTheDocument();
+});
+
+it('should show correct suggestions when all issues are already assigned to current user', async () => {
+  const user = userEvent.setup();
+  renderAssigneeSelect({
+    currentUser: mockLoggedInUser({ login: 'luke', name: 'Skywalker' }),
+    issues: [mockIssue(false, { assignee: 'luke' })],
+  });
+
+  await user.click(ui.combobox.get());
+  expect(screen.queryByText('Skywalker')).not.toBeInTheDocument();
 });
 
-it('should render options correctly', () => {
-  const wrapper = shallowRender();
-
-  expect(
-    shallow(
-      wrapper.instance().renderAssignee({
-        avatar: '##avatar1',
-        value: 'toto@toto',
-        label: 'toto',
-      })
-    )
-      .find(Avatar)
-      .exists()
-  ).toBe(true);
-
-  expect(
-    shallow(
-      wrapper.instance().renderAssignee({
-        value: 'toto@toto',
-        label: 'toto',
-      })
-    )
-      .find(Avatar)
-      .exists()
-  ).toBe(false);
+it('should show correct suggestions when there is no assigneable issue', async () => {
+  const user = userEvent.setup();
+  renderAssigneeSelect({
+    currentUser: mockLoggedInUser({ name: 'Skywalker' }),
+  });
+
+  await user.click(ui.combobox.get());
+  expect(screen.queryByText('Skywalker')).not.toBeInTheDocument();
 });
 
-it('should render noOptionsMessage correctly', () => {
-  const wrapper = shallowRender();
-  expect(
-    wrapper.find<AsyncProps<AssigneeOption, false, GroupBase<AssigneeOption>>>(SearchSelect).props()
-      .noOptionsMessage!({ inputValue: 'a' })
-  ).toBe(`select2.tooShort.${MIN_QUERY_LENGTH}`);
-
-  expect(
-    wrapper.find<AsyncProps<AssigneeOption, false, GroupBase<AssigneeOption>>>(SearchSelect).props()
-      .noOptionsMessage!({ inputValue: 'droids' })
-  ).toBe('select2.noMatches');
+it('should handle assignee search correctly', async () => {
+  const user = userEvent.setup();
+  renderAssigneeSelect();
+
+  // Minimum MIN_QUERY_LENGTH charachters to trigger search
+  await user.type(ui.combobox.get(), 'a');
+  expect(await screen.findByText(`select2.tooShort.${MIN_QUERY_LENGTH}`)).toBeInTheDocument();
+
+  // Trigger search
+  await user.type(ui.combobox.get(), 'someone');
+  expect(await screen.findByText('toto')).toBeInTheDocument();
+  expect(await screen.findByText('user.x_deleted.tata')).toBeInTheDocument();
+  expect(await screen.findByText('user.x_deleted.titi@titi')).toBeInTheDocument();
 });
 
-it('should handle assignee search', async () => {
+it('should handle assignee selection', async () => {
   const onAssigneeSelect = jest.fn();
-  const wrapper = shallowRender({ onAssigneeSelect });
+  const user = userEvent.setup();
+  renderAssigneeSelect({ onAssigneeSelect });
 
-  wrapper.instance().handleAssigneeSearch('a', jest.fn());
-  expect(searchAssignees).not.toHaveBeenCalled();
-
-  const result = await new Promise((resolve: (opts: AssigneeOption[]) => void) => {
-    wrapper.instance().handleAssigneeSearch('someone', resolve);
+  await act(async () => {
+    await user.type(ui.combobox.get(), 'tot');
   });
 
-  expect(result).toEqual([
-    {
-      avatar: '##avatar1',
-      value: 'toto@toto',
-      label: 'toto',
-    },
-    {
-      avatar: '##avatar2',
-      value: 'tata@tata',
-      label: 'user.x_deleted.tata',
-    },
-    {
-      avatar: '##avatar3',
-      value: 'titi@titi',
-      label: 'user.x_deleted.titi@titi',
-    },
-  ]);
+  // Do not select assignee until suggestion is selected
+  expect(onAssigneeSelect).not.toHaveBeenCalled();
+
+  // Select assignee when suggestion is selected
+  await user.click(screen.getByText('toto'));
+  expect(onAssigneeSelect).toHaveBeenCalledTimes(1);
 });
 
-function shallowRender(overrides: Partial<AssigneeSelectProps> = {}) {
-  return shallow<AssigneeSelect>(
+function renderAssigneeSelect(overrides: Partial<AssigneeSelectProps> = {}) {
+  return renderComponent(
     <AssigneeSelect
       inputId="id"
       currentUser={mockCurrentUser()}
diff --git a/server/sonar-web/src/main/js/apps/issues/components/__tests__/BulkChangeModal-it.tsx b/server/sonar-web/src/main/js/apps/issues/components/__tests__/BulkChangeModal-it.tsx
new file mode 100644 (file)
index 0000000..bb0566c
--- /dev/null
@@ -0,0 +1,203 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import * as React from 'react';
+import selectEvent from 'react-select-event';
+import { bulkChangeIssues } from '../../../../api/issues';
+import { SEVERITIES } from '../../../../helpers/constants';
+import { mockIssue, mockLoggedInUser } from '../../../../helpers/testMocks';
+import { renderComponent } from '../../../../helpers/testReactTestingUtils';
+import { IssueType } from '../../../../types/issues';
+import { Issue } from '../../../../types/types';
+import BulkChangeModal, { MAX_PAGE_SIZE } from '../BulkChangeModal';
+
+jest.mock('../../../../api/issues', () => ({
+  searchIssueTags: jest.fn().mockResolvedValue(['tag1', 'tag2']),
+  bulkChangeIssues: jest.fn().mockResolvedValue({}),
+}));
+
+afterEach(() => {
+  jest.clearAllMocks();
+});
+
+it('should display error message when no issues available', async () => {
+  renderBulkChangeModal([]);
+
+  expect(await screen.findByText('issue_bulk_change.no_match')).toBeInTheDocument();
+});
+
+it('should display warning when too many issues are passed', async () => {
+  const issues: Issue[] = [];
+  for (let i = MAX_PAGE_SIZE + 1; i > 0; i--) {
+    issues.push(mockIssue());
+  }
+  renderBulkChangeModal(issues);
+
+  expect(
+    await screen.findByText(`issue_bulk_change.form.title.${MAX_PAGE_SIZE}`)
+  ).toBeInTheDocument();
+  expect(await screen.findByText('issue_bulk_change.max_issues_reached')).toBeInTheDocument();
+});
+
+it.each([
+  ['type', 'set_type'],
+  ['severity', 'set_severity'],
+])('should render select for %s', async (_field, action) => {
+  renderBulkChangeModal([mockIssue(false, { actions: [action] })]);
+
+  expect(await screen.findByText('issue.' + action)).toBeInTheDocument();
+});
+
+it('should render tags correctly', async () => {
+  renderBulkChangeModal([mockIssue(false, { actions: ['set_tags'] })]);
+
+  expect(await screen.findByRole('combobox', { name: 'issue.add_tags' })).toBeInTheDocument();
+  expect(await screen.findByRole('combobox', { name: 'issue.remove_tags' })).toBeInTheDocument();
+});
+
+it('should render transitions correctly', async () => {
+  renderBulkChangeModal([
+    mockIssue(false, { actions: ['set_transition'], transitions: ['Transition1'] }),
+  ]);
+
+  expect(await screen.findByText('issue.transition')).toBeInTheDocument();
+  expect(await screen.findByText('issue.transition.Transition1')).toBeInTheDocument();
+});
+
+it('should disable the submit button unless some change is configured', async () => {
+  const user = userEvent.setup();
+  renderBulkChangeModal([mockIssue(false, { actions: ['set_severity', 'comment'] })]);
+
+  // Apply button should be disabled
+  expect(await screen.findByRole('button', { name: 'apply' })).toBeDisabled();
+
+  // Adding a comment should not enable the submit button
+  await user.type(screen.getByRole('textbox', { name: 'issue.comment.formlink' }), 'some comment');
+  expect(screen.getByRole('button', { name: 'apply' })).toBeDisabled();
+
+  // Select a severity
+  await selectEvent.select(screen.getByRole('combobox', { name: 'issue.set_severity' }), [
+    `severity.${SEVERITIES[0]}`,
+  ]);
+
+  // Apply button should be enabled now
+  expect(screen.getByRole('button', { name: 'apply' })).toBeEnabled();
+});
+
+it('should properly submit', async () => {
+  const onDone = jest.fn();
+  const user = userEvent.setup();
+  renderBulkChangeModal(
+    [
+      mockIssue(false, {
+        key: 'issue1',
+        actions: ['assign', 'set_transition', 'set_tags', 'set_type', 'set_severity', 'comment'],
+        transitions: ['Transition1', 'Transition2'],
+      }),
+      mockIssue(false, {
+        key: 'issue2',
+        actions: ['assign', 'set_transition', 'set_tags', 'set_type', 'set_severity', 'comment'],
+        transitions: ['Transition1', 'Transition2'],
+      }),
+    ],
+    {
+      onDone,
+      currentUser: mockLoggedInUser({
+        login: 'toto',
+        name: 'Toto',
+      }),
+    }
+  );
+
+  expect(bulkChangeIssues).toHaveBeenCalledTimes(0);
+  expect(onDone).toHaveBeenCalledTimes(0);
+
+  // Assign
+  await user.click(await screen.findByRole('combobox', { name: 'issue.assign.formlink' }));
+  await user.click(await screen.findByText('Toto'));
+
+  // Transition
+  await user.click(await screen.findByText('issue.transition.Transition2'));
+
+  // Add a tag
+  await selectEvent.select(screen.getByRole('combobox', { name: 'issue.add_tags' }), [
+    'tag1',
+    'tag2',
+  ]);
+
+  // Select a type
+  await selectEvent.select(screen.getByRole('combobox', { name: 'issue.set_type' }), [
+    `issue.type.CODE_SMELL`,
+  ]);
+
+  // Select a severity
+  await selectEvent.select(screen.getByRole('combobox', { name: 'issue.set_severity' }), [
+    `severity.${SEVERITIES[0]}`,
+  ]);
+
+  // Severity
+  await selectEvent.select(screen.getByRole('combobox', { name: 'issue.set_severity' }), [
+    `severity.${SEVERITIES[0]}`,
+  ]);
+
+  // Comment
+  await user.type(screen.getByRole('textbox', { name: 'issue.comment.formlink' }), 'some comment');
+
+  // Send notification
+  await user.click(screen.getByRole('checkbox', { name: 'issue.send_notifications' }));
+
+  // Submit
+  await user.click(screen.getByRole('button', { name: 'apply' }));
+
+  expect(bulkChangeIssues).toHaveBeenCalledTimes(1);
+  expect(onDone).toHaveBeenCalledTimes(1);
+  expect(bulkChangeIssues).toHaveBeenCalledWith(['issue1', 'issue2'], {
+    assign: 'toto',
+    comment: 'some comment',
+    set_severity: 'BLOCKER',
+    add_tags: 'tag1,tag2',
+    do_transition: 'Transition2',
+    set_type: IssueType.CodeSmell,
+    sendNotifications: true,
+  });
+});
+
+function renderBulkChangeModal(issues: Issue[], props: Partial<BulkChangeModal['props']> = {}) {
+  return renderComponent(
+    <BulkChangeModal
+      component={undefined}
+      currentUser={{ isLoggedIn: true, dismissedNotices: {} }}
+      fetchIssues={() =>
+        Promise.resolve({
+          issues,
+          paging: {
+            pageIndex: issues.length,
+            pageSize: issues.length,
+            total: issues.length,
+          },
+        })
+      }
+      onClose={() => {}}
+      onDone={() => {}}
+      {...props}
+    />
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/issues/components/__tests__/BulkChangeModal-test.tsx b/server/sonar-web/src/main/js/apps/issues/components/__tests__/BulkChangeModal-test.tsx
deleted file mode 100644 (file)
index f71b112..0000000
+++ /dev/null
@@ -1,125 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * 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 { Props as ReactSelectProps } from 'react-select';
-import { searchIssueTags } from '../../../../api/issues';
-import { SubmitButton } from '../../../../components/controls/buttons';
-import Select, { CreatableSelect, SearchSelect } from '../../../../components/controls/Select';
-import { mockIssue } from '../../../../helpers/testMocks';
-import { change, waitAndUpdate } from '../../../../helpers/testUtils';
-import { Issue } from '../../../../types/types';
-import BulkChangeModal, { MAX_PAGE_SIZE } from '../BulkChangeModal';
-
-jest.mock('../../../../api/issues', () => ({
-  searchIssueTags: jest.fn().mockResolvedValue([undefined, []]),
-}));
-
-it('should display error message when no issues available', async () => {
-  const wrapper = getWrapper([]);
-  await waitAndUpdate(wrapper);
-  expect(wrapper).toMatchSnapshot();
-});
-
-it('should display form when issues are present', async () => {
-  const wrapper = getWrapper([mockIssue()]);
-  await waitAndUpdate(wrapper);
-  expect(wrapper).toMatchSnapshot();
-});
-
-it('should display warning when too many issues are passed', async () => {
-  const issues: Issue[] = [];
-  for (let i = MAX_PAGE_SIZE + 1; i > 0; i--) {
-    issues.push(mockIssue());
-  }
-
-  const wrapper = getWrapper(issues);
-  await waitAndUpdate(wrapper);
-  expect(wrapper.find('h2')).toMatchSnapshot();
-  expect(wrapper.find('Alert')).toMatchSnapshot();
-});
-
-it('should properly handle the search for tags', async () => {
-  const wrapper = getWrapper([]);
-  await new Promise((resolve) => {
-    wrapper.instance().handleTagsSearch('query', resolve);
-  });
-  expect(searchIssueTags).toHaveBeenCalled();
-});
-
-it.each([
-  ['type', 'set_type'],
-  ['severity', 'set_severity'],
-])('should render select for %s', async (_field, action) => {
-  const wrapper = getWrapper([mockIssue(false, { actions: [action] })]);
-  await waitAndUpdate(wrapper);
-
-  const { Option, SingleValue } = wrapper.find(Select).props().components;
-
-  expect(Option({ data: { label: 'label', value: 'value' } })).toMatchSnapshot('Option');
-  expect(SingleValue({ data: { label: 'label', value: 'value' } })).toMatchSnapshot('SingleValue');
-});
-
-it('should render tags correctly', async () => {
-  const wrapper = getWrapper([mockIssue(false, { actions: ['set_tags'] })]);
-  await waitAndUpdate(wrapper);
-
-  expect(wrapper.find(CreatableSelect).exists()).toBe(true);
-  expect(wrapper.find(SearchSelect).exists()).toBe(true);
-});
-
-it('should disable the submit button unless some change is configured', async () => {
-  const wrapper = getWrapper([mockIssue(false, { actions: ['set_severity', 'comment'] })]);
-  await waitAndUpdate(wrapper);
-
-  return new Promise<void>((resolve) => {
-    expect(wrapper.find(SubmitButton).props().disabled).toBe(true);
-
-    // Setting a comment is not sufficient; some other change must occur.
-    change(wrapper.find('#comment'), 'Some comment');
-    expect(wrapper.find(SubmitButton).props().disabled).toBe(true);
-
-    wrapper.find<ReactSelectProps>(Select).at(0).simulate('change', { value: 'foo' });
-
-    expect(wrapper.find(SubmitButton).props().disabled).toBe(false);
-    resolve();
-  });
-});
-
-const getWrapper = (issues: Issue[]) => {
-  return shallow<BulkChangeModal>(
-    <BulkChangeModal
-      component={undefined}
-      currentUser={{ isLoggedIn: true, dismissedNotices: {} }}
-      fetchIssues={() =>
-        Promise.resolve({
-          issues,
-          paging: {
-            pageIndex: issues.length,
-            pageSize: issues.length,
-            total: issues.length,
-          },
-        })
-      }
-      onClose={() => {}}
-      onDone={() => {}}
-    />
-  );
-};
index 7fa48437c22ed63d93b4cf3bebc9fdc05a0fde3d..fb6b445d99e510abb00c8417635ced71ccab6b2b 100644 (file)
  * 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 { screen } from '@testing-library/react';
 import * as React from 'react';
 import { mockComponent } from '../../../../helpers/mocks/component';
 import { mockIssue } from '../../../../helpers/testMocks';
+import { renderComponent } from '../../../../helpers/testReactTestingUtils';
 import { ComponentQualifier } from '../../../../types/component';
+import { Component, Issue } from '../../../../types/types';
 import ComponentBreadcrumbs from '../ComponentBreadcrumbs';
 
 const baseIssue = mockIssue(false, {
@@ -33,27 +35,32 @@ const baseIssue = mockIssue(false, {
   branch: 'test-branch',
 });
 
-it('renders', () => {
-  expect(
-    shallow(<ComponentBreadcrumbs component={undefined} issue={baseIssue} />)
-  ).toMatchSnapshot();
-});
+describe('renders properly', () => {
+  it('without component with issue', () => {
+    renderComponentBreadcrumbs(mockComponent());
+
+    expect(screen.getByLabelText('issues.on_file_x.comp-name')).toBeInTheDocument();
+  });
+
+  it('with component without issue branch', () => {
+    renderComponentBreadcrumbs(mockComponent({ qualifier: ComponentQualifier.Portfolio }), {
+      branch: undefined,
+    });
 
-it('renders issues properly for views', () => {
-  expect(
-    shallow(
-      <ComponentBreadcrumbs
-        component={mockComponent({ qualifier: ComponentQualifier.Portfolio })}
-        issue={{ ...baseIssue, branch: undefined }}
-      />
-    )
-  ).toMatchSnapshot();
-  expect(
-    shallow(
-      <ComponentBreadcrumbs
-        component={mockComponent({ qualifier: ComponentQualifier.Portfolio })}
-        issue={{ ...baseIssue }}
-      />
-    )
-  ).toMatchSnapshot('with branch information');
+    expect(screen.getByLabelText('issues.on_file_x.proj-name, comp-name')).toBeInTheDocument();
+    expect(screen.queryByText('test-branch')).not.toBeInTheDocument();
+  });
+
+  it('with component and issue branch', () => {
+    renderComponentBreadcrumbs(mockComponent({ qualifier: ComponentQualifier.Portfolio }));
+
+    expect(screen.getByLabelText('issues.on_file_x.proj-name, comp-name')).toBeInTheDocument();
+    expect(screen.getByText('test-branch')).toBeInTheDocument();
+  });
 });
+
+function renderComponentBreadcrumbs(component?: Component, issue: Partial<Issue> = {}) {
+  return renderComponent(
+    <ComponentBreadcrumbs component={component} issue={{ ...baseIssue, ...issue }} />
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/issues/components/__tests__/IssuesApp-test.tsx b/server/sonar-web/src/main/js/apps/issues/components/__tests__/IssuesApp-test.tsx
deleted file mode 100644 (file)
index ea2b2f6..0000000
+++ /dev/null
@@ -1,497 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * 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 { searchIssues } from '../../../../api/issues';
-import { getRuleDetails } from '../../../../api/rules';
-import handleRequiredAuthentication from '../../../../helpers/handleRequiredAuthentication';
-import { KeyboardKeys } from '../../../../helpers/keycodes';
-import { mockPullRequest } from '../../../../helpers/mocks/branch-like';
-import { mockComponent } from '../../../../helpers/mocks/component';
-import {
-  addSideBarClass,
-  addWhitePageClass,
-  removeSideBarClass,
-  removeWhitePageClass,
-} from '../../../../helpers/pages';
-import {
-  mockCurrentUser,
-  mockIssue,
-  mockLocation,
-  mockLoggedInUser,
-  mockRawIssue,
-  mockRouter,
-} from '../../../../helpers/testMocks';
-import { keydown, mockEvent, waitAndUpdate } from '../../../../helpers/testUtils';
-import { ReferencedComponent } from '../../../../types/issues';
-import { Issue, Paging } from '../../../../types/types';
-import {
-  disableLocationsNavigator,
-  enableLocationsNavigator,
-  selectNextFlow,
-  selectNextLocation,
-  selectPreviousFlow,
-  selectPreviousLocation,
-} from '../../actions';
-import BulkChangeModal from '../BulkChangeModal';
-import { App } from '../IssuesApp';
-
-jest.mock('../../../../helpers/pages', () => ({
-  addSideBarClass: jest.fn(),
-  addWhitePageClass: jest.fn(),
-  removeSideBarClass: jest.fn(),
-  removeWhitePageClass: jest.fn(),
-}));
-
-jest.mock('../../../../helpers/handleRequiredAuthentication', () => jest.fn());
-
-jest.mock('../../../../api/issues', () => ({
-  searchIssues: jest.fn().mockResolvedValue({ facets: [], issues: [] }),
-}));
-
-jest.mock('../../../../api/rules', () => ({
-  getRuleDetails: jest.fn(),
-}));
-
-jest.mock('../../../../api/users', () => ({
-  getCurrentUser: jest.fn().mockResolvedValue({
-    dismissedNotices: {
-      something: false,
-    },
-  }),
-  dismissNotification: jest.fn(),
-}));
-
-const RAW_ISSUES = [
-  mockRawIssue(false, { key: 'foo' }),
-  mockRawIssue(false, { key: 'bar' }),
-  mockRawIssue(true, { key: 'third' }),
-  mockRawIssue(false, { key: 'fourth' }),
-];
-const ISSUES = [
-  mockIssue(false, { key: 'foo' }),
-  mockIssue(false, { key: 'bar' }),
-  mockIssue(true, { key: 'third' }),
-  mockIssue(false, { key: 'fourth' }),
-];
-const FACETS = [{ property: 'severities', values: [{ val: 'MINOR', count: 4 }] }];
-const PAGING = { pageIndex: 1, pageSize: 100, total: 4 };
-
-const referencedComponent: ReferencedComponent = { key: 'foo-key', name: 'bar', uuid: 'foo-uuid' };
-
-beforeEach(() => {
-  (searchIssues as jest.Mock).mockResolvedValue({
-    components: [referencedComponent],
-    effortTotal: 1,
-    facets: FACETS,
-    issues: RAW_ISSUES,
-    languages: [],
-    paging: PAGING,
-    rules: [],
-    users: [],
-  });
-
-  (getRuleDetails as jest.Mock).mockResolvedValue({ rule: { test: 'test' } });
-});
-
-afterEach(() => {
-  (searchIssues as jest.Mock).mockReset();
-});
-
-it('should render a list of issue', async () => {
-  const wrapper = shallowRender();
-  await waitAndUpdate(wrapper);
-  expect(wrapper.state().issues.length).toBe(4);
-  expect(wrapper.state().referencedComponentsById).toEqual({ 'foo-uuid': referencedComponent });
-  expect(wrapper.state().referencedComponentsByKey).toEqual({ 'foo-key': referencedComponent });
-
-  expect(addSideBarClass).toHaveBeenCalled();
-  expect(addWhitePageClass).toHaveBeenCalled();
-});
-
-it('should handle my issue change properly', () => {
-  const push = jest.fn();
-  const wrapper = shallowRender({ router: mockRouter({ push }) });
-  wrapper.instance().handleMyIssuesChange(true);
-
-  expect(push).toHaveBeenCalledWith({
-    pathname: '/issues',
-    query: {
-      id: 'foo',
-      author: [],
-      myIssues: 'true',
-    },
-  });
-});
-
-it('should load search result count correcly', async () => {
-  const wrapper = shallowRender();
-  const count = await wrapper.instance().loadSearchResultCount('severities', {});
-  expect(count).toStrictEqual({ MINOR: 4 });
-});
-
-it('should not render for anonymous user', () => {
-  shallowRender({
-    currentUser: mockCurrentUser({ isLoggedIn: false }),
-    location: mockLocation({ query: { myIssues: true.toString() } }),
-  });
-  expect(handleRequiredAuthentication).toHaveBeenCalled();
-});
-
-it('should handle reset properly', () => {
-  const push = jest.fn();
-  const wrapper = shallowRender({ router: mockRouter({ push }) });
-  wrapper.instance().handleReset();
-  expect(push).toHaveBeenCalledWith({
-    pathname: '/issues',
-    query: {
-      id: 'foo',
-      myIssues: undefined,
-      resolved: 'false',
-    },
-  });
-});
-
-it('should open standard facets for vulnerabilities and hotspots', () => {
-  const wrapper = shallowRender({
-    location: mockLocation({ pathname: '/issues', query: { types: 'VULNERABILITY' } }),
-  });
-  const instance = wrapper.instance();
-  const fetchFacet = jest.spyOn(instance, 'fetchFacet');
-
-  expect(wrapper.state('openFacets').standards).toEqual(true);
-  expect(wrapper.state('openFacets').sonarsourceSecurity).toEqual(true);
-
-  instance.handleFacetToggle('standards');
-  expect(wrapper.state('openFacets').standards).toEqual(false);
-  expect(fetchFacet).not.toHaveBeenCalled();
-
-  instance.handleFacetToggle('standards');
-  expect(wrapper.state('openFacets').standards).toEqual(true);
-  expect(wrapper.state('openFacets').sonarsourceSecurity).toEqual(true);
-  expect(fetchFacet).toHaveBeenLastCalledWith('sonarsourceSecurity');
-
-  instance.handleFacetToggle('owaspTop10');
-  expect(wrapper.state('openFacets').owaspTop10).toEqual(true);
-  expect(fetchFacet).toHaveBeenLastCalledWith('owaspTop10');
-});
-
-it('should correctly bind key events for issue navigation', async () => {
-  const push = jest.fn();
-  const addEventListenerSpy = jest.spyOn(document, 'addEventListener');
-  const wrapper = shallowRender({ router: mockRouter({ push }) });
-  await waitAndUpdate(wrapper);
-
-  expect(addEventListenerSpy).toHaveBeenCalledTimes(2);
-
-  expect(wrapper.state('selected')).toBe(ISSUES[0].key);
-
-  keydown({ key: KeyboardKeys.DownArrow });
-  expect(wrapper.state('selected')).toBe(ISSUES[1].key);
-
-  keydown({ key: KeyboardKeys.UpArrow });
-  keydown({ key: KeyboardKeys.UpArrow });
-  expect(wrapper.state('selected')).toBe(ISSUES[0].key);
-
-  keydown({ key: KeyboardKeys.DownArrow });
-  keydown({ key: KeyboardKeys.DownArrow });
-  keydown({ key: KeyboardKeys.DownArrow });
-  keydown({ key: KeyboardKeys.DownArrow });
-  keydown({ key: KeyboardKeys.DownArrow });
-  keydown({ key: KeyboardKeys.DownArrow });
-  expect(wrapper.state('selected')).toBe(ISSUES[3].key);
-
-  keydown({ key: KeyboardKeys.RightArrow, ctrlKey: true });
-  expect(push).not.toHaveBeenCalled();
-  keydown({ key: KeyboardKeys.RightArrow });
-  expect(push).toHaveBeenCalledTimes(1);
-
-  keydown({ key: KeyboardKeys.LeftArrow });
-  expect(push).toHaveBeenCalledTimes(2);
-
-  addEventListenerSpy.mockReset();
-});
-
-it('should correctly clean up on unmount', () => {
-  const removeEventListenerSpy = jest.spyOn(document, 'removeEventListener');
-  const wrapper = shallowRender();
-
-  wrapper.unmount();
-  expect(removeSideBarClass).toHaveBeenCalled();
-  expect(removeWhitePageClass).toHaveBeenCalled();
-  expect(removeEventListenerSpy).toHaveBeenCalledTimes(2);
-
-  removeEventListenerSpy.mockReset();
-});
-
-it('should be able to bulk change specific issues', async () => {
-  const wrapper = shallowRender({ currentUser: mockLoggedInUser() });
-  await waitAndUpdate(wrapper);
-
-  const instance = wrapper.instance();
-  expect(wrapper.state().checked.length).toBe(0);
-  instance.handleIssueCheck('foo');
-  instance.handleIssueCheck('bar');
-  expect(wrapper.state().checked.length).toBe(2);
-
-  instance.handleOpenBulkChange();
-  wrapper.update();
-  expect(wrapper.find(BulkChangeModal).exists()).toBe(true);
-  const { issues } = await wrapper.find(BulkChangeModal).props().fetchIssues({});
-  expect(issues).toHaveLength(2);
-});
-
-it('should display the right facets open', () => {
-  expect(
-    shallowRender({
-      location: mockLocation({ query: { types: 'BUGS' } }),
-    }).state('openFacets')
-  ).toEqual({
-    owaspTop10: false,
-    'owaspTop10-2021': false,
-    sansTop25: false,
-    severities: true,
-    standards: false,
-    sonarsourceSecurity: false,
-    types: true,
-  });
-  expect(
-    shallowRender({
-      location: mockLocation({ query: { owaspTop10: 'a1' } }),
-    }).state('openFacets')
-  ).toEqual({
-    owaspTop10: true,
-    'owaspTop10-2021': false,
-    sansTop25: false,
-    severities: true,
-    standards: true,
-    sonarsourceSecurity: false,
-    types: true,
-  });
-});
-
-it('should correctly handle filter changes', () => {
-  const push = jest.fn();
-  const instance = shallowRender({ router: mockRouter({ push }) }).instance();
-  instance.setState({ openFacets: { types: true } });
-  instance.handleFilterChange({ types: ['VULNERABILITY'] });
-  expect(instance.state.openFacets).toEqual({
-    types: true,
-    sonarsourceSecurity: true,
-    standards: true,
-  });
-  expect(push).toHaveBeenCalled();
-  instance.handleFilterChange({ types: ['BUGS'] });
-  expect(instance.state.openFacets).toEqual({
-    types: true,
-    sonarsourceSecurity: true,
-    standards: true,
-  });
-});
-
-it('should fetch issues until defined', async () => {
-  (searchIssues as jest.Mock).mockImplementation(mockSearchIssuesResponse());
-
-  const mockDone = (_: Issue[], paging: Paging) =>
-    paging.total <= paging.pageIndex * paging.pageSize;
-
-  const wrapper = shallowRender({
-    location: mockLocation({
-      query: { open: '0' },
-    }),
-  });
-  const instance = wrapper.instance();
-  await waitAndUpdate(wrapper);
-
-  const result = await instance.fetchIssuesUntil(1, mockDone);
-  expect(result.issues).toHaveLength(6);
-  expect(result.paging.pageIndex).toBe(3);
-});
-
-describe('keydown event handler', () => {
-  const wrapper = shallowRender();
-  const instance = wrapper.instance();
-  jest.spyOn(instance, 'setState');
-
-  beforeEach(() => {
-    jest.resetAllMocks();
-  });
-
-  it('should handle alt', () => {
-    instance.handleKeyDown(mockEvent({ key: KeyboardKeys.Alt }));
-    expect(instance.setState).toHaveBeenCalledWith(enableLocationsNavigator);
-  });
-  it('should handle alt+↓', () => {
-    instance.handleKeyDown(mockEvent({ altKey: true, key: KeyboardKeys.DownArrow }));
-    expect(instance.setState).toHaveBeenCalledWith(selectNextLocation);
-  });
-  it('should handle alt+↑', () => {
-    instance.handleKeyDown(mockEvent({ altKey: true, key: KeyboardKeys.UpArrow }));
-    expect(instance.setState).toHaveBeenCalledWith(selectPreviousLocation);
-  });
-  it('should handle alt+←', () => {
-    instance.handleKeyDown(mockEvent({ altKey: true, key: KeyboardKeys.LeftArrow }));
-    expect(instance.setState).toHaveBeenCalledWith(selectPreviousFlow);
-  });
-  it('should handle alt+→', () => {
-    instance.handleKeyDown(mockEvent({ altKey: true, key: KeyboardKeys.RightArrow }));
-    expect(instance.setState).toHaveBeenCalledWith(selectNextFlow);
-  });
-  it('should ignore if modal is open', () => {
-    wrapper.setState({ bulkChangeModal: true });
-    instance.handleKeyDown(mockEvent({ key: KeyboardKeys.Alt }));
-    expect(instance.setState).not.toHaveBeenCalled();
-  });
-});
-
-describe('keyup event handler', () => {
-  const wrapper = shallowRender();
-  const instance = wrapper.instance();
-  jest.spyOn(instance, 'setState');
-
-  beforeEach(() => {
-    jest.resetAllMocks();
-  });
-
-  it('should handle alt', () => {
-    instance.handleKeyUp(mockEvent({ key: KeyboardKeys.Alt }));
-    expect(instance.setState).toHaveBeenCalledWith(disableLocationsNavigator);
-  });
-});
-
-it('should fetch more issues', async () => {
-  (searchIssues as jest.Mock).mockImplementation(mockSearchIssuesResponse());
-  const wrapper = shallowRender({});
-  const instance = wrapper.instance();
-  await waitAndUpdate(wrapper);
-
-  await instance.fetchMoreIssues();
-  await waitAndUpdate(wrapper);
-  expect(wrapper.state('issues')).toHaveLength(4);
-});
-
-it('should refresh branch status if issues are updated', async () => {
-  const fetchBranchStatus = jest.fn();
-  const branchLike = mockPullRequest();
-  const component = mockComponent();
-  const wrapper = shallowRender({ branchLike, component, fetchBranchStatus });
-  const instance = wrapper.instance();
-  await waitAndUpdate(wrapper);
-
-  const updatedIssue: Issue = { ...ISSUES[0], type: 'SECURITY_HOTSPOT' };
-  instance.handleIssueChange(updatedIssue);
-  expect(wrapper.state().issues[0].type).toEqual(updatedIssue.type);
-  expect(fetchBranchStatus).toHaveBeenCalledWith(branchLike, component.key);
-
-  fetchBranchStatus.mockClear();
-  instance.handleBulkChangeDone();
-  expect(fetchBranchStatus).toHaveBeenCalled();
-});
-
-it('should update the open issue when it is changed', async () => {
-  (searchIssues as jest.Mock).mockImplementation(mockSearchIssuesResponse());
-
-  const wrapper = shallowRender();
-  await waitAndUpdate(wrapper);
-
-  const issue = wrapper.state().issues[0];
-
-  wrapper.setProps({ location: mockLocation({ query: { open: issue.key } }) });
-  await waitAndUpdate(wrapper);
-
-  expect(wrapper.state().openIssue).toEqual(issue);
-
-  const updatedIssue: Issue = { ...issue, type: 'SECURITY_HOTSPOT' };
-  wrapper.instance().handleIssueChange(updatedIssue);
-
-  await waitAndUpdate(wrapper);
-  expect(wrapper.state().openIssue).toEqual(updatedIssue);
-});
-
-it('should handle createAfter query param with time', async () => {
-  (searchIssues as jest.Mock).mockImplementation(mockSearchIssuesResponse());
-
-  const wrapper = shallowRender({
-    location: mockLocation({ query: { createdAfter: '2020-10-21' } }),
-  });
-  expect(wrapper.instance().createdAfterIncludesTime()).toBe(false);
-  await waitAndUpdate(wrapper);
-
-  wrapper.setProps({ location: mockLocation({ query: { createdAfter: '2020-10-21T17:21:00Z' } }) });
-  expect(wrapper.instance().createdAfterIncludesTime()).toBe(true);
-
-  (searchIssues as jest.Mock).mockClear();
-
-  wrapper.instance().fetchIssues({});
-  expect(searchIssues).toHaveBeenCalledWith(
-    expect.objectContaining({ createdAfter: '2020-10-21T17:21:00+0000' })
-  );
-});
-
-function mockSearchIssuesResponse(keyCount = 0, lineCount = 1) {
-  return ({ p = 1 }) =>
-    Promise.resolve({
-      components: [referencedComponent],
-      effortTotal: 1,
-      facets: FACETS,
-      issues: [
-        mockRawIssue(false, {
-          key: `${keyCount++}`,
-          textRange: {
-            startLine: lineCount++,
-            endLine: lineCount,
-            startOffset: 0,
-            endOffset: 15,
-          },
-        }),
-        mockRawIssue(false, {
-          key: `${keyCount}`,
-          textRange: {
-            startLine: lineCount++,
-            endLine: lineCount,
-            startOffset: 0,
-            endOffset: 15,
-          },
-        }),
-      ],
-      languages: [],
-      paging: { pageIndex: p, pageSize: 2, total: 6 },
-      rules: [],
-      users: [],
-    });
-}
-
-function shallowRender(props: Partial<App['props']> = {}) {
-  return shallow<App>(
-    <App
-      component={{
-        breadcrumbs: [],
-        key: 'foo',
-        name: 'bar',
-        qualifier: 'Doe',
-      }}
-      currentUser={mockLoggedInUser()}
-      fetchBranchStatus={jest.fn()}
-      location={mockLocation({ pathname: '/issues', query: {} })}
-      router={mockRouter()}
-      {...props}
-    />
-  );
-}
diff --git a/server/sonar-web/src/main/js/apps/issues/components/__tests__/IssuesContainer-test.tsx b/server/sonar-web/src/main/js/apps/issues/components/__tests__/IssuesContainer-test.tsx
deleted file mode 100644 (file)
index b49f0e7..0000000
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * 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 IssuesCounter from '../IssuesCounter';
-
-it('formats numbers', () => {
-  expect(shallow(<IssuesCounter current={1234} total={987654321} />)).toMatchSnapshot();
-});
-
-it('does not show current', () => {
-  expect(shallow(<IssuesCounter current={undefined} total={987654321} />)).toMatchSnapshot();
-});
diff --git a/server/sonar-web/src/main/js/apps/issues/components/__tests__/IssuesList-test.tsx b/server/sonar-web/src/main/js/apps/issues/components/__tests__/IssuesList-test.tsx
deleted file mode 100644 (file)
index 70b9b0f..0000000
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * 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 { mockIssue } from '../../../../helpers/testMocks';
-import IssuesList from '../IssuesList';
-
-it('should render correctly', () => {
-  const wrapper = shallowRender({ issues: [] });
-  expect(wrapper).toMatchSnapshot();
-  wrapper.setProps({ issues: [mockIssue(), mockIssue(false, { key: 'AVsae-CQS-9G3txfbFN3' })] });
-  expect(wrapper).toMatchSnapshot();
-});
-
-function shallowRender(overrides: Partial<IssuesList['props']> = {}) {
-  return shallow(
-    <IssuesList
-      branchLike={undefined}
-      checked={[]}
-      component={undefined}
-      issues={[mockIssue(), mockIssue(false, { key: 'AVsae-CQS-9G3txfbFN3' })]}
-      onFilterChange={jest.fn()}
-      onIssueChange={jest.fn()}
-      onIssueCheck={jest.fn()}
-      onIssueClick={jest.fn()}
-      onPopupToggle={jest.fn()}
-      openPopup={undefined}
-      selectedIssue={undefined}
-      {...overrides}
-    />
-  );
-}
diff --git a/server/sonar-web/src/main/js/apps/issues/components/__tests__/IssuesSourceViewer-test.tsx b/server/sonar-web/src/main/js/apps/issues/components/__tests__/IssuesSourceViewer-test.tsx
deleted file mode 100644 (file)
index a921345..0000000
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * 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 { mockMainBranch } from '../../../../helpers/mocks/branch-like';
-import { mockFlowLocation, mockIssue } from '../../../../helpers/testMocks';
-import IssuesSourceViewer, { IssuesSourceViewerProps } from '../IssuesSourceViewer';
-
-it('should render SourceViewer correctly', () => {
-  expect(shallowRender()).toMatchSnapshot('default');
-  expect(
-    shallowRender({
-      issues: [mockIssue(true)],
-      openIssue: mockIssue(true, { flows: [[mockFlowLocation()]] }),
-    })
-  ).toMatchSnapshot('single secondary location');
-  expect(
-    shallowRender({
-      issues: [mockIssue(true)],
-      openIssue: mockIssue(true, {
-        flows: [[mockFlowLocation(), mockFlowLocation(), mockFlowLocation()]],
-      }),
-    })
-  ).toMatchSnapshot('all secondary locations on same line');
-});
-
-it('should render CrossComponentSourceViewer correctly', () => {
-  expect(
-    shallowRender({
-      issues: [mockIssue(true)],
-      openIssue: mockIssue(true, {
-        flows: [
-          [
-            mockFlowLocation(),
-            mockFlowLocation({
-              textRange: {
-                startLine: 10,
-                startOffset: 1,
-                endLine: 12,
-                endOffset: 2,
-              },
-            }),
-          ],
-        ],
-      }),
-    })
-  ).toMatchSnapshot();
-});
-
-function shallowRender(props: Partial<IssuesSourceViewerProps> = {}) {
-  return shallow(
-    <IssuesSourceViewer
-      branchLike={mockMainBranch()}
-      issues={[mockIssue()]}
-      locationsNavigator={true}
-      onIssueSelect={jest.fn()}
-      onLocationSelect={jest.fn()}
-      openIssue={mockIssue()}
-      selectedFlowIndex={undefined}
-      selectedLocationIndex={undefined}
-      {...props}
-    />
-  );
-}
diff --git a/server/sonar-web/src/main/js/apps/issues/components/__tests__/ListItem-test.tsx b/server/sonar-web/src/main/js/apps/issues/components/__tests__/ListItem-test.tsx
deleted file mode 100644 (file)
index 1be879f..0000000
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * 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 { mockBranch } from '../../../../helpers/mocks/branch-like';
-import { mockIssue } from '../../../../helpers/testMocks';
-import ListItem from '../ListItem';
-
-it('should render correctly', () => {
-  const wrapper = shallowRender();
-  expect(wrapper).toMatchSnapshot();
-});
-
-function shallowRender(props: Partial<ListItem['props']> = {}) {
-  return shallow<ListItem>(
-    <ListItem
-      branchLike={mockBranch()}
-      checked={false}
-      issue={mockIssue()}
-      onChange={jest.fn()}
-      onCheck={jest.fn()}
-      onClick={jest.fn()}
-      onFilterChange={jest.fn()}
-      onPopupToggle={jest.fn()}
-      openPopup={undefined}
-      selected={false}
-      {...props}
-    />
-  );
-}
diff --git a/server/sonar-web/src/main/js/apps/issues/components/__tests__/PageActions-test.tsx b/server/sonar-web/src/main/js/apps/issues/components/__tests__/PageActions-test.tsx
deleted file mode 100644 (file)
index f888987..0000000
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * 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 PageActions from '../PageActions';
-
-it('should render', () => {
-  expect(shallow(<PageActions canSetHome={true} effortTotal={125} />)).toMatchSnapshot();
-});
diff --git a/server/sonar-web/src/main/js/apps/issues/components/__tests__/TotalEffort-test.tsx b/server/sonar-web/src/main/js/apps/issues/components/__tests__/TotalEffort-test.tsx
deleted file mode 100644 (file)
index ce8399c..0000000
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * 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 TotalEffort from '../TotalEffort';
-
-it('should render', () => {
-  expect(shallow(<TotalEffort effort={125} />)).toMatchSnapshot();
-});
diff --git a/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/AssigneeSelect-test.tsx.snap b/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/AssigneeSelect-test.tsx.snap
deleted file mode 100644 (file)
index c104851..0000000
+++ /dev/null
@@ -1,88 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render correctly: default 1`] = `
-<SearchSelect
-  className="input-super-large"
-  components={
-    {
-      "Option": [Function],
-      "SingleValue": [Function],
-    }
-  }
-  defaultOptions={[]}
-  inputId="id"
-  isClearable={true}
-  loadOptions={[Function]}
-  noOptionsMessage={[Function]}
-  onChange={[MockFunction]}
-/>
-`;
-
-exports[`should render correctly: logged in & assignable issues 1`] = `
-<SearchSelect
-  className="input-super-large"
-  components={
-    {
-      "Option": [Function],
-      "SingleValue": [Function],
-    }
-  }
-  defaultOptions={
-    [
-      {
-        "avatar": undefined,
-        "label": "Skywalker",
-        "value": "luke",
-      },
-    ]
-  }
-  inputId="id"
-  isClearable={true}
-  loadOptions={[Function]}
-  noOptionsMessage={[Function]}
-  onChange={[MockFunction]}
-/>
-`;
-
-exports[`should render correctly: logged in & no assignable issues 1`] = `
-<SearchSelect
-  className="input-super-large"
-  components={
-    {
-      "Option": [Function],
-      "SingleValue": [Function],
-    }
-  }
-  defaultOptions={[]}
-  inputId="id"
-  isClearable={true}
-  loadOptions={[Function]}
-  noOptionsMessage={[Function]}
-  onChange={[MockFunction]}
-/>
-`;
-
-exports[`should render correctly: unassignable issues 1`] = `
-<SearchSelect
-  className="input-super-large"
-  components={
-    {
-      "Option": [Function],
-      "SingleValue": [Function],
-    }
-  }
-  defaultOptions={
-    [
-      {
-        "label": "unassigned",
-        "value": "",
-      },
-    ]
-  }
-  inputId="id"
-  isClearable={true}
-  loadOptions={[Function]}
-  noOptionsMessage={[Function]}
-  onChange={[MockFunction]}
-/>
-`;
diff --git a/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/BulkChangeModal-test.tsx.snap b/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/BulkChangeModal-test.tsx.snap
deleted file mode 100644 (file)
index 25d869b..0000000
+++ /dev/null
@@ -1,202 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should display error message when no issues available 1`] = `
-<Modal
-  onRequestClose={[Function]}
-  size="small"
->
-  <form
-    id="bulk-change-form"
-    onSubmit={[Function]}
-  >
-    <div
-      className="modal-head"
-    >
-      <h2>
-        issue_bulk_change.form.title.0
-      </h2>
-    </div>
-    <div
-      className="modal-body modal-container"
-    >
-      <Alert
-        variant="warning"
-      >
-        issue_bulk_change.no_match
-      </Alert>
-    </div>
-    <div
-      className="modal-foot"
-    >
-      <SubmitButton
-        disabled={true}
-        id="bulk-change-submit"
-      >
-        apply
-      </SubmitButton>
-      <ResetButtonLink
-        onClick={[Function]}
-      >
-        cancel
-      </ResetButtonLink>
-    </div>
-  </form>
-</Modal>
-`;
-
-exports[`should display form when issues are present 1`] = `
-<Modal
-  onRequestClose={[Function]}
-  size="small"
->
-  <form
-    id="bulk-change-form"
-    onSubmit={[Function]}
-  >
-    <div
-      className="modal-head"
-    >
-      <h2>
-        issue_bulk_change.form.title.1
-      </h2>
-    </div>
-    <div
-      className="modal-body modal-container"
-    >
-      <Checkbox
-        checked={false}
-        className="display-inline-block spacer-top"
-        id="send-notifications"
-        onCheck={[Function]}
-        right={true}
-        thirdState={false}
-      >
-        <strong
-          className="little-spacer-right"
-        >
-          issue.send_notifications
-        </strong>
-      </Checkbox>
-    </div>
-    <div
-      className="modal-foot"
-    >
-      <SubmitButton
-        disabled={true}
-        id="bulk-change-submit"
-      >
-        apply
-      </SubmitButton>
-      <ResetButtonLink
-        onClick={[Function]}
-      >
-        cancel
-      </ResetButtonLink>
-    </div>
-  </form>
-</Modal>
-`;
-
-exports[`should display warning when too many issues are passed 1`] = `
-<h2>
-  issue_bulk_change.form.title.500
-</h2>
-`;
-
-exports[`should display warning when too many issues are passed 2`] = `
-<Alert
-  variant="warning"
->
-  <FormattedMessage
-    defaultMessage="issue_bulk_change.max_issues_reached"
-    id="issue_bulk_change.max_issues_reached"
-    values={
-      {
-        "max": <strong>
-          500
-        </strong>,
-      }
-    }
-  />
-</Alert>
-`;
-
-exports[`should render select for severity: Option 1`] = `
-<Option
-  data={
-    {
-      "label": "label",
-      "value": "value",
-    }
-  }
->
-  <SeverityHelper
-    className="display-flex-center"
-    severity="value"
-  />
-</Option>
-`;
-
-exports[`should render select for severity: SingleValue 1`] = `
-<SingleValue
-  data={
-    {
-      "label": "label",
-      "value": "value",
-    }
-  }
->
-  <SeverityHelper
-    className="display-flex-center"
-    severity="value"
-  />
-</SingleValue>
-`;
-
-exports[`should render select for type: Option 1`] = `
-<Option
-  data={
-    {
-      "label": "label",
-      "value": "value",
-    }
-  }
->
-  <div
-    className="display-flex-center"
-  >
-    <IssueTypeIcon
-      query="value"
-    />
-    <span
-      className="little-spacer-left"
-    >
-      label
-    </span>
-  </div>
-</Option>
-`;
-
-exports[`should render select for type: SingleValue 1`] = `
-<SingleValue
-  data={
-    {
-      "label": "label",
-      "value": "value",
-    }
-  }
->
-  <div
-    className="display-flex-center"
-  >
-    <IssueTypeIcon
-      query="value"
-    />
-    <span
-      className="little-spacer-left"
-    >
-      label
-    </span>
-  </div>
-</SingleValue>
-`;
diff --git a/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/ComponentBreadcrumbs-test.tsx.snap b/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/ComponentBreadcrumbs-test.tsx.snap
deleted file mode 100644 (file)
index 7b2700d..0000000
+++ /dev/null
@@ -1,87 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`renders 1`] = `
-<div
-  aria-label="issues.on_file_x.proj-name, comp-name"
-  className="component-name text-ellipsis"
->
-  <QualifierIcon
-    className="spacer-right"
-    qualifier="FIL"
-  />
-  <span
-    title="proj-name - test-branch"
-  >
-    proj-name
-    <span
-      className="slash-separator"
-    />
-  </span>
-  <span
-    title="comp-name"
-  >
-    comp-name
-  </span>
-</div>
-`;
-
-exports[`renders issues properly for views 1`] = `
-<div
-  aria-label="issues.on_file_x.proj-name, comp-name"
-  className="component-name text-ellipsis"
->
-  <QualifierIcon
-    className="spacer-right"
-    qualifier="FIL"
-  />
-  <span
-    title="proj-name"
-  >
-    proj-name
-     - 
-    <span
-      className="badge"
-    >
-      branches.main_branch
-    </span>
-    <span
-      className="slash-separator"
-    />
-  </span>
-  <span
-    title="comp-name"
-  >
-    comp-name
-  </span>
-</div>
-`;
-
-exports[`renders issues properly for views: with branch information 1`] = `
-<div
-  aria-label="issues.on_file_x.proj-name, comp-name"
-  className="component-name text-ellipsis"
->
-  <QualifierIcon
-    className="spacer-right"
-    qualifier="FIL"
-  />
-  <span
-    title="proj-name - test-branch"
-  >
-    proj-name
-     - 
-    <BranchIcon />
-    <span>
-      test-branch
-    </span>
-    <span
-      className="slash-separator"
-    />
-  </span>
-  <span
-    title="comp-name"
-  >
-    comp-name
-  </span>
-</div>
-`;
diff --git a/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/IssuesContainer-test.tsx.snap b/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/IssuesContainer-test.tsx.snap
deleted file mode 100644 (file)
index 8caedd7..0000000
+++ /dev/null
@@ -1,18 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`does not show current 1`] = `
-<PageCounter
-  className="spacer-left"
-  label="issues.issues"
-  total={987654321}
-/>
-`;
-
-exports[`formats numbers 1`] = `
-<PageCounter
-  className="spacer-left"
-  current={1234}
-  label="issues.issues"
-  total={987654321}
-/>
-`;
diff --git a/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/IssuesList-test.tsx.snap b/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/IssuesList-test.tsx.snap
deleted file mode 100644 (file)
index 896ba13..0000000
+++ /dev/null
@@ -1,140 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render correctly 1`] = `
-<div>
-  <i
-    className="spinner"
-  />
-</div>
-`;
-
-exports[`should render correctly 2`] = `
-<ul>
-  <li>
-    <div
-      className="issues-workspace-list-component note"
-    >
-      <ComponentBreadcrumbs
-        issue={
-          {
-            "actions": [],
-            "component": "main.js",
-            "componentEnabled": true,
-            "componentLongName": "main.js",
-            "componentQualifier": "FIL",
-            "componentUuid": "foo1234",
-            "creationDate": "2017-03-01T09:36:01+0100",
-            "flows": [],
-            "flowsWithType": [],
-            "key": "AVsae-CQS-9G3txfbFN2",
-            "line": 25,
-            "message": "Reduce the number of conditional operators (4) used in the expression",
-            "project": "myproject",
-            "projectKey": "foo",
-            "projectName": "Foo",
-            "rule": "javascript:S1067",
-            "ruleName": "foo",
-            "secondaryLocations": [],
-            "severity": "MAJOR",
-            "status": "OPEN",
-            "textRange": {
-              "endLine": 26,
-              "endOffset": 15,
-              "startLine": 25,
-              "startOffset": 0,
-            },
-            "transitions": [],
-            "type": "BUG",
-          }
-        }
-      />
-    </div>
-  </li>
-  <ul>
-    <ListItem
-      checked={false}
-      issue={
-        {
-          "actions": [],
-          "component": "main.js",
-          "componentEnabled": true,
-          "componentLongName": "main.js",
-          "componentQualifier": "FIL",
-          "componentUuid": "foo1234",
-          "creationDate": "2017-03-01T09:36:01+0100",
-          "flows": [],
-          "flowsWithType": [],
-          "key": "AVsae-CQS-9G3txfbFN2",
-          "line": 25,
-          "message": "Reduce the number of conditional operators (4) used in the expression",
-          "project": "myproject",
-          "projectKey": "foo",
-          "projectName": "Foo",
-          "rule": "javascript:S1067",
-          "ruleName": "foo",
-          "secondaryLocations": [],
-          "severity": "MAJOR",
-          "status": "OPEN",
-          "textRange": {
-            "endLine": 26,
-            "endOffset": 15,
-            "startLine": 25,
-            "startOffset": 0,
-          },
-          "transitions": [],
-          "type": "BUG",
-        }
-      }
-      key="AVsae-CQS-9G3txfbFN2"
-      onChange={[MockFunction]}
-      onCheck={[MockFunction]}
-      onClick={[MockFunction]}
-      onFilterChange={[MockFunction]}
-      onPopupToggle={[MockFunction]}
-      selected={false}
-    />
-    <ListItem
-      checked={false}
-      issue={
-        {
-          "actions": [],
-          "component": "main.js",
-          "componentEnabled": true,
-          "componentLongName": "main.js",
-          "componentQualifier": "FIL",
-          "componentUuid": "foo1234",
-          "creationDate": "2017-03-01T09:36:01+0100",
-          "flows": [],
-          "flowsWithType": [],
-          "key": "AVsae-CQS-9G3txfbFN3",
-          "line": 25,
-          "message": "Reduce the number of conditional operators (4) used in the expression",
-          "project": "myproject",
-          "projectKey": "foo",
-          "projectName": "Foo",
-          "rule": "javascript:S1067",
-          "ruleName": "foo",
-          "secondaryLocations": [],
-          "severity": "MAJOR",
-          "status": "OPEN",
-          "textRange": {
-            "endLine": 26,
-            "endOffset": 15,
-            "startLine": 25,
-            "startOffset": 0,
-          },
-          "transitions": [],
-          "type": "BUG",
-        }
-      }
-      key="AVsae-CQS-9G3txfbFN3"
-      onChange={[MockFunction]}
-      onCheck={[MockFunction]}
-      onClick={[MockFunction]}
-      onFilterChange={[MockFunction]}
-      onPopupToggle={[MockFunction]}
-      selected={false}
-    />
-  </ul>
-</ul>
-`;
diff --git a/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/IssuesSourceViewer-test.tsx.snap b/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/IssuesSourceViewer-test.tsx.snap
deleted file mode 100644 (file)
index 1c81bef..0000000
+++ /dev/null
@@ -1,767 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render CrossComponentSourceViewer correctly 1`] = `
-<ContextProvider
-  value={
-    {
-      "registerPrimaryLocationRef": [Function],
-      "registerSelectedSecondaryLocationRef": [Function],
-    }
-  }
->
-  <CrossComponentSourceViewer
-    branchLike={
-      {
-        "analysisDate": "2018-01-01",
-        "excludedFromPurge": true,
-        "isMain": true,
-        "name": "master",
-      }
-    }
-    issue={
-      {
-        "actions": [],
-        "component": "main.js",
-        "componentEnabled": true,
-        "componentLongName": "main.js",
-        "componentQualifier": "FIL",
-        "componentUuid": "foo1234",
-        "creationDate": "2017-03-01T09:36:01+0100",
-        "flows": [
-          [
-            {
-              "component": "main.js",
-              "textRange": {
-                "endLine": 2,
-                "endOffset": 2,
-                "startLine": 1,
-                "startOffset": 1,
-              },
-            },
-            {
-              "component": "main.js",
-              "textRange": {
-                "endLine": 12,
-                "endOffset": 2,
-                "startLine": 10,
-                "startOffset": 1,
-              },
-            },
-          ],
-        ],
-        "flowsWithType": [],
-        "key": "AVsae-CQS-9G3txfbFN2",
-        "line": 25,
-        "message": "Reduce the number of conditional operators (4) used in the expression",
-        "project": "myproject",
-        "projectKey": "foo",
-        "projectName": "Foo",
-        "rule": "javascript:S1067",
-        "ruleName": "foo",
-        "secondaryLocations": [
-          {
-            "component": "main.js",
-            "index": 0,
-            "textRange": {
-              "endLine": 2,
-              "endOffset": 2,
-              "startLine": 1,
-              "startOffset": 1,
-            },
-          },
-          {
-            "component": "main.js",
-            "index": 1,
-            "textRange": {
-              "endLine": 2,
-              "endOffset": 2,
-              "startLine": 1,
-              "startOffset": 1,
-            },
-          },
-        ],
-        "severity": "MAJOR",
-        "status": "OPEN",
-        "textRange": {
-          "endLine": 26,
-          "endOffset": 15,
-          "startLine": 25,
-          "startOffset": 0,
-        },
-        "transitions": [],
-        "type": "BUG",
-      }
-    }
-    issues={
-      [
-        {
-          "actions": [],
-          "component": "main.js",
-          "componentEnabled": true,
-          "componentLongName": "main.js",
-          "componentQualifier": "FIL",
-          "componentUuid": "foo1234",
-          "creationDate": "2017-03-01T09:36:01+0100",
-          "flows": [
-            [
-              {
-                "component": "main.js",
-                "textRange": {
-                  "endLine": 2,
-                  "endOffset": 2,
-                  "startLine": 1,
-                  "startOffset": 1,
-                },
-              },
-              {
-                "component": "main.js",
-                "textRange": {
-                  "endLine": 2,
-                  "endOffset": 2,
-                  "startLine": 1,
-                  "startOffset": 1,
-                },
-              },
-              {
-                "component": "main.js",
-                "textRange": {
-                  "endLine": 2,
-                  "endOffset": 2,
-                  "startLine": 1,
-                  "startOffset": 1,
-                },
-              },
-            ],
-            [
-              {
-                "component": "main.js",
-                "textRange": {
-                  "endLine": 2,
-                  "endOffset": 2,
-                  "startLine": 1,
-                  "startOffset": 1,
-                },
-              },
-              {
-                "component": "main.js",
-                "textRange": {
-                  "endLine": 2,
-                  "endOffset": 2,
-                  "startLine": 1,
-                  "startOffset": 1,
-                },
-              },
-            ],
-          ],
-          "flowsWithType": [],
-          "key": "AVsae-CQS-9G3txfbFN2",
-          "line": 25,
-          "message": "Reduce the number of conditional operators (4) used in the expression",
-          "project": "myproject",
-          "projectKey": "foo",
-          "projectName": "Foo",
-          "rule": "javascript:S1067",
-          "ruleName": "foo",
-          "secondaryLocations": [
-            {
-              "component": "main.js",
-              "textRange": {
-                "endLine": 2,
-                "endOffset": 2,
-                "startLine": 1,
-                "startOffset": 1,
-              },
-            },
-            {
-              "component": "main.js",
-              "textRange": {
-                "endLine": 2,
-                "endOffset": 2,
-                "startLine": 1,
-                "startOffset": 1,
-              },
-            },
-          ],
-          "severity": "MAJOR",
-          "status": "OPEN",
-          "textRange": {
-            "endLine": 26,
-            "endOffset": 15,
-            "startLine": 25,
-            "startOffset": 0,
-          },
-          "transitions": [],
-          "type": "BUG",
-        },
-      ]
-    }
-    locations={
-      [
-        {
-          "component": "main.js",
-          "index": 0,
-          "textRange": {
-            "endLine": 2,
-            "endOffset": 2,
-            "startLine": 1,
-            "startOffset": 1,
-          },
-        },
-        {
-          "component": "main.js",
-          "index": 1,
-          "textRange": {
-            "endLine": 2,
-            "endOffset": 2,
-            "startLine": 1,
-            "startOffset": 1,
-          },
-        },
-      ]
-    }
-    onIssueSelect={[MockFunction]}
-    onLocationSelect={[MockFunction]}
-  />
-</ContextProvider>
-`;
-
-exports[`should render SourceViewer correctly: all secondary locations on same line 1`] = `
-<ContextProvider
-  value={
-    {
-      "registerPrimaryLocationRef": [Function],
-      "registerSelectedSecondaryLocationRef": [Function],
-    }
-  }
->
-  <CrossComponentSourceViewer
-    branchLike={
-      {
-        "analysisDate": "2018-01-01",
-        "excludedFromPurge": true,
-        "isMain": true,
-        "name": "master",
-      }
-    }
-    issue={
-      {
-        "actions": [],
-        "component": "main.js",
-        "componentEnabled": true,
-        "componentLongName": "main.js",
-        "componentQualifier": "FIL",
-        "componentUuid": "foo1234",
-        "creationDate": "2017-03-01T09:36:01+0100",
-        "flows": [
-          [
-            {
-              "component": "main.js",
-              "textRange": {
-                "endLine": 2,
-                "endOffset": 2,
-                "startLine": 1,
-                "startOffset": 1,
-              },
-            },
-            {
-              "component": "main.js",
-              "textRange": {
-                "endLine": 2,
-                "endOffset": 2,
-                "startLine": 1,
-                "startOffset": 1,
-              },
-            },
-            {
-              "component": "main.js",
-              "textRange": {
-                "endLine": 2,
-                "endOffset": 2,
-                "startLine": 1,
-                "startOffset": 1,
-              },
-            },
-          ],
-        ],
-        "flowsWithType": [],
-        "key": "AVsae-CQS-9G3txfbFN2",
-        "line": 25,
-        "message": "Reduce the number of conditional operators (4) used in the expression",
-        "project": "myproject",
-        "projectKey": "foo",
-        "projectName": "Foo",
-        "rule": "javascript:S1067",
-        "ruleName": "foo",
-        "secondaryLocations": [
-          {
-            "component": "main.js",
-            "index": 0,
-            "textRange": {
-              "endLine": 2,
-              "endOffset": 2,
-              "startLine": 1,
-              "startOffset": 1,
-            },
-          },
-          {
-            "component": "main.js",
-            "index": 1,
-            "textRange": {
-              "endLine": 2,
-              "endOffset": 2,
-              "startLine": 1,
-              "startOffset": 1,
-            },
-          },
-        ],
-        "severity": "MAJOR",
-        "status": "OPEN",
-        "textRange": {
-          "endLine": 26,
-          "endOffset": 15,
-          "startLine": 25,
-          "startOffset": 0,
-        },
-        "transitions": [],
-        "type": "BUG",
-      }
-    }
-    issues={
-      [
-        {
-          "actions": [],
-          "component": "main.js",
-          "componentEnabled": true,
-          "componentLongName": "main.js",
-          "componentQualifier": "FIL",
-          "componentUuid": "foo1234",
-          "creationDate": "2017-03-01T09:36:01+0100",
-          "flows": [
-            [
-              {
-                "component": "main.js",
-                "textRange": {
-                  "endLine": 2,
-                  "endOffset": 2,
-                  "startLine": 1,
-                  "startOffset": 1,
-                },
-              },
-              {
-                "component": "main.js",
-                "textRange": {
-                  "endLine": 2,
-                  "endOffset": 2,
-                  "startLine": 1,
-                  "startOffset": 1,
-                },
-              },
-              {
-                "component": "main.js",
-                "textRange": {
-                  "endLine": 2,
-                  "endOffset": 2,
-                  "startLine": 1,
-                  "startOffset": 1,
-                },
-              },
-            ],
-            [
-              {
-                "component": "main.js",
-                "textRange": {
-                  "endLine": 2,
-                  "endOffset": 2,
-                  "startLine": 1,
-                  "startOffset": 1,
-                },
-              },
-              {
-                "component": "main.js",
-                "textRange": {
-                  "endLine": 2,
-                  "endOffset": 2,
-                  "startLine": 1,
-                  "startOffset": 1,
-                },
-              },
-            ],
-          ],
-          "flowsWithType": [],
-          "key": "AVsae-CQS-9G3txfbFN2",
-          "line": 25,
-          "message": "Reduce the number of conditional operators (4) used in the expression",
-          "project": "myproject",
-          "projectKey": "foo",
-          "projectName": "Foo",
-          "rule": "javascript:S1067",
-          "ruleName": "foo",
-          "secondaryLocations": [
-            {
-              "component": "main.js",
-              "textRange": {
-                "endLine": 2,
-                "endOffset": 2,
-                "startLine": 1,
-                "startOffset": 1,
-              },
-            },
-            {
-              "component": "main.js",
-              "textRange": {
-                "endLine": 2,
-                "endOffset": 2,
-                "startLine": 1,
-                "startOffset": 1,
-              },
-            },
-          ],
-          "severity": "MAJOR",
-          "status": "OPEN",
-          "textRange": {
-            "endLine": 26,
-            "endOffset": 15,
-            "startLine": 25,
-            "startOffset": 0,
-          },
-          "transitions": [],
-          "type": "BUG",
-        },
-      ]
-    }
-    locations={
-      [
-        {
-          "component": "main.js",
-          "index": 0,
-          "textRange": {
-            "endLine": 2,
-            "endOffset": 2,
-            "startLine": 1,
-            "startOffset": 1,
-          },
-        },
-        {
-          "component": "main.js",
-          "index": 1,
-          "textRange": {
-            "endLine": 2,
-            "endOffset": 2,
-            "startLine": 1,
-            "startOffset": 1,
-          },
-        },
-      ]
-    }
-    onIssueSelect={[MockFunction]}
-    onLocationSelect={[MockFunction]}
-  />
-</ContextProvider>
-`;
-
-exports[`should render SourceViewer correctly: default 1`] = `
-<ContextProvider
-  value={
-    {
-      "registerPrimaryLocationRef": [Function],
-      "registerSelectedSecondaryLocationRef": [Function],
-    }
-  }
->
-  <CrossComponentSourceViewer
-    branchLike={
-      {
-        "analysisDate": "2018-01-01",
-        "excludedFromPurge": true,
-        "isMain": true,
-        "name": "master",
-      }
-    }
-    issue={
-      {
-        "actions": [],
-        "component": "main.js",
-        "componentEnabled": true,
-        "componentLongName": "main.js",
-        "componentQualifier": "FIL",
-        "componentUuid": "foo1234",
-        "creationDate": "2017-03-01T09:36:01+0100",
-        "flows": [],
-        "flowsWithType": [],
-        "key": "AVsae-CQS-9G3txfbFN2",
-        "line": 25,
-        "message": "Reduce the number of conditional operators (4) used in the expression",
-        "project": "myproject",
-        "projectKey": "foo",
-        "projectName": "Foo",
-        "rule": "javascript:S1067",
-        "ruleName": "foo",
-        "secondaryLocations": [],
-        "severity": "MAJOR",
-        "status": "OPEN",
-        "textRange": {
-          "endLine": 26,
-          "endOffset": 15,
-          "startLine": 25,
-          "startOffset": 0,
-        },
-        "transitions": [],
-        "type": "BUG",
-      }
-    }
-    issues={
-      [
-        {
-          "actions": [],
-          "component": "main.js",
-          "componentEnabled": true,
-          "componentLongName": "main.js",
-          "componentQualifier": "FIL",
-          "componentUuid": "foo1234",
-          "creationDate": "2017-03-01T09:36:01+0100",
-          "flows": [],
-          "flowsWithType": [],
-          "key": "AVsae-CQS-9G3txfbFN2",
-          "line": 25,
-          "message": "Reduce the number of conditional operators (4) used in the expression",
-          "project": "myproject",
-          "projectKey": "foo",
-          "projectName": "Foo",
-          "rule": "javascript:S1067",
-          "ruleName": "foo",
-          "secondaryLocations": [],
-          "severity": "MAJOR",
-          "status": "OPEN",
-          "textRange": {
-            "endLine": 26,
-            "endOffset": 15,
-            "startLine": 25,
-            "startOffset": 0,
-          },
-          "transitions": [],
-          "type": "BUG",
-        },
-      ]
-    }
-    locations={[]}
-    onIssueSelect={[MockFunction]}
-    onLocationSelect={[MockFunction]}
-  />
-</ContextProvider>
-`;
-
-exports[`should render SourceViewer correctly: single secondary location 1`] = `
-<ContextProvider
-  value={
-    {
-      "registerPrimaryLocationRef": [Function],
-      "registerSelectedSecondaryLocationRef": [Function],
-    }
-  }
->
-  <CrossComponentSourceViewer
-    branchLike={
-      {
-        "analysisDate": "2018-01-01",
-        "excludedFromPurge": true,
-        "isMain": true,
-        "name": "master",
-      }
-    }
-    issue={
-      {
-        "actions": [],
-        "component": "main.js",
-        "componentEnabled": true,
-        "componentLongName": "main.js",
-        "componentQualifier": "FIL",
-        "componentUuid": "foo1234",
-        "creationDate": "2017-03-01T09:36:01+0100",
-        "flows": [
-          [
-            {
-              "component": "main.js",
-              "textRange": {
-                "endLine": 2,
-                "endOffset": 2,
-                "startLine": 1,
-                "startOffset": 1,
-              },
-            },
-          ],
-        ],
-        "flowsWithType": [],
-        "key": "AVsae-CQS-9G3txfbFN2",
-        "line": 25,
-        "message": "Reduce the number of conditional operators (4) used in the expression",
-        "project": "myproject",
-        "projectKey": "foo",
-        "projectName": "Foo",
-        "rule": "javascript:S1067",
-        "ruleName": "foo",
-        "secondaryLocations": [
-          {
-            "component": "main.js",
-            "index": 0,
-            "textRange": {
-              "endLine": 2,
-              "endOffset": 2,
-              "startLine": 1,
-              "startOffset": 1,
-            },
-          },
-          {
-            "component": "main.js",
-            "index": 1,
-            "textRange": {
-              "endLine": 2,
-              "endOffset": 2,
-              "startLine": 1,
-              "startOffset": 1,
-            },
-          },
-        ],
-        "severity": "MAJOR",
-        "status": "OPEN",
-        "textRange": {
-          "endLine": 26,
-          "endOffset": 15,
-          "startLine": 25,
-          "startOffset": 0,
-        },
-        "transitions": [],
-        "type": "BUG",
-      }
-    }
-    issues={
-      [
-        {
-          "actions": [],
-          "component": "main.js",
-          "componentEnabled": true,
-          "componentLongName": "main.js",
-          "componentQualifier": "FIL",
-          "componentUuid": "foo1234",
-          "creationDate": "2017-03-01T09:36:01+0100",
-          "flows": [
-            [
-              {
-                "component": "main.js",
-                "textRange": {
-                  "endLine": 2,
-                  "endOffset": 2,
-                  "startLine": 1,
-                  "startOffset": 1,
-                },
-              },
-              {
-                "component": "main.js",
-                "textRange": {
-                  "endLine": 2,
-                  "endOffset": 2,
-                  "startLine": 1,
-                  "startOffset": 1,
-                },
-              },
-              {
-                "component": "main.js",
-                "textRange": {
-                  "endLine": 2,
-                  "endOffset": 2,
-                  "startLine": 1,
-                  "startOffset": 1,
-                },
-              },
-            ],
-            [
-              {
-                "component": "main.js",
-                "textRange": {
-                  "endLine": 2,
-                  "endOffset": 2,
-                  "startLine": 1,
-                  "startOffset": 1,
-                },
-              },
-              {
-                "component": "main.js",
-                "textRange": {
-                  "endLine": 2,
-                  "endOffset": 2,
-                  "startLine": 1,
-                  "startOffset": 1,
-                },
-              },
-            ],
-          ],
-          "flowsWithType": [],
-          "key": "AVsae-CQS-9G3txfbFN2",
-          "line": 25,
-          "message": "Reduce the number of conditional operators (4) used in the expression",
-          "project": "myproject",
-          "projectKey": "foo",
-          "projectName": "Foo",
-          "rule": "javascript:S1067",
-          "ruleName": "foo",
-          "secondaryLocations": [
-            {
-              "component": "main.js",
-              "textRange": {
-                "endLine": 2,
-                "endOffset": 2,
-                "startLine": 1,
-                "startOffset": 1,
-              },
-            },
-            {
-              "component": "main.js",
-              "textRange": {
-                "endLine": 2,
-                "endOffset": 2,
-                "startLine": 1,
-                "startOffset": 1,
-              },
-            },
-          ],
-          "severity": "MAJOR",
-          "status": "OPEN",
-          "textRange": {
-            "endLine": 26,
-            "endOffset": 15,
-            "startLine": 25,
-            "startOffset": 0,
-          },
-          "transitions": [],
-          "type": "BUG",
-        },
-      ]
-    }
-    locations={
-      [
-        {
-          "component": "main.js",
-          "index": 0,
-          "textRange": {
-            "endLine": 2,
-            "endOffset": 2,
-            "startLine": 1,
-            "startOffset": 1,
-          },
-        },
-        {
-          "component": "main.js",
-          "index": 1,
-          "textRange": {
-            "endLine": 2,
-            "endOffset": 2,
-            "startLine": 1,
-            "startOffset": 1,
-          },
-        },
-      ]
-    }
-    onIssueSelect={[MockFunction]}
-    onLocationSelect={[MockFunction]}
-  />
-</ContextProvider>
-`;
diff --git a/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/ListItem-test.tsx.snap b/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/ListItem-test.tsx.snap
deleted file mode 100644 (file)
index d8cefe9..0000000
+++ /dev/null
@@ -1,57 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render correctly 1`] = `
-<li
-  className="issues-workspace-list-item"
->
-  <Issue
-    branchLike={
-      {
-        "analysisDate": "2018-01-01",
-        "excludedFromPurge": true,
-        "isMain": false,
-        "name": "branch-6.7",
-      }
-    }
-    checked={false}
-    issue={
-      {
-        "actions": [],
-        "component": "main.js",
-        "componentEnabled": true,
-        "componentLongName": "main.js",
-        "componentQualifier": "FIL",
-        "componentUuid": "foo1234",
-        "creationDate": "2017-03-01T09:36:01+0100",
-        "flows": [],
-        "flowsWithType": [],
-        "key": "AVsae-CQS-9G3txfbFN2",
-        "line": 25,
-        "message": "Reduce the number of conditional operators (4) used in the expression",
-        "project": "myproject",
-        "projectKey": "foo",
-        "projectName": "Foo",
-        "rule": "javascript:S1067",
-        "ruleName": "foo",
-        "secondaryLocations": [],
-        "severity": "MAJOR",
-        "status": "OPEN",
-        "textRange": {
-          "endLine": 26,
-          "endOffset": 15,
-          "startLine": 25,
-          "startOffset": 0,
-        },
-        "transitions": [],
-        "type": "BUG",
-      }
-    }
-    onChange={[MockFunction]}
-    onCheck={[MockFunction]}
-    onClick={[MockFunction]}
-    onFilter={[Function]}
-    onPopupToggle={[MockFunction]}
-    selected={false}
-  />
-</li>
-`;
diff --git a/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/PageActions-test.tsx.snap b/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/PageActions-test.tsx.snap
deleted file mode 100644 (file)
index 59fd91e..0000000
+++ /dev/null
@@ -1,27 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render 1`] = `
-<div
-  className="display-flex-center display-flex-justify-end"
->
-  <PageShortcutsTooltip
-    leftAndRightLabel="issues.to_navigate"
-    upAndDownLabel="issues.to_select_issues"
-  />
-  <div
-    className="spacer-left issues-page-actions"
-  >
-    <TotalEffort
-      effort={125}
-    />
-  </div>
-  <withCurrentUserContext(HomePageSelect)
-    className="huge-spacer-left"
-    currentPage={
-      {
-        "type": "ISSUES",
-      }
-    }
-  />
-</div>
-`;
diff --git a/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/TotalEffort-test.tsx.snap b/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/TotalEffort-test.tsx.snap
deleted file mode 100644 (file)
index 9de1e34..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render 1`] = `
-<div
-  className="display-inline-block bordered-left spacer-left"
->
-  <div
-    className="spacer-left"
-  >
-    <FormattedMessage
-      defaultMessage="issue.x_effort"
-      id="issue.x_effort"
-      values={
-        {
-          "0": <strong>
-            work_duration.x_hours.2 work_duration.x_minutes.5
-          </strong>,
-        }
-      }
-    />
-  </div>
-</div>
-`;
index 15c403163d52fe935423691873ce5a86bd1f885a..72e67a7638c1023a29a14b6d6ea5fc5b79e56963 100644 (file)
@@ -20,6 +20,8 @@
 import { Issue, Paging, TextRange } from './types';
 import { UserBase } from './users';
 
+export const ASSIGNEE_ME = '__me__';
+
 export enum IssueType {
   CodeSmell = 'CODE_SMELL',
   Vulnerability = 'VULNERABILITY',