From 019be73fb00148ee912ed827284f63c33df9b95a Mon Sep 17 00:00:00 2001 From: 7PH Date: Fri, 24 Feb 2023 11:38:09 +0100 Subject: [PATCH] SONAR-18552 Migrate issues app components to RTL --- .../main/js/api/mocks/IssuesServiceMock.ts | 65 +- .../js/apps/issues/__tests__/IssuesApp-it.tsx | 1203 +++++++++-------- .../issues/components/BulkChangeModal.tsx | 4 +- .../js/apps/issues/components/IssuesApp.tsx | 5 +- .../__tests__/AssigneeSelect-test.tsx | 167 +-- .../__tests__/BulkChangeModal-it.tsx | 203 +++ .../__tests__/BulkChangeModal-test.tsx | 125 -- .../__tests__/ComponentBreadcrumbs-test.tsx | 53 +- .../components/__tests__/IssuesApp-test.tsx | 497 ------- .../__tests__/IssuesContainer-test.tsx | 30 - .../components/__tests__/IssuesList-test.tsx | 49 - .../__tests__/IssuesSourceViewer-test.tsx | 81 -- .../components/__tests__/ListItem-test.tsx | 47 - .../components/__tests__/PageActions-test.tsx | 26 - .../components/__tests__/TotalEffort-test.tsx | 26 - .../AssigneeSelect-test.tsx.snap | 88 -- .../BulkChangeModal-test.tsx.snap | 202 --- .../ComponentBreadcrumbs-test.tsx.snap | 87 -- .../IssuesContainer-test.tsx.snap | 18 - .../__snapshots__/IssuesList-test.tsx.snap | 140 -- .../IssuesSourceViewer-test.tsx.snap | 767 ----------- .../__snapshots__/ListItem-test.tsx.snap | 57 - .../__snapshots__/PageActions-test.tsx.snap | 27 - .../__snapshots__/TotalEffort-test.tsx.snap | 23 - server/sonar-web/src/main/js/types/issues.ts | 2 + 25 files changed, 1036 insertions(+), 2956 deletions(-) create mode 100644 server/sonar-web/src/main/js/apps/issues/components/__tests__/BulkChangeModal-it.tsx delete mode 100644 server/sonar-web/src/main/js/apps/issues/components/__tests__/BulkChangeModal-test.tsx delete mode 100644 server/sonar-web/src/main/js/apps/issues/components/__tests__/IssuesApp-test.tsx delete mode 100644 server/sonar-web/src/main/js/apps/issues/components/__tests__/IssuesContainer-test.tsx delete mode 100644 server/sonar-web/src/main/js/apps/issues/components/__tests__/IssuesList-test.tsx delete mode 100644 server/sonar-web/src/main/js/apps/issues/components/__tests__/IssuesSourceViewer-test.tsx delete mode 100644 server/sonar-web/src/main/js/apps/issues/components/__tests__/ListItem-test.tsx delete mode 100644 server/sonar-web/src/main/js/apps/issues/components/__tests__/PageActions-test.tsx delete mode 100644 server/sonar-web/src/main/js/apps/issues/components/__tests__/TotalEffort-test.tsx delete mode 100644 server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/AssigneeSelect-test.tsx.snap delete mode 100644 server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/BulkChangeModal-test.tsx.snap delete mode 100644 server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/ComponentBreadcrumbs-test.tsx.snap delete mode 100644 server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/IssuesContainer-test.tsx.snap delete mode 100644 server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/IssuesList-test.tsx.snap delete mode 100644 server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/IssuesSourceViewer-test.tsx.snap delete mode 100644 server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/ListItem-test.tsx.snap delete mode 100644 server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/PageActions-test.tsx.snap delete mode 100644 server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/TotalEffort-test.tsx.snap diff --git a/server/sonar-web/src/main/js/api/mocks/IssuesServiceMock.ts b/server/sonar-web/src/main/js/api/mocks/IssuesServiceMock.ts index 5f1cfdeff52..ec195133c9a 100644 --- a/server/sonar-web/src/main/js/api/mocks/IssuesServiceMock.ts +++ b/server/sonar-web/src/main/js/api/mocks/IssuesServiceMock.ts @@ -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, + }), }); }; diff --git a/server/sonar-web/src/main/js/apps/issues/__tests__/IssuesApp-it.tsx b/server/sonar-web/src/main/js/apps/issues/__tests__/IssuesApp-it.tsx index d21fe46459b..8507717caa2 100644 --- a/server/sonar-web/src/main/js/apps/issues/__tests__/IssuesApp-it.tsx +++ b/server/sonar-web/src/main/js/apps/issues/__tests__/IssuesApp-it.tsx @@ -17,16 +17,17 @@ * 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(); }); }); diff --git a/server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.tsx b/server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.tsx index 220f5f173d9..1a31ccfb7ff 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.tsx +++ b/server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.tsx @@ -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 { 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, })); diff --git a/server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx b/server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx index b9429272f57..99904430b65 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx +++ b/server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx @@ -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 { } 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 { }; if (myIssues) { - Object.assign(parameters, { assignees: '__me__' }); + Object.assign(parameters, { assignees: ASSIGNEE_ME }); } return this.fetchIssuesHelper(parameters).then(({ facets }) => parseFacets(facets)[property]); diff --git a/server/sonar-web/src/main/js/apps/issues/components/__tests__/AssigneeSelect-test.tsx b/server/sonar-web/src/main/js/apps/issues/components/__tests__/AssigneeSelect-test.tsx index af11d1b37f6..48474e56cf3 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/__tests__/AssigneeSelect-test.tsx +++ b/server/sonar-web/src/main/js/apps/issues/components/__tests__/AssigneeSelect-test.tsx @@ -17,129 +17,110 @@ * 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>>(SearchSelect).props() - .noOptionsMessage!({ inputValue: 'a' }) - ).toBe(`select2.tooShort.${MIN_QUERY_LENGTH}`); - - expect( - wrapper.find>>(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 = {}) { - return shallow( +function renderAssigneeSelect(overrides: Partial = {}) { + return renderComponent( ({ + 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 = {}) { + return renderComponent( + + 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 index f71b1122431..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/components/__tests__/BulkChangeModal-test.tsx +++ /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((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(Select).at(0).simulate('change', { value: 'foo' }); - - expect(wrapper.find(SubmitButton).props().disabled).toBe(false); - resolve(); - }); -}); - -const getWrapper = (issues: Issue[]) => { - return shallow( - - Promise.resolve({ - issues, - paging: { - pageIndex: issues.length, - pageSize: issues.length, - total: issues.length, - }, - }) - } - onClose={() => {}} - onDone={() => {}} - /> - ); -}; diff --git a/server/sonar-web/src/main/js/apps/issues/components/__tests__/ComponentBreadcrumbs-test.tsx b/server/sonar-web/src/main/js/apps/issues/components/__tests__/ComponentBreadcrumbs-test.tsx index 7fa48437c22..fb6b445d99e 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/__tests__/ComponentBreadcrumbs-test.tsx +++ b/server/sonar-web/src/main/js/apps/issues/components/__tests__/ComponentBreadcrumbs-test.tsx @@ -17,11 +17,13 @@ * 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() - ).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( - - ) - ).toMatchSnapshot(); - expect( - shallow( - - ) - ).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 = {}) { + return renderComponent( + + ); +} 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 index ea2b2f63ff9..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/components/__tests__/IssuesApp-test.tsx +++ /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 = {}) { - return shallow( - - ); -} 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 index b49f0e78098..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/components/__tests__/IssuesContainer-test.tsx +++ /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()).toMatchSnapshot(); -}); - -it('does not show current', () => { - expect(shallow()).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 index 70b9b0f5e60..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/components/__tests__/IssuesList-test.tsx +++ /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 = {}) { - return shallow( - - ); -} 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 index a921345aa2d..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/components/__tests__/IssuesSourceViewer-test.tsx +++ /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 = {}) { - return shallow( - - ); -} 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 index 1be879f64fa..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/components/__tests__/ListItem-test.tsx +++ /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 = {}) { - return shallow( - - ); -} 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 index f8889874eed..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/components/__tests__/PageActions-test.tsx +++ /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()).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 index ce8399cd696..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/components/__tests__/TotalEffort-test.tsx +++ /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()).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 index c1048510935..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/AssigneeSelect-test.tsx.snap +++ /dev/null @@ -1,88 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render correctly: default 1`] = ` - -`; - -exports[`should render correctly: logged in & assignable issues 1`] = ` - -`; - -exports[`should render correctly: logged in & no assignable issues 1`] = ` - -`; - -exports[`should render correctly: unassignable issues 1`] = ` - -`; 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 index 25d869b3512..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/BulkChangeModal-test.tsx.snap +++ /dev/null @@ -1,202 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should display error message when no issues available 1`] = ` - -
-
-

- issue_bulk_change.form.title.0 -

-
-
- - issue_bulk_change.no_match - -
-
- - apply - - - cancel - -
-
-
-`; - -exports[`should display form when issues are present 1`] = ` - -
-
-

- issue_bulk_change.form.title.1 -

-
-
- - - issue.send_notifications - - -
-
- - apply - - - cancel - -
-
-
-`; - -exports[`should display warning when too many issues are passed 1`] = ` -

- issue_bulk_change.form.title.500 -

-`; - -exports[`should display warning when too many issues are passed 2`] = ` - - - 500 - , - } - } - /> - -`; - -exports[`should render select for severity: Option 1`] = ` - -`; - -exports[`should render select for severity: SingleValue 1`] = ` - - - -`; - -exports[`should render select for type: Option 1`] = ` - -`; - -exports[`should render select for type: SingleValue 1`] = ` - -
- - - label - -
-
-`; 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 index 7b2700d83e6..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/ComponentBreadcrumbs-test.tsx.snap +++ /dev/null @@ -1,87 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`renders 1`] = ` -
- - - proj-name - - - - comp-name - -
-`; - -exports[`renders issues properly for views 1`] = ` -
- - - proj-name - - - - branches.main_branch - - - - - comp-name - -
-`; - -exports[`renders issues properly for views: with branch information 1`] = ` -
- - - proj-name - - - - - test-branch - - - - - comp-name - -
-`; 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 index 8caedd7a37b..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/IssuesContainer-test.tsx.snap +++ /dev/null @@ -1,18 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`does not show current 1`] = ` - -`; - -exports[`formats numbers 1`] = ` - -`; 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 index 896ba138392..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/IssuesList-test.tsx.snap +++ /dev/null @@ -1,140 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render correctly 1`] = ` -
- -
-`; - -exports[`should render correctly 2`] = ` -
    -
  • -
    - -
    -
  • -
      - - -
    -
-`; 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 index 1c81bef37a8..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/IssuesSourceViewer-test.tsx.snap +++ /dev/null @@ -1,767 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render CrossComponentSourceViewer correctly 1`] = ` - - - -`; - -exports[`should render SourceViewer correctly: all secondary locations on same line 1`] = ` - - - -`; - -exports[`should render SourceViewer correctly: default 1`] = ` - - - -`; - -exports[`should render SourceViewer correctly: single secondary location 1`] = ` - - - -`; 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 index d8cefe95f1b..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/ListItem-test.tsx.snap +++ /dev/null @@ -1,57 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render correctly 1`] = ` -
  • - -
  • -`; 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 index 59fd91e1449..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/PageActions-test.tsx.snap +++ /dev/null @@ -1,27 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render 1`] = ` -
    - -
    - -
    - -
    -`; 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 index 9de1e34ae9b..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/components/__tests__/__snapshots__/TotalEffort-test.tsx.snap +++ /dev/null @@ -1,23 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render 1`] = ` -
    -
    - - work_duration.x_hours.2 work_duration.x_minutes.5 - , - } - } - /> -
    -
    -`; diff --git a/server/sonar-web/src/main/js/types/issues.ts b/server/sonar-web/src/main/js/types/issues.ts index 15c403163d5..72e67a7638c 100644 --- a/server/sonar-web/src/main/js/types/issues.ts +++ b/server/sonar-web/src/main/js/types/issues.ts @@ -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', -- 2.39.5