]> source.dussan.org Git - sonarqube.git/blob
084e134f393cecdc517a1844e3c1e855b95f601a
[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('prevents selection of global setting if it is not compliant and warns non-admin about it', async () => {
74   newCodeDefinitionMock.setNewCodePeriod({
75     type: NewCodeDefinitionType.NumberOfDays,
76     value: '99',
77     inherited: true,
78   });
79
80   const { ui } = getPageObjects();
81   renderProjectNewCodeDefinitionApp();
82   await ui.appIsLoaded();
83
84   expect(await ui.generalSettingRadio.find()).toBeChecked();
85   expect(ui.generalSettingRadio.get()).toBeDisabled();
86   expect(ui.complianceWarning.get()).toBeVisible();
87 });
88
89 it('prevents selection of global setting if it is not compliant and warns admin about it', async () => {
90   newCodeDefinitionMock.setNewCodePeriod({
91     type: NewCodeDefinitionType.NumberOfDays,
92     value: '99',
93     inherited: true,
94   });
95
96   const { ui } = getPageObjects();
97   renderProjectNewCodeDefinitionApp({ appState: mockAppState({ canAdmin: true }) });
98   await ui.appIsLoaded();
99
100   expect(await ui.generalSettingRadio.find()).toBeChecked();
101   expect(ui.generalSettingRadio.get()).toBeDisabled();
102   expect(ui.complianceWarningAdmin.get()).toBeVisible();
103   expect(ui.complianceWarning.query()).not.toBeInTheDocument();
104 });
105
106 it('renders correctly with branch support feature', async () => {
107   const { ui } = getPageObjects();
108   renderProjectNewCodeDefinitionApp({
109     featureList: [Feature.BranchSupport],
110     appState: mockAppState({ canAdmin: true }),
111   });
112   await ui.appIsLoaded();
113
114   expect(await ui.generalSettingRadio.find()).toBeChecked();
115   expect(ui.specificAnalysisRadio.query()).not.toBeInTheDocument();
116
117   // User is admin
118   expect(ui.generalSettingsLink.get()).toBeInTheDocument();
119
120   // Specific branch setting is rendered with feature support branch
121   expect(ui.branchListHeading.get()).toBeInTheDocument();
122   expect(ui.referenceBranchRadio.get()).toBeInTheDocument();
123 });
124
125 it('can set previous version specific setting', async () => {
126   const { ui, user } = getPageObjects();
127   renderProjectNewCodeDefinitionApp();
128   await ui.appIsLoaded();
129
130   expect(await ui.previousVersionRadio.find()).toHaveClass('disabled');
131   await ui.setPreviousVersionSetting();
132   expect(ui.previousVersionRadio.get()).toBeChecked();
133
134   // Save changes
135   await user.click(ui.saveButton.get());
136
137   expect(ui.saved.get()).toBeInTheDocument();
138
139   // Set general setting
140   await user.click(ui.generalSettingRadio.get());
141   expect(ui.previousVersionRadio.get()).toHaveClass('disabled');
142   await user.click(ui.saveButton.get());
143   expect(ui.saved.get()).toBeInTheDocument();
144 });
145
146 it('can set number of days specific setting', async () => {
147   const { ui, user } = getPageObjects();
148   renderProjectNewCodeDefinitionApp();
149   await ui.appIsLoaded();
150
151   expect(await ui.numberDaysRadio.find()).toHaveClass('disabled');
152   await ui.setNumberDaysSetting('10');
153   expect(ui.numberDaysRadio.get()).toBeChecked();
154
155   // Reset to initial state
156   await user.click(ui.cancelButton.get());
157   expect(ui.generalSettingRadio.get()).toBeChecked();
158   expect(ui.numberDaysRadio.get()).toHaveClass('disabled');
159
160   // Save changes
161   await ui.setNumberDaysSetting('10');
162   await user.click(ui.saveButton.get());
163
164   expect(ui.saved.get()).toBeInTheDocument();
165 });
166
167 it('can set reference branch specific setting', async () => {
168   const { ui, user } = getPageObjects();
169   renderProjectNewCodeDefinitionApp({
170     featureList: [Feature.BranchSupport],
171   });
172   await ui.appIsLoaded();
173
174   expect(await ui.referenceBranchRadio.find()).toHaveClass('disabled');
175   await ui.setReferenceBranchSetting('main');
176   expect(ui.referenceBranchRadio.get()).toBeChecked();
177
178   // Save changes
179   await user.click(ui.saveButton.get());
180
181   expect(ui.saved.get()).toBeInTheDocument();
182 });
183
184 it('cannot set specific analysis setting', async () => {
185   const { ui } = getPageObjects();
186   newCodeDefinitionMock.setNewCodePeriod({
187     type: NewCodeDefinitionType.SpecificAnalysis,
188     value: 'analysis_id',
189   });
190   renderProjectNewCodeDefinitionApp();
191   await ui.appIsLoaded();
192
193   expect(await ui.specificAnalysisRadio.find()).toBeChecked();
194   expect(ui.specificAnalysisRadio.get()).toHaveClass('disabled');
195   expect(ui.specificAnalysisWarning.get()).toBeInTheDocument();
196
197   await selectEvent.select(ui.analysisFromSelect.get(), 'baseline.branch_analyses.ranges.allTime');
198
199   expect(first(ui.analysisListItem.getAll())).toHaveClass('disabled');
200   expect(ui.saveButton.get()).toBeDisabled();
201 });
202
203 it('renders correctly branch modal', async () => {
204   const { ui } = getPageObjects();
205   renderProjectNewCodeDefinitionApp({
206     featureList: [Feature.BranchSupport],
207   });
208   await ui.appIsLoaded();
209
210   await ui.openBranchSettingModal('main');
211
212   expect(ui.specificAnalysisRadio.query()).not.toBeInTheDocument();
213 });
214
215 it('can set a previous version setting for branch', async () => {
216   const { ui, user } = getPageObjects();
217   renderProjectNewCodeDefinitionApp({
218     featureList: [Feature.BranchSupport],
219   });
220   await ui.appIsLoaded();
221   await ui.setBranchPreviousVersionSetting('main');
222
223   expect(
224     within(byRole('table').get()).getByText('new_code_definition.previous_version')
225   ).toBeInTheDocument();
226
227   await user.click(await ui.branchActionsButton('main').find());
228
229   expect(ui.resetToDefaultButton.get()).toBeInTheDocument();
230   await user.click(ui.resetToDefaultButton.get());
231
232   expect(
233     first(within(byRole('table').get()).getAllByText('branch_list.default_setting'))
234   ).toBeInTheDocument();
235 });
236
237 it('can set a number of days setting for branch', async () => {
238   const { ui } = getPageObjects();
239   renderProjectNewCodeDefinitionApp({
240     featureList: [Feature.BranchSupport],
241   });
242   await ui.appIsLoaded();
243
244   await ui.setBranchNumberOfDaysSetting('main', '15');
245
246   expect(
247     within(byRole('table').get()).getByText('new_code_definition.number_days: 15')
248   ).toBeInTheDocument();
249 });
250
251 it('cannot set a specific analysis setting for branch', async () => {
252   const { ui } = getPageObjects();
253   newCodeDefinitionMock.setListBranchesNewCode([
254     mockNewCodePeriodBranch({
255       branchKey: 'main',
256       type: NewCodeDefinitionType.SpecificAnalysis,
257       value: 'analysis_id',
258     }),
259   ]);
260   renderProjectNewCodeDefinitionApp({
261     featureList: [Feature.BranchSupport],
262   });
263   await ui.appIsLoaded();
264
265   await ui.openBranchSettingModal('main');
266
267   expect(ui.specificAnalysisRadio.get()).toBeChecked();
268   expect(ui.specificAnalysisRadio.get()).toHaveClass('disabled');
269   expect(ui.specificAnalysisWarning.get()).toBeInTheDocument();
270
271   await selectEvent.select(ui.analysisFromSelect.get(), 'baseline.branch_analyses.ranges.allTime');
272
273   expect(first(ui.analysisListItem.getAll())).toHaveClass('disabled');
274   expect(last(ui.saveButton.getAll())).toBeDisabled();
275 });
276
277 it('can set a reference branch setting for branch', async () => {
278   const { ui } = getPageObjects();
279   renderProjectNewCodeDefinitionApp({
280     featureList: [Feature.BranchSupport],
281   });
282   await ui.appIsLoaded();
283
284   await ui.setBranchReferenceToBranchSetting('main', 'normal-branch');
285
286   expect(
287     byRole('table').byText('baseline.reference_branch: normal-branch').get()
288   ).toBeInTheDocument();
289 });
290
291 it('should display NCD banner if some branches had their NCD automatically changed', async () => {
292   const { ui } = getPageObjects();
293
294   newCodeDefinitionMock.setListBranchesNewCode([
295     {
296       projectKey: 'test-project:test',
297       branchKey: 'test-branch',
298       type: NewCodeDefinitionType.NumberOfDays,
299       value: '25',
300       inherited: true,
301       updatedAt: 1692720953662,
302     },
303     {
304       projectKey: 'test-project:test',
305       branchKey: 'master',
306       type: NewCodeDefinitionType.NumberOfDays,
307       value: '32',
308       previousNonCompliantValue: '150',
309       updatedAt: 1692721852743,
310     },
311   ]);
312
313   renderProjectNewCodeDefinitionApp({
314     featureList: [Feature.BranchSupport],
315   });
316
317   expect(await ui.branchNCDsBanner.find()).toBeInTheDocument();
318   expect(
319     ui.branchNCDsBanner.byText('new_code_definition.auto_update.branch.list_itemmaster32150').get()
320   ).toBeInTheDocument();
321 });
322
323 it('should not display NCD banner if some branches had their NCD automatically changed and banne has been dismissed', async () => {
324   const { ui } = getPageObjects();
325
326   newCodeDefinitionMock.setListBranchesNewCode([
327     {
328       projectKey: 'test-project:test',
329       branchKey: 'test-branch',
330       type: NewCodeDefinitionType.NumberOfDays,
331       value: '25',
332       inherited: true,
333       updatedAt: 1692720953662,
334     },
335     {
336       projectKey: 'test-project:test',
337       branchKey: 'master',
338       type: NewCodeDefinitionType.NumberOfDays,
339       value: '32',
340       previousNonCompliantValue: '150',
341       updatedAt: 1692721852743,
342     },
343   ]);
344   messagesMock.setMessageDismissed({
345     projectKey: 'test-project:test',
346     messageType: MessageTypes.BranchNcd90,
347   });
348
349   renderProjectNewCodeDefinitionApp({
350     featureList: [Feature.BranchSupport],
351   });
352
353   expect(await ui.branchNCDsBanner.query()).not.toBeInTheDocument();
354 });
355
356 it('should correctly dismiss branch banner', async () => {
357   const { ui } = getPageObjects();
358
359   newCodeDefinitionMock.setListBranchesNewCode([
360     {
361       projectKey: 'test-project:test',
362       branchKey: 'test-branch',
363       type: NewCodeDefinitionType.NumberOfDays,
364       value: '25',
365       inherited: true,
366       updatedAt: 1692720953662,
367     },
368     {
369       projectKey: 'test-project:test',
370       branchKey: 'master',
371       type: NewCodeDefinitionType.NumberOfDays,
372       value: '32',
373       previousNonCompliantValue: '150',
374       updatedAt: 1692721852743,
375     },
376   ]);
377
378   renderProjectNewCodeDefinitionApp({
379     featureList: [Feature.BranchSupport],
380   });
381
382   expect(await ui.branchNCDsBanner.find()).toBeInTheDocument();
383
384   const user = userEvent.setup();
385   await act(async () => {
386     await user.click(ui.dismissButton.get());
387   });
388
389   expect(ui.branchNCDsBanner.query()).not.toBeInTheDocument();
390 });
391
392 function renderProjectNewCodeDefinitionApp(context: RenderContext = {}, params?: string) {
393   return renderAppWithComponentContext(
394     'baseline',
395     routes,
396     {
397       ...context,
398       navigateTo: params ? `baseline?id=my-project&${params}` : 'baseline?id=my-project',
399     },
400     {
401       component: mockComponent(),
402     }
403   );
404 }
405
406 function getPageObjects() {
407   const user = userEvent.setup();
408
409   const ui = {
410     pageHeading: byRole('heading', { name: 'project_baseline.page' }),
411     branchTableHeading: byText('branch_list.branch'),
412     branchListHeading: byRole('heading', { name: 'project_baseline.default_setting' }),
413     generalSettingsLink: byRole('link', { name: 'project_baseline.page.description2.link' }),
414     generalSettingRadio: byRole('radio', { name: 'project_baseline.global_setting' }),
415     specificSettingRadio: byRole('radio', { name: 'project_baseline.specific_setting' }),
416     previousVersionRadio: byRole('radio', {
417       name: /new_code_definition.previous_version.description/,
418     }),
419     numberDaysRadio: byRole('radio', { name: /new_code_definition.number_days.description/ }),
420     numberDaysInput: byRole('spinbutton'),
421     referenceBranchRadio: byRole('radio', { name: /baseline.reference_branch.description/ }),
422     chooseBranchSelect: byRole('combobox', { name: 'baseline.reference_branch.choose' }),
423     specificAnalysisRadio: byRole('radio', { name: /baseline.specific_analysis.description/ }),
424     specificAnalysisWarning: byText('baseline.specific_analysis.compliance_warning.title'),
425     analysisFromSelect: byRole('combobox', { name: 'baseline.analysis_from' }),
426     analysisListItem: byRole('radio', { name: /baseline.branch_analyses.analysis_for_x/ }),
427     saveButton: byRole('button', { name: 'save' }),
428     cancelButton: byRole('button', { name: 'cancel' }),
429     branchActionsButton: (branch: string) =>
430       byRole('button', { name: `branch_list.show_actions_for_x.${branch}` }),
431     editButton: byRole('button', { name: 'edit' }),
432     resetToDefaultButton: byRole('button', { name: 'reset_to_default' }),
433     saved: byText('settings.state.saved'),
434     complianceWarningAdmin: byText('new_code_definition.compliance.warning.explanation.admin'),
435     complianceWarning: byText('new_code_definition.compliance.warning.explanation'),
436     branchNCDsBanner: byText(/new_code_definition.auto_update.branch.message/),
437     dismissButton: byLabelText('alert.dismiss'),
438   };
439
440   async function appIsLoaded() {
441     expect(await ui.pageHeading.find()).toBeInTheDocument();
442   }
443
444   async function setPreviousVersionSetting() {
445     await user.click(ui.specificSettingRadio.get());
446     await user.click(ui.previousVersionRadio.get());
447   }
448
449   async function setBranchPreviousVersionSetting(branch: string) {
450     await openBranchSettingModal(branch);
451     await user.click(last(ui.previousVersionRadio.getAll()) as HTMLElement);
452     await user.click(last(ui.saveButton.getAll()) as HTMLElement);
453   }
454
455   async function setNumberDaysSetting(value: string) {
456     await user.click(ui.specificSettingRadio.get());
457     await user.click(ui.numberDaysRadio.get());
458     await user.clear(ui.numberDaysInput.get());
459     await user.type(ui.numberDaysInput.get(), value);
460   }
461
462   async function setBranchNumberOfDaysSetting(branch: string, value: string) {
463     await openBranchSettingModal(branch);
464     await user.click(last(ui.numberDaysRadio.getAll()) as HTMLElement);
465     await user.clear(ui.numberDaysInput.get());
466     await user.type(ui.numberDaysInput.get(), value);
467     await user.click(last(ui.saveButton.getAll()) as HTMLElement);
468   }
469
470   async function setReferenceBranchSetting(branch: string) {
471     await user.click(ui.specificSettingRadio.get());
472     await user.click(ui.referenceBranchRadio.get());
473     await selectEvent.select(ui.chooseBranchSelect.get(), branch);
474   }
475
476   async function setBranchReferenceToBranchSetting(branch: string, branchRef: string) {
477     await openBranchSettingModal(branch);
478     await user.click(last(ui.referenceBranchRadio.getAll()) as HTMLElement);
479     await selectEvent.select(ui.chooseBranchSelect.get(), branchRef);
480     await user.click(last(ui.saveButton.getAll()) as HTMLElement);
481   }
482
483   async function openBranchSettingModal(branch: string) {
484     await user.click(await ui.branchActionsButton(branch).find());
485     await user.click(ui.editButton.get());
486   }
487
488   return {
489     ui: {
490       ...ui,
491       appIsLoaded,
492       setNumberDaysSetting,
493       setPreviousVersionSetting,
494       setReferenceBranchSetting,
495       setBranchPreviousVersionSetting,
496       setBranchNumberOfDaysSetting,
497       setBranchReferenceToBranchSetting,
498       openBranchSettingModal,
499     },
500     user,
501   };
502 }