]> source.dussan.org Git - sonarqube.git/blob
2e271e26ec0507ae475df86759a98440fd1061de
[sonarqube.git] /
1 /*
2  * SonarQube
3  * Copyright (C) 2009-2023 SonarSource SA
4  * mailto:info AT sonarsource DOT com
5  *
6  * This program is free software; you can redistribute it and/or
7  * modify it under the terms of the GNU Lesser General Public
8  * License as published by the Free Software Foundation; either
9  * version 3 of the License, or (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14  * Lesser General Public License for more details.
15  *
16  * You should have received a copy of the GNU Lesser General Public License
17  * along with this program; if not, write to the Free Software Foundation,
18  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
19  */
20 import { act, within } from '@testing-library/react';
21 import userEvent from '@testing-library/user-event';
22 import { first, last } from 'lodash';
23 import selectEvent from 'react-select-event';
24 import { MessageTypes } from '../../../../api/messages';
25 import BranchesServiceMock from '../../../../api/mocks/BranchesServiceMock';
26 import MessagesServiceMock from '../../../../api/mocks/MessagesServiceMock';
27 import NewCodeDefinitionServiceMock from '../../../../api/mocks/NewCodeDefinitionServiceMock';
28 import { ProjectActivityServiceMock } from '../../../../api/mocks/ProjectActivityServiceMock';
29 import { mockComponent } from '../../../../helpers/mocks/component';
30 import { mockNewCodePeriodBranch } from '../../../../helpers/mocks/new-code-definition';
31 import { mockAppState } from '../../../../helpers/testMocks';
32 import {
33   RenderContext,
34   renderAppWithComponentContext,
35 } from '../../../../helpers/testReactTestingUtils';
36 import { byLabelText, byRole, byText } from '../../../../helpers/testSelector';
37 import { Feature } from '../../../../types/features';
38 import { NewCodeDefinitionType } from '../../../../types/new-code-definition';
39 import routes from '../../routes';
40
41 jest.mock('../../../../api/newCodeDefinition');
42 jest.mock('../../../../api/projectActivity');
43 jest.mock('../../../../api/branches');
44
45 const newCodeDefinitionMock = new NewCodeDefinitionServiceMock();
46 const projectActivityMock = new ProjectActivityServiceMock();
47 const branchHandler = new BranchesServiceMock();
48 const messagesMock = new MessagesServiceMock();
49
50 afterEach(() => {
51   branchHandler.reset();
52   newCodeDefinitionMock.reset();
53   projectActivityMock.reset();
54   messagesMock.reset();
55 });
56
57 it('renders correctly without branch support feature', async () => {
58   const { ui } = getPageObjects();
59   renderProjectNewCodeDefinitionApp();
60   await ui.appIsLoaded();
61
62   expect(await ui.generalSettingRadio.find()).toBeChecked();
63   expect(ui.specificAnalysisRadio.query()).not.toBeInTheDocument();
64
65   // User is not admin
66   expect(ui.generalSettingsLink.query()).not.toBeInTheDocument();
67
68   // Specific branch setting is not rendered without feature branch
69   expect(ui.branchListHeading.query()).not.toBeInTheDocument();
70   expect(ui.referenceBranchRadio.query()).not.toBeInTheDocument();
71 });
72
73 it('renders correctly with branch support feature', async () => {
74   const { ui } = getPageObjects();
75   renderProjectNewCodeDefinitionApp({
76     featureList: [Feature.BranchSupport],
77     appState: mockAppState({ canAdmin: true }),
78   });
79   await ui.appIsLoaded();
80
81   expect(await ui.generalSettingRadio.find()).toBeChecked();
82   expect(ui.specificAnalysisRadio.query()).not.toBeInTheDocument();
83
84   // User is admin
85   expect(ui.generalSettingsLink.get()).toBeInTheDocument();
86
87   // Specific branch setting is rendered with feature support branch
88   expect(ui.branchListHeading.get()).toBeInTheDocument();
89   expect(ui.referenceBranchRadio.get()).toBeInTheDocument();
90 });
91
92 it('can set previous version specific setting', async () => {
93   const { ui, user } = getPageObjects();
94   renderProjectNewCodeDefinitionApp();
95   await ui.appIsLoaded();
96
97   expect(await ui.previousVersionRadio.find()).toHaveClass('disabled');
98   await ui.setPreviousVersionSetting();
99   expect(ui.previousVersionRadio.get()).toBeChecked();
100
101   // Save changes
102   await user.click(ui.saveButton.get());
103
104   expect(ui.saveButton.get()).toBeDisabled();
105
106   // Set general setting
107   await user.click(ui.generalSettingRadio.get());
108   expect(ui.previousVersionRadio.get()).toHaveClass('disabled');
109   await user.click(ui.saveButton.get());
110   expect(ui.saveButton.get()).toBeDisabled();
111 });
112
113 it('can set number of days specific setting', async () => {
114   const { ui, user } = getPageObjects();
115   renderProjectNewCodeDefinitionApp();
116   await ui.appIsLoaded();
117
118   expect(await ui.numberDaysRadio.find()).toHaveClass('disabled');
119   await ui.setNumberDaysSetting('10');
120   expect(ui.numberDaysRadio.get()).toBeChecked();
121
122   // Reset to initial state
123   await user.click(ui.cancelButton.get());
124   expect(ui.generalSettingRadio.get()).toBeChecked();
125   expect(ui.numberDaysRadio.get()).toHaveClass('disabled');
126
127   // Save changes
128   await ui.setNumberDaysSetting('10');
129   await user.click(ui.saveButton.get());
130
131   expect(ui.saveButton.get()).toBeDisabled();
132 });
133
134 it('can set reference branch specific setting', async () => {
135   const { ui, user } = getPageObjects();
136   renderProjectNewCodeDefinitionApp({
137     featureList: [Feature.BranchSupport],
138   });
139   await ui.appIsLoaded();
140
141   expect(await ui.referenceBranchRadio.find()).toHaveClass('disabled');
142   await ui.setReferenceBranchSetting('main');
143   expect(ui.referenceBranchRadio.get()).toBeChecked();
144
145   // Save changes
146   await user.click(ui.saveButton.get());
147
148   expect(ui.saveButton.get()).toBeDisabled();
149 });
150
151 it('cannot set specific analysis setting', async () => {
152   const { ui } = getPageObjects();
153   newCodeDefinitionMock.setListBranchesNewCode([
154     mockNewCodePeriodBranch({
155       branchKey: 'main',
156       type: NewCodeDefinitionType.SpecificAnalysis,
157       value: 'analysis_id',
158     }),
159   ]);
160   renderProjectNewCodeDefinitionApp();
161   await ui.appIsLoaded();
162
163   expect(await ui.specificAnalysisRadio.find()).toBeChecked();
164   expect(ui.specificAnalysisRadio.get()).toHaveClass('disabled');
165   expect(ui.specificAnalysisWarning.get()).toBeInTheDocument();
166
167   await selectEvent.select(ui.analysisFromSelect.get(), 'baseline.branch_analyses.ranges.allTime');
168
169   expect(first(ui.analysisListItem.getAll())).toHaveClass('disabled');
170   expect(ui.saveButton.get()).toBeDisabled();
171 });
172
173 it('renders correctly branch modal', async () => {
174   const { ui } = getPageObjects();
175   renderProjectNewCodeDefinitionApp({
176     featureList: [Feature.BranchSupport],
177   });
178   await ui.appIsLoaded();
179
180   await ui.openBranchSettingModal('main');
181
182   expect(ui.specificAnalysisRadio.query()).not.toBeInTheDocument();
183 });
184
185 it('can set a previous version setting for branch', async () => {
186   const { ui, user } = getPageObjects();
187   renderProjectNewCodeDefinitionApp({
188     featureList: [Feature.BranchSupport],
189   });
190   await ui.appIsLoaded();
191   await ui.setBranchPreviousVersionSetting('main');
192
193   expect(
194     within(byRole('table').get()).getByText('new_code_definition.previous_version'),
195   ).toBeInTheDocument();
196
197   await user.click(await ui.branchActionsButton('main').find());
198
199   expect(ui.resetToDefaultButton.get()).toBeInTheDocument();
200   await user.click(ui.resetToDefaultButton.get());
201
202   expect(
203     first(within(byRole('table').get()).getAllByText('branch_list.default_setting')),
204   ).toBeInTheDocument();
205 });
206
207 it('can set a number of days setting for branch', async () => {
208   const { ui } = getPageObjects();
209   renderProjectNewCodeDefinitionApp({
210     featureList: [Feature.BranchSupport],
211   });
212   await ui.appIsLoaded();
213
214   await ui.setBranchNumberOfDaysSetting('main', '15');
215
216   expect(
217     within(byRole('table').get()).getByText('new_code_definition.number_days: 15'),
218   ).toBeInTheDocument();
219 });
220
221 it('cannot set a specific analysis setting for branch', async () => {
222   const { ui } = getPageObjects();
223   newCodeDefinitionMock.setListBranchesNewCode([
224     mockNewCodePeriodBranch({
225       branchKey: 'main',
226       type: NewCodeDefinitionType.SpecificAnalysis,
227       value: 'analysis_id',
228     }),
229   ]);
230   renderProjectNewCodeDefinitionApp({
231     featureList: [Feature.BranchSupport],
232   });
233   await ui.appIsLoaded();
234
235   await ui.openBranchSettingModal('main');
236
237   expect(ui.specificAnalysisRadio.get()).toBeChecked();
238   expect(ui.specificAnalysisRadio.get()).toHaveClass('disabled');
239   expect(ui.specificAnalysisWarning.get()).toBeInTheDocument();
240
241   await selectEvent.select(ui.analysisFromSelect.get(), 'baseline.branch_analyses.ranges.allTime');
242
243   expect(first(ui.analysisListItem.getAll())).toHaveClass('disabled');
244   expect(last(ui.saveButton.getAll())).toBeDisabled();
245 });
246
247 it('can set a reference branch setting for branch', async () => {
248   const { ui } = getPageObjects();
249   renderProjectNewCodeDefinitionApp({
250     featureList: [Feature.BranchSupport],
251   });
252   await ui.appIsLoaded();
253
254   await ui.setBranchReferenceToBranchSetting('main', 'normal-branch');
255
256   expect(
257     byRole('table').byText('baseline.reference_branch: normal-branch').get(),
258   ).toBeInTheDocument();
259 });
260
261 it('should display NCD banner if some branches had their NCD automatically changed', async () => {
262   const { ui } = getPageObjects();
263
264   newCodeDefinitionMock.setListBranchesNewCode([
265     {
266       projectKey: 'test-project:test',
267       branchKey: 'test-branch',
268       type: NewCodeDefinitionType.NumberOfDays,
269       value: '25',
270       inherited: true,
271       updatedAt: 1692720953662,
272     },
273     {
274       projectKey: 'test-project:test',
275       branchKey: 'master',
276       type: NewCodeDefinitionType.NumberOfDays,
277       value: '32',
278       previousNonCompliantValue: '150',
279       updatedAt: 1692721852743,
280     },
281   ]);
282
283   renderProjectNewCodeDefinitionApp({
284     featureList: [Feature.BranchSupport],
285   });
286
287   expect(await ui.branchNCDsBanner.find()).toBeInTheDocument();
288   expect(
289     ui.branchNCDsBanner.byText('new_code_definition.auto_update.branch.list_itemmaster32150').get(),
290   ).toBeInTheDocument();
291 });
292
293 it('should not display NCD banner if some branches had their NCD automatically changed and banne has been dismissed', async () => {
294   const { ui } = getPageObjects();
295
296   newCodeDefinitionMock.setListBranchesNewCode([
297     {
298       projectKey: 'test-project:test',
299       branchKey: 'test-branch',
300       type: NewCodeDefinitionType.NumberOfDays,
301       value: '25',
302       inherited: true,
303       updatedAt: 1692720953662,
304     },
305     {
306       projectKey: 'test-project:test',
307       branchKey: 'master',
308       type: NewCodeDefinitionType.NumberOfDays,
309       value: '32',
310       previousNonCompliantValue: '150',
311       updatedAt: 1692721852743,
312     },
313   ]);
314   messagesMock.setMessageDismissed({
315     projectKey: 'test-project:test',
316     messageType: MessageTypes.BranchNcd90,
317   });
318
319   renderProjectNewCodeDefinitionApp({
320     featureList: [Feature.BranchSupport],
321   });
322
323   expect(await ui.branchNCDsBanner.query()).not.toBeInTheDocument();
324 });
325
326 it('should correctly dismiss branch banner', async () => {
327   const { ui } = getPageObjects();
328
329   newCodeDefinitionMock.setListBranchesNewCode([
330     {
331       projectKey: 'test-project:test',
332       branchKey: 'test-branch',
333       type: NewCodeDefinitionType.NumberOfDays,
334       value: '25',
335       inherited: true,
336       updatedAt: 1692720953662,
337     },
338     {
339       projectKey: 'test-project:test',
340       branchKey: 'master',
341       type: NewCodeDefinitionType.NumberOfDays,
342       value: '32',
343       previousNonCompliantValue: '150',
344       updatedAt: 1692721852743,
345     },
346   ]);
347
348   renderProjectNewCodeDefinitionApp({
349     featureList: [Feature.BranchSupport],
350   });
351
352   expect(await ui.branchNCDsBanner.find()).toBeInTheDocument();
353
354   const user = userEvent.setup();
355   await act(async () => {
356     await user.click(ui.dismissButton.get());
357   });
358
359   expect(ui.branchNCDsBanner.query()).not.toBeInTheDocument();
360 });
361
362 function renderProjectNewCodeDefinitionApp(context: RenderContext = {}, params?: string) {
363   return renderAppWithComponentContext(
364     'baseline',
365     routes,
366     {
367       ...context,
368       navigateTo: params ? `baseline?id=my-project&${params}` : 'baseline?id=my-project',
369     },
370     {
371       component: mockComponent(),
372     },
373   );
374 }
375
376 function getPageObjects() {
377   const user = userEvent.setup();
378
379   const ui = {
380     pageHeading: byRole('heading', { name: 'project_baseline.page' }),
381     branchTableHeading: byText('branch_list.branch'),
382     branchListHeading: byRole('heading', { name: 'project_baseline.default_setting' }),
383     generalSettingsLink: byRole('link', { name: 'project_baseline.page.description2.link' }),
384     generalSettingRadio: byRole('radio', { name: 'project_baseline.global_setting' }),
385     specificSettingRadio: byRole('radio', { name: 'project_baseline.specific_setting' }),
386     previousVersionRadio: byRole('radio', {
387       name: /new_code_definition.previous_version.description/,
388     }),
389     numberDaysRadio: byRole('radio', { name: /new_code_definition.number_days.description/ }),
390     numberDaysInput: byRole('spinbutton'),
391     referenceBranchRadio: byRole('radio', { name: /baseline.reference_branch.description/ }),
392     chooseBranchSelect: byRole('combobox', { name: 'baseline.reference_branch.choose' }),
393     specificAnalysisRadio: byRole('radio', { name: /baseline.specific_analysis.description/ }),
394     specificAnalysisWarning: byText('baseline.specific_analysis.compliance_warning.title'),
395     analysisFromSelect: byRole('combobox', { name: 'baseline.analysis_from' }),
396     analysisListItem: byRole('radio', { name: /baseline.branch_analyses.analysis_for_x/ }),
397     saveButton: byRole('button', { name: 'save' }),
398     cancelButton: byRole('button', { name: 'cancel' }),
399     branchActionsButton: (branch: string) =>
400       byRole('button', { name: `branch_list.show_actions_for_x.${branch}` }),
401     editButton: byRole('button', { name: 'edit' }),
402     resetToDefaultButton: byRole('button', { name: 'reset_to_default' }),
403     branchNCDsBanner: byText(/new_code_definition.auto_update.branch.message/),
404     dismissButton: byLabelText('alert.dismiss'),
405   };
406
407   async function appIsLoaded() {
408     expect(await ui.pageHeading.find()).toBeInTheDocument();
409   }
410
411   async function setPreviousVersionSetting() {
412     await user.click(ui.specificSettingRadio.get());
413     await user.click(ui.previousVersionRadio.get());
414   }
415
416   async function setBranchPreviousVersionSetting(branch: string) {
417     await openBranchSettingModal(branch);
418     await user.click(last(ui.previousVersionRadio.getAll()) as HTMLElement);
419     await user.click(last(ui.saveButton.getAll()) as HTMLElement);
420   }
421
422   async function setNumberDaysSetting(value: string) {
423     await user.click(ui.specificSettingRadio.get());
424     await user.click(ui.numberDaysRadio.get());
425     await user.clear(ui.numberDaysInput.get());
426     await user.type(ui.numberDaysInput.get(), value);
427   }
428
429   async function setBranchNumberOfDaysSetting(branch: string, value: string) {
430     await openBranchSettingModal(branch);
431     await user.click(last(ui.numberDaysRadio.getAll()) as HTMLElement);
432     await user.clear(ui.numberDaysInput.get());
433     await user.type(ui.numberDaysInput.get(), value);
434     await user.click(last(ui.saveButton.getAll()) as HTMLElement);
435   }
436
437   async function setReferenceBranchSetting(branch: string) {
438     await user.click(ui.specificSettingRadio.get());
439     await user.click(ui.referenceBranchRadio.get());
440     await selectEvent.select(ui.chooseBranchSelect.get(), branch);
441   }
442
443   async function setBranchReferenceToBranchSetting(branch: string, branchRef: string) {
444     await openBranchSettingModal(branch);
445     await user.click(last(ui.referenceBranchRadio.getAll()) as HTMLElement);
446     await selectEvent.select(ui.chooseBranchSelect.get(), branchRef);
447     await user.click(last(ui.saveButton.getAll()) as HTMLElement);
448   }
449
450   async function openBranchSettingModal(branch: string) {
451     await user.click(await ui.branchActionsButton(branch).find());
452     await user.click(ui.editButton.get());
453   }
454
455   return {
456     ui: {
457       ...ui,
458       appIsLoaded,
459       setNumberDaysSetting,
460       setPreviousVersionSetting,
461       setReferenceBranchSetting,
462       setBranchPreviousVersionSetting,
463       setBranchNumberOfDaysSetting,
464       setBranchReferenceToBranchSetting,
465       openBranchSettingModal,
466     },
467     user,
468   };
469 }