From: stanislavh Date: Fri, 3 Mar 2023 12:48:04 +0000 (+0100) Subject: SONAR-18593 Migrate rtl for settings X-Git-Tag: 10.0.0.68432~170 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=2ef9e45cf45c5fb14c61981f880249eb36dc0029;p=sonarqube.git SONAR-18593 Migrate rtl for settings --- diff --git a/server/sonar-web/src/main/js/api/mocks/NewCodePeriodsServiceMock.ts b/server/sonar-web/src/main/js/api/mocks/NewCodePeriodsServiceMock.ts new file mode 100644 index 00000000000..df8b3b6ba57 --- /dev/null +++ b/server/sonar-web/src/main/js/api/mocks/NewCodePeriodsServiceMock.ts @@ -0,0 +1,60 @@ +/* + * 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 { cloneDeep } from 'lodash'; +import { mockNewCodePeriod } from '../../helpers/mocks/new-code-period'; +import { NewCodePeriod, NewCodePeriodSettingType } from '../../types/types'; +import { getNewCodePeriod, setNewCodePeriod } from '../newCodePeriod'; + +jest.mock('../newCodePeriod'); + +const defaultNewCodePeriod = mockNewCodePeriod(); + +export default class NewCodePeriodsServiceMock { + #newCodePeriod: NewCodePeriod; + + constructor() { + this.#newCodePeriod = cloneDeep(defaultNewCodePeriod); + jest.mocked(getNewCodePeriod).mockImplementation(this.handleGetNewCodePeriod); + jest.mocked(setNewCodePeriod).mockImplementation(this.handleSetNewCodePeriod); + } + + handleGetNewCodePeriod = () => { + return this.reply(this.#newCodePeriod); + }; + + handleSetNewCodePeriod = (data: { + project?: string; + branch?: string; + type: NewCodePeriodSettingType; + value?: string; + }) => { + const { type, value } = data; + this.#newCodePeriod = mockNewCodePeriod({ type, value }); + return this.reply(undefined); + }; + + reset = () => { + this.#newCodePeriod = cloneDeep(defaultNewCodePeriod); + }; + + reply(response: T): Promise { + return Promise.resolve(cloneDeep(response)); + } +} diff --git a/server/sonar-web/src/main/js/api/mocks/SettingsServiceMock.ts b/server/sonar-web/src/main/js/api/mocks/SettingsServiceMock.ts index abfd964dcb8..b22051af84a 100644 --- a/server/sonar-web/src/main/js/api/mocks/SettingsServiceMock.ts +++ b/server/sonar-web/src/main/js/api/mocks/SettingsServiceMock.ts @@ -17,9 +17,9 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { cloneDeep } from 'lodash'; +import { cloneDeep, isArray, isObject, isString } from 'lodash'; import { HousekeepingPolicy } from '../../apps/audit-logs/utils'; -import { mockDefinition } from '../../helpers/mocks/settings'; +import { mockDefinition, mockSettingFieldDefinition } from '../../helpers/mocks/settings'; import { BranchParameters } from '../../types/branch-like'; import { ExtendedSettingDefinition, @@ -37,7 +37,8 @@ import { setSettingValue, } from '../settings'; -const isEmptyString = (i: string) => i.trim() === ''; +const isEmptyField = (o: any) => isObject(o) && Object.values(o).some(isEmptyString); +const isEmptyString = (i: any) => isString(i) && i.trim() === ''; export const DEFAULT_DEFINITIONS_MOCK = [ mockDefinition({ @@ -46,7 +47,6 @@ export const DEFAULT_DEFINITIONS_MOCK = [ subCategory: 'Announcement', name: 'Announcement message', description: 'Enter message', - defaultValue: '.js,.jsx,.cjs,.vue,.mjs', type: SettingType.TEXT, }), mockDefinition({ @@ -55,7 +55,6 @@ export const DEFAULT_DEFINITIONS_MOCK = [ subCategory: 'Compute Engine', name: 'Run analysis in paralel', description: 'Enter message', - defaultValue: '.js,.jsx,.cjs,.vue,.mjs', type: SettingType.TEXT, }), mockDefinition({ @@ -84,6 +83,18 @@ export const DEFAULT_DEFINITIONS_MOCK = [ description: 'Paths to xml files', multiValues: true, }), + mockDefinition({ + category: 'COBOL', + key: 'sonar.cobol.compilationConstants', + subCategory: 'Preprocessor', + name: 'Compilation Constants', + description: 'Lets do it', + type: SettingType.PROPERTY_SET, + fields: [ + mockSettingFieldDefinition(), + mockSettingFieldDefinition({ key: 'value', name: 'Value' }), + ], + }), ]; export default class SettingsServiceMock { @@ -131,8 +142,9 @@ export default class SettingsServiceMock { handleSetSettingValue = (definition: SettingDefinition, value: any): Promise => { if ( - (typeof value === 'string' && isEmptyString(value)) || - (value instanceof Array && value.some(isEmptyString)) + isEmptyString(value) || + (isArray(value) && value.some(isEmptyString)) || + isEmptyField(value) ) { throw new ResponseError('validation error', { errors: [{ msg: 'A non empty value must be provided' }], @@ -149,7 +161,9 @@ export default class SettingsServiceMock { const definition = this.#definitions.find( (d) => d.key === data.keys ) as ExtendedSettingDefinition; - if (definition.multiValues === true) { + if (definition.type === SettingType.PROPERTY_SET) { + setting.fieldValues = []; + } else if (definition.multiValues === true) { setting.values = definition.defaultValue?.split(',') ?? []; } else { setting.value = definition.defaultValue ?? ''; @@ -168,12 +182,17 @@ export default class SettingsServiceMock { if (setting) { setting.value = value; setting.values = value; + setting.fieldValues = value; } else { - this.#settingValues.push({ key, value }); + this.#settingValues.push({ key, value, values: value, fieldValues: value }); } return this; }; + setDefinition = (definition: ExtendedSettingDefinition) => { + this.#definitions.push(definition); + }; + reset = () => { this.#settingValues = cloneDeep(this.#defaultValues); this.#definitions = cloneDeep(DEFAULT_DEFINITIONS_MOCK); diff --git a/server/sonar-web/src/main/js/apps/account/__tests__/Account-it.tsx b/server/sonar-web/src/main/js/apps/account/__tests__/Account-it.tsx index 02ec79d5a74..a740d347879 100644 --- a/server/sonar-web/src/main/js/apps/account/__tests__/Account-it.tsx +++ b/server/sonar-web/src/main/js/apps/account/__tests__/Account-it.tsx @@ -236,7 +236,7 @@ describe('profile page', () => { const user = userEvent.setup(); renderAccountApp(mockLoggedInUser()); - const toggle = screen.getByRole('button', { + const toggle = screen.getByRole('switch', { name: 'my_account.preferences.keyboard_shortcuts.enabled', }); expect(toggle).toBeInTheDocument(); diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/LeakPeriodLegend.tsx b/server/sonar-web/src/main/js/apps/component-measures/components/LeakPeriodLegend.tsx index 812d7f22130..f6f5acb46a5 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/LeakPeriodLegend.tsx +++ b/server/sonar-web/src/main/js/apps/component-measures/components/LeakPeriodLegend.tsx @@ -27,7 +27,7 @@ import DateFromNow from '../../../components/intl/DateFromNow'; import DateTimeFormatter, { formatterOption } from '../../../components/intl/DateTimeFormatter'; import { translate, translateWithParameters } from '../../../helpers/l10n'; import { getPeriodDate, getPeriodLabel } from '../../../helpers/periods'; -import { ComponentMeasure, Period } from '../../../types/types'; +import { ComponentMeasure, NewCodePeriodSettingType, Period } from '../../../types/types'; interface Props { className?: string; @@ -65,7 +65,7 @@ export class LeakPeriodLegend extends React.PureComponent ); - if (period.mode === 'days' || period.mode === 'NUMBER_OF_DAYS') { + if (period.mode === 'days' || period.mode === NewCodePeriodSettingType.NUMBER_OF_DAYS) { return label; } diff --git a/server/sonar-web/src/main/js/apps/overview/branches/MeasuresPanelNoNewCode.tsx b/server/sonar-web/src/main/js/apps/overview/branches/MeasuresPanelNoNewCode.tsx index e75f4fe0a6a..37d8c2ac37f 100644 --- a/server/sonar-web/src/main/js/apps/overview/branches/MeasuresPanelNoNewCode.tsx +++ b/server/sonar-web/src/main/js/apps/overview/branches/MeasuresPanelNoNewCode.tsx @@ -27,7 +27,7 @@ import { getBaseUrl } from '../../../helpers/system'; import { queryToSearch } from '../../../helpers/urls'; import { Branch } from '../../../types/branch-like'; import { ComponentQualifier } from '../../../types/component'; -import { Component, Period } from '../../../types/types'; +import { Component, NewCodePeriodSettingType, Period } from '../../../types/types'; export interface MeasuresPanelNoNewCodeProps { branch?: Branch; @@ -41,7 +41,7 @@ export default function MeasuresPanelNoNewCode(props: MeasuresPanelNoNewCodeProp const isApp = component.qualifier === ComponentQualifier.Application; const hasBadReferenceBranch = - !isApp && !!period && !period.date && period.mode === 'REFERENCE_BRANCH'; + !isApp && !!period && !period.date && period.mode === NewCodePeriodSettingType.REFERENCE_BRANCH; /* * If the period is "reference branch"-based, and if there's no date, it means * that we're not lacking a second analysis, but that we'll never have new code because the diff --git a/server/sonar-web/src/main/js/apps/overview/branches/ProjectLeakPeriodInfo.tsx b/server/sonar-web/src/main/js/apps/overview/branches/ProjectLeakPeriodInfo.tsx index 51eadd8dc84..ec76ddfafde 100644 --- a/server/sonar-web/src/main/js/apps/overview/branches/ProjectLeakPeriodInfo.tsx +++ b/server/sonar-web/src/main/js/apps/overview/branches/ProjectLeakPeriodInfo.tsx @@ -24,7 +24,7 @@ import DateFromNow from '../../../components/intl/DateFromNow'; import { formatterOption } from '../../../components/intl/DateTimeFormatter'; import { translateWithParameters } from '../../../helpers/l10n'; import { getPeriodDate, getPeriodLabel } from '../../../helpers/periods'; -import { Period } from '../../../types/types'; +import { NewCodePeriodSettingType, Period } from '../../../types/types'; export interface ProjectLeakPeriodInfoProps extends WrappedComponentProps { leakPeriod: Period; @@ -38,7 +38,7 @@ export function ProjectLeakPeriodInfo(props: ProjectLeakPeriodInfoProps) { const leakPeriodLabel = getPeriodLabel( leakPeriod, - ['manual_baseline', 'SPECIFIC_ANALYSIS'].includes(leakPeriod.mode) + ['manual_baseline', NewCodePeriodSettingType.SPECIFIC_ANALYSIS].includes(leakPeriod.mode) ? (date: string) => formatTime(date, formatterOption) : (date: string) => formatDate(date, longFormatterOption) ); @@ -49,8 +49,8 @@ export function ProjectLeakPeriodInfo(props: ProjectLeakPeriodInfoProps) { if ( leakPeriod.mode === 'days' || - leakPeriod.mode === 'NUMBER_OF_DAYS' || - leakPeriod.mode === 'REFERENCE_BRANCH' + leakPeriod.mode === NewCodePeriodSettingType.NUMBER_OF_DAYS || + leakPeriod.mode === NewCodePeriodSettingType.REFERENCE_BRANCH ) { return
{leakPeriodLabel}
; } diff --git a/server/sonar-web/src/main/js/apps/overview/branches/__tests__/ProjectLeakPeriodInfo-test.tsx b/server/sonar-web/src/main/js/apps/overview/branches/__tests__/ProjectLeakPeriodInfo-test.tsx index fe635fe2114..516922ab183 100644 --- a/server/sonar-web/src/main/js/apps/overview/branches/__tests__/ProjectLeakPeriodInfo-test.tsx +++ b/server/sonar-web/src/main/js/apps/overview/branches/__tests__/ProjectLeakPeriodInfo-test.tsx @@ -23,7 +23,7 @@ import * as React from 'react'; import { IntlShape } from 'react-intl'; import { mockPeriod } from '../../../../helpers/testMocks'; import { renderComponent } from '../../../../helpers/testReactTestingUtils'; -import { Period } from '../../../../types/types'; +import { NewCodePeriodSettingType, Period } from '../../../../types/types'; import { ProjectLeakPeriodInfo } from '../ProjectLeakPeriodInfo'; jest.mock('date-fns', () => { @@ -62,7 +62,7 @@ it('should render correctly for "previous_analysis"', async () => { it('should render correctly for "REFERENCE_BRANCH"', async () => { renderProjectLeakPeriodInfo({ - mode: 'REFERENCE_BRANCH', + mode: NewCodePeriodSettingType.REFERENCE_BRANCH, parameter: 'master', }); expect(await screen.findByText('overview.period.reference_branch.master')).toBeInTheDocument(); diff --git a/server/sonar-web/src/main/js/apps/overview/components/LeakPeriodLegend.tsx b/server/sonar-web/src/main/js/apps/overview/components/LeakPeriodLegend.tsx index 86c20c726a5..43fe036e6be 100644 --- a/server/sonar-web/src/main/js/apps/overview/components/LeakPeriodLegend.tsx +++ b/server/sonar-web/src/main/js/apps/overview/components/LeakPeriodLegend.tsx @@ -26,7 +26,7 @@ import DateFromNow from '../../../components/intl/DateFromNow'; import DateTimeFormatter, { formatterOption } from '../../../components/intl/DateTimeFormatter'; import { translateWithParameters } from '../../../helpers/l10n'; import { getPeriodDate, getPeriodLabel } from '../../../helpers/periods'; -import { Dict, Period } from '../../../types/types'; +import { Dict, NewCodePeriodSettingType, Period } from '../../../types/types'; interface Props { period: Period; @@ -56,7 +56,7 @@ export class LeakPeriodLegend extends React.PureComponent {translateWithParameters('overview.new_code_period_x', leakPeriodLabel)} diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/App.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/components/App.tsx deleted file mode 100644 index 80dbcf8f0eb..00000000000 --- a/server/sonar-web/src/main/js/apps/projectBaseline/components/App.tsx +++ /dev/null @@ -1,325 +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 classNames from 'classnames'; -import { debounce } from 'lodash'; -import * as React from 'react'; -import { getNewCodePeriod, resetNewCodePeriod, setNewCodePeriod } from '../../../api/newCodePeriod'; -import withAppStateContext from '../../../app/components/app-state/withAppStateContext'; -import withAvailableFeatures, { - WithAvailableFeaturesProps, -} from '../../../app/components/available-features/withAvailableFeatures'; -import withComponentContext from '../../../app/components/componentContext/withComponentContext'; -import Suggestions from '../../../components/embed-docs-modal/Suggestions'; -import AlertSuccessIcon from '../../../components/icons/AlertSuccessIcon'; -import DeferredSpinner from '../../../components/ui/DeferredSpinner'; -import { isBranch, sortBranches } from '../../../helpers/branch-like'; -import { translate } from '../../../helpers/l10n'; -import { AppState } from '../../../types/appstate'; -import { Branch, BranchLike } from '../../../types/branch-like'; -import { Feature } from '../../../types/features'; -import { ParsedAnalysis } from '../../../types/project-activity'; -import { Component, NewCodePeriod, NewCodePeriodSettingType } from '../../../types/types'; -import '../styles.css'; -import { getSettingValue } from '../utils'; -import AppHeader from './AppHeader'; -import BranchList from './BranchList'; -import ProjectBaselineSelector from './ProjectBaselineSelector'; - -interface Props extends WithAvailableFeaturesProps { - branchLike: Branch; - branchLikes: BranchLike[]; - component: Component; - appState: AppState; -} - -interface State { - analysis?: string; - branchList: Branch[]; - currentSetting?: NewCodePeriodSettingType; - currentSettingValue?: string; - days: string; - generalSetting?: NewCodePeriod; - loading: boolean; - overrideGeneralSetting?: boolean; - referenceBranch?: string; - saving: boolean; - selected?: NewCodePeriodSettingType; - success?: boolean; -} - -const DEFAULT_NUMBER_OF_DAYS = '30'; - -const DEFAULT_GENERAL_SETTING: { type: NewCodePeriodSettingType } = { - type: 'PREVIOUS_VERSION', -}; - -export class App extends React.PureComponent { - mounted = false; - state: State = { - branchList: [], - days: DEFAULT_NUMBER_OF_DAYS, - loading: true, - saving: false, - }; - - // We use debounce as we could have multiple save in less that 3sec. - resetSuccess = debounce(() => this.setState({ success: undefined }), 3000); - - componentDidMount() { - this.mounted = true; - this.fetchLeakPeriodSetting(); - this.sortAndFilterBranches(this.props.branchLikes); - } - - componentDidUpdate(prevProps: Props) { - if (prevProps.branchLikes !== this.props.branchLikes) { - this.sortAndFilterBranches(this.props.branchLikes); - } - } - - componentWillUnmount() { - this.mounted = false; - } - - getUpdatedState(params: { - currentSetting?: NewCodePeriodSettingType; - currentSettingValue?: string; - generalSetting: NewCodePeriod; - }) { - const { currentSetting, currentSettingValue, generalSetting } = params; - const { referenceBranch } = this.state; - - const defaultDays = - (generalSetting.type === 'NUMBER_OF_DAYS' && generalSetting.value) || DEFAULT_NUMBER_OF_DAYS; - - return { - loading: false, - currentSetting, - currentSettingValue, - generalSetting, - selected: currentSetting || generalSetting.type, - overrideGeneralSetting: Boolean(currentSetting), - days: (currentSetting === 'NUMBER_OF_DAYS' && currentSettingValue) || defaultDays, - analysis: (currentSetting === 'SPECIFIC_ANALYSIS' && currentSettingValue) || '', - referenceBranch: - (currentSetting === 'REFERENCE_BRANCH' && currentSettingValue) || referenceBranch, - }; - } - - sortAndFilterBranches(branchLikes: BranchLike[] = []) { - const branchList = sortBranches(branchLikes.filter(isBranch)); - this.setState({ branchList, referenceBranch: branchList[0].name }); - } - - fetchLeakPeriodSetting() { - const { branchLike, component } = this.props; - - this.setState({ loading: true }); - - Promise.all([ - getNewCodePeriod(), - getNewCodePeriod({ - branch: this.props.hasFeature(Feature.BranchSupport) ? undefined : branchLike.name, - project: component.key, - }), - ]).then( - ([generalSetting, setting]) => { - if (this.mounted) { - if (!generalSetting.type) { - generalSetting = DEFAULT_GENERAL_SETTING; - } - const currentSettingValue = setting.value; - const currentSetting = setting.inherited ? undefined : setting.type || 'PREVIOUS_VERSION'; - - this.setState( - this.getUpdatedState({ - generalSetting, - currentSetting, - currentSettingValue, - }) - ); - } - }, - () => { - this.setState({ loading: false }); - } - ); - } - - resetSetting = () => { - this.setState({ saving: true }); - resetNewCodePeriod({ project: this.props.component.key }).then( - () => { - this.setState({ - saving: false, - currentSetting: undefined, - selected: undefined, - success: true, - }); - this.resetSuccess(); - }, - () => { - this.setState({ saving: false }); - } - ); - }; - - handleSelectAnalysis = (analysis: ParsedAnalysis) => this.setState({ analysis: analysis.key }); - - handleSelectDays = (days: string) => this.setState({ days }); - - handleSelectReferenceBranch = (referenceBranch: string) => { - this.setState({ referenceBranch }); - }; - - handleCancel = () => - this.setState( - ({ generalSetting = DEFAULT_GENERAL_SETTING, currentSetting, currentSettingValue }) => - this.getUpdatedState({ generalSetting, currentSetting, currentSettingValue }) - ); - - handleSelectSetting = (selected?: NewCodePeriodSettingType) => this.setState({ selected }); - - handleToggleSpecificSetting = (overrideGeneralSetting: boolean) => - this.setState({ overrideGeneralSetting }); - - handleSubmit = (e: React.SyntheticEvent) => { - e.preventDefault(); - - const { component } = this.props; - const { analysis, days, selected: type, referenceBranch, overrideGeneralSetting } = this.state; - - if (!overrideGeneralSetting) { - this.resetSetting(); - return; - } - - const value = getSettingValue({ type, analysis, days, referenceBranch }); - - if (type) { - this.setState({ saving: true }); - setNewCodePeriod({ - project: component.key, - type, - value, - }).then( - () => { - this.setState({ - saving: false, - currentSetting: type, - currentSettingValue: value || undefined, - success: true, - }); - this.resetSuccess(); - }, - () => { - this.setState({ saving: false }); - } - ); - } - }; - - render() { - const { appState, component, branchLike } = this.props; - const { - analysis, - branchList, - currentSetting, - days, - generalSetting, - loading, - currentSettingValue, - overrideGeneralSetting, - referenceBranch, - saving, - selected, - success, - } = this.state; - const branchSupportEnabled = this.props.hasFeature(Feature.BranchSupport); - - return ( - <> - -
- - {loading ? ( - - ) : ( -
- {branchSupportEnabled &&

{translate('project_baseline.default_setting')}

} - - {generalSetting && overrideGeneralSetting !== undefined && ( - - )} - -
- - - {translate('settings.state.saved')} - -
- {generalSetting && branchSupportEnabled && ( -
-
-

{translate('project_baseline.configure_branches')}

- -
- )} -
- )} -
- - ); - } -} - -export default withComponentContext(withAvailableFeatures(withAppStateContext(App))); diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/BaselineSettingAnalysis.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/components/BaselineSettingAnalysis.tsx index f31cd308728..0528174c70c 100644 --- a/server/sonar-web/src/main/js/apps/projectBaseline/components/BaselineSettingAnalysis.tsx +++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/BaselineSettingAnalysis.tsx @@ -32,7 +32,7 @@ export default function BaselineSettingAnalysis({ disabled, onSelect, selected } return ( onSelect('SPECIFIC_ANALYSIS')} + onClick={() => onSelect(NewCodePeriodSettingType.SPECIFIC_ANALYSIS)} selected={selected} title={translate('baseline.specific_analysis')} > diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/BaselineSettingDays.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/components/BaselineSettingDays.tsx index 0f356bfd880..ef42354448d 100644 --- a/server/sonar-web/src/main/js/apps/projectBaseline/components/BaselineSettingDays.tsx +++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/BaselineSettingDays.tsx @@ -41,7 +41,7 @@ export default function BaselineSettingDays(props: Props) { onSelect('NUMBER_OF_DAYS')} + onClick={() => onSelect(NewCodePeriodSettingType.NUMBER_OF_DAYS)} selected={selected} title={translate('baseline.number_days')} > diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/BaselineSettingPreviousVersion.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/components/BaselineSettingPreviousVersion.tsx index 5084aa5a124..e0ccc8487f9 100644 --- a/server/sonar-web/src/main/js/apps/projectBaseline/components/BaselineSettingPreviousVersion.tsx +++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/BaselineSettingPreviousVersion.tsx @@ -34,7 +34,7 @@ export default function BaselineSettingPreviousVersion(props: Props) { return ( onSelect('PREVIOUS_VERSION')} + onClick={() => onSelect(NewCodePeriodSettingType.PREVIOUS_VERSION)} selected={selected} title={ translate('baseline.previous_version') + (isDefault ? ` (${translate('default')})` : '') diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/BaselineSettingReferenceBranch.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/components/BaselineSettingReferenceBranch.tsx index 35600e31718..6604d1e39da 100644 --- a/server/sonar-web/src/main/js/apps/projectBaseline/components/BaselineSettingReferenceBranch.tsx +++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/BaselineSettingReferenceBranch.tsx @@ -98,7 +98,7 @@ export default function BaselineSettingReferenceBranch(props: BaselineSettingRef props.onSelect('REFERENCE_BRANCH')} + onClick={() => props.onSelect(NewCodePeriodSettingType.REFERENCE_BRANCH)} selected={selected} title={translate('baseline.reference_branch')} > diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/BranchBaselineSettingModal.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/components/BranchBaselineSettingModal.tsx index a9236fe447e..0a3ec852329 100644 --- a/server/sonar-web/src/main/js/apps/projectBaseline/components/BranchBaselineSettingModal.tsx +++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/BranchBaselineSettingModal.tsx @@ -60,9 +60,10 @@ export default class BranchBaselineSettingModal extends React.PureComponent 0 ? otherBranches[0].name : ''; this.state = { - analysis: this.getValueFromProps('SPECIFIC_ANALYSIS') || '', - days: this.getValueFromProps('NUMBER_OF_DAYS') || '30', - referenceBranch: this.getValueFromProps('REFERENCE_BRANCH') || defaultBranch, + analysis: this.getValueFromProps(NewCodePeriodSettingType.SPECIFIC_ANALYSIS) || '', + days: this.getValueFromProps(NewCodePeriodSettingType.NUMBER_OF_DAYS) || '30', + referenceBranch: + this.getValueFromProps(NewCodePeriodSettingType.REFERENCE_BRANCH) || defaultBranch, saving: false, selected: this.props.branch.newCodePeriod && this.props.branch.newCodePeriod.type, }; @@ -164,7 +165,7 @@ export default class BranchBaselineSettingModal extends React.PureComponent - {selected === 'SPECIFIC_ANALYSIS' && ( + {selected === NewCodePeriodSettingType.SPECIFIC_ANALYSIS && ( { if (!newCodePeriod) { return b; } - const { type = 'PREVIOUS_VERSION', value, effectiveValue } = newCodePeriod; + const { type = DEFAULT_GENERAL_SETTING_TYPE, value, effectiveValue } = newCodePeriod; return { ...b, newCodePeriod: { type, value, effectiveValue }, diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/BranchListRow.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/components/BranchListRow.tsx index 9ed131753c9..ff07fee94dd 100644 --- a/server/sonar-web/src/main/js/apps/projectBaseline/components/BranchListRow.tsx +++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/BranchListRow.tsx @@ -25,7 +25,7 @@ import WarningIcon from '../../../components/icons/WarningIcon'; import DateTimeFormatter from '../../../components/intl/DateTimeFormatter'; import { translate, translateWithParameters } from '../../../helpers/l10n'; import { BranchWithNewCodePeriod } from '../../../types/branch-like'; -import { NewCodePeriod } from '../../../types/types'; +import { NewCodePeriod, NewCodePeriodSettingType } from '../../../types/types'; export interface BranchListRowProps { branch: BranchWithNewCodePeriod; @@ -37,7 +37,7 @@ export interface BranchListRowProps { function renderNewCodePeriodSetting(newCodePeriod: NewCodePeriod) { switch (newCodePeriod.type) { - case 'SPECIFIC_ANALYSIS': + case NewCodePeriodSettingType.SPECIFIC_ANALYSIS: return ( <> {`${translate('baseline.specific_analysis')}: `} @@ -48,11 +48,11 @@ function renderNewCodePeriodSetting(newCodePeriod: NewCodePeriod) { )} ); - case 'NUMBER_OF_DAYS': + case NewCodePeriodSettingType.NUMBER_OF_DAYS: return `${translate('baseline.number_days')}: ${newCodePeriod.value}`; - case 'PREVIOUS_VERSION': + case NewCodePeriodSettingType.PREVIOUS_VERSION: return translate('baseline.previous_version'); - case 'REFERENCE_BRANCH': + case NewCodePeriodSettingType.REFERENCE_BRANCH: return `${translate('baseline.reference_branch')}: ${newCodePeriod.value}`; default: return newCodePeriod.type; @@ -65,7 +65,7 @@ function branchInheritsItselfAsReference( ) { return ( !branch.newCodePeriod && - inheritedSetting.type === 'REFERENCE_BRANCH' && + inheritedSetting.type === NewCodePeriodSettingType.REFERENCE_BRANCH && branch.name === inheritedSetting.value ); } @@ -77,7 +77,7 @@ function referenceBranchDoesNotExist( return ( branch.newCodePeriod && branch.newCodePeriod.value && - branch.newCodePeriod.type === 'REFERENCE_BRANCH' && + branch.newCodePeriod.type === NewCodePeriodSettingType.REFERENCE_BRANCH && !existingBranches.includes(branch.newCodePeriod.value) ); } diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/ProjectBaselineApp.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/components/ProjectBaselineApp.tsx new file mode 100644 index 00000000000..025cc21974e --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/ProjectBaselineApp.tsx @@ -0,0 +1,333 @@ +/* + * 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 classNames from 'classnames'; +import { debounce } from 'lodash'; +import * as React from 'react'; +import { getNewCodePeriod, resetNewCodePeriod, setNewCodePeriod } from '../../../api/newCodePeriod'; +import withAppStateContext from '../../../app/components/app-state/withAppStateContext'; +import withAvailableFeatures, { + WithAvailableFeaturesProps, +} from '../../../app/components/available-features/withAvailableFeatures'; +import withComponentContext from '../../../app/components/componentContext/withComponentContext'; +import Suggestions from '../../../components/embed-docs-modal/Suggestions'; +import AlertSuccessIcon from '../../../components/icons/AlertSuccessIcon'; +import DeferredSpinner from '../../../components/ui/DeferredSpinner'; +import { isBranch, sortBranches } from '../../../helpers/branch-like'; +import { translate } from '../../../helpers/l10n'; +import { AppState } from '../../../types/appstate'; +import { Branch, BranchLike } from '../../../types/branch-like'; +import { Feature } from '../../../types/features'; +import { ParsedAnalysis } from '../../../types/project-activity'; +import { Component, NewCodePeriod, NewCodePeriodSettingType } from '../../../types/types'; +import { DEFAULT_GENERAL_SETTING_TYPE } from '../constants'; +import '../styles.css'; +import { getSettingValue } from '../utils'; +import AppHeader from './AppHeader'; +import BranchList from './BranchList'; +import ProjectBaselineSelector from './ProjectBaselineSelector'; + +interface Props extends WithAvailableFeaturesProps { + branchLike: Branch; + branchLikes: BranchLike[]; + component: Component; + appState: AppState; +} + +interface State { + analysis?: string; + branchList: Branch[]; + currentSetting?: NewCodePeriodSettingType; + currentSettingValue?: string; + days: string; + generalSetting?: NewCodePeriod; + loading: boolean; + overrideGeneralSetting?: boolean; + referenceBranch?: string; + saving: boolean; + selected?: NewCodePeriodSettingType; + success?: boolean; +} + +const DEFAULT_NUMBER_OF_DAYS = '30'; + +export class ProjectBaselineApp extends React.PureComponent { + mounted = false; + state: State = { + branchList: [], + days: DEFAULT_NUMBER_OF_DAYS, + loading: true, + saving: false, + }; + + // We use debounce as we could have multiple save in less that 3sec. + resetSuccess = debounce(() => this.setState({ success: undefined }), 3000); + + componentDidMount() { + this.mounted = true; + this.fetchLeakPeriodSetting(); + this.sortAndFilterBranches(this.props.branchLikes); + } + + componentDidUpdate(prevProps: Props) { + if (prevProps.branchLikes !== this.props.branchLikes) { + this.sortAndFilterBranches(this.props.branchLikes); + } + } + + componentWillUnmount() { + this.mounted = false; + } + + getUpdatedState(params: { + currentSetting?: NewCodePeriodSettingType; + currentSettingValue?: string; + generalSetting: NewCodePeriod; + }) { + const { currentSetting, currentSettingValue, generalSetting } = params; + const { referenceBranch } = this.state; + + const defaultDays = + (generalSetting.type === NewCodePeriodSettingType.NUMBER_OF_DAYS && generalSetting.value) || + DEFAULT_NUMBER_OF_DAYS; + + return { + loading: false, + currentSetting, + currentSettingValue, + generalSetting, + selected: currentSetting || generalSetting.type, + overrideGeneralSetting: Boolean(currentSetting), + days: + (currentSetting === NewCodePeriodSettingType.NUMBER_OF_DAYS && currentSettingValue) || + defaultDays, + analysis: + (currentSetting === NewCodePeriodSettingType.SPECIFIC_ANALYSIS && currentSettingValue) || + '', + referenceBranch: + (currentSetting === NewCodePeriodSettingType.REFERENCE_BRANCH && currentSettingValue) || + referenceBranch, + }; + } + + sortAndFilterBranches(branchLikes: BranchLike[] = []) { + const branchList = sortBranches(branchLikes.filter(isBranch)); + this.setState({ branchList, referenceBranch: branchList[0].name }); + } + + fetchLeakPeriodSetting() { + const { branchLike, component } = this.props; + + this.setState({ loading: true }); + + Promise.all([ + getNewCodePeriod(), + getNewCodePeriod({ + branch: this.props.hasFeature(Feature.BranchSupport) ? undefined : branchLike.name, + project: component.key, + }), + ]).then( + ([generalSetting, setting]) => { + if (this.mounted) { + if (!generalSetting.type) { + generalSetting = { type: DEFAULT_GENERAL_SETTING_TYPE }; + } + const currentSettingValue = setting.value; + const currentSetting = setting.inherited + ? undefined + : setting.type || DEFAULT_GENERAL_SETTING_TYPE; + + this.setState( + this.getUpdatedState({ + generalSetting, + currentSetting, + currentSettingValue, + }) + ); + } + }, + () => { + this.setState({ loading: false }); + } + ); + } + + resetSetting = () => { + this.setState({ saving: true }); + resetNewCodePeriod({ project: this.props.component.key }).then( + () => { + this.setState({ + saving: false, + currentSetting: undefined, + selected: undefined, + success: true, + }); + this.resetSuccess(); + }, + () => { + this.setState({ saving: false }); + } + ); + }; + + handleSelectAnalysis = (analysis: ParsedAnalysis) => this.setState({ analysis: analysis.key }); + + handleSelectDays = (days: string) => this.setState({ days }); + + handleSelectReferenceBranch = (referenceBranch: string) => { + this.setState({ referenceBranch }); + }; + + handleCancel = () => + this.setState( + ({ + generalSetting = { type: DEFAULT_GENERAL_SETTING_TYPE }, + currentSetting, + currentSettingValue, + }) => this.getUpdatedState({ generalSetting, currentSetting, currentSettingValue }) + ); + + handleSelectSetting = (selected?: NewCodePeriodSettingType) => this.setState({ selected }); + + handleToggleSpecificSetting = (overrideGeneralSetting: boolean) => + this.setState({ overrideGeneralSetting }); + + handleSubmit = (e: React.SyntheticEvent) => { + e.preventDefault(); + + const { component } = this.props; + const { analysis, days, selected: type, referenceBranch, overrideGeneralSetting } = this.state; + + if (!overrideGeneralSetting) { + this.resetSetting(); + return; + } + + const value = getSettingValue({ type, analysis, days, referenceBranch }); + + if (type) { + this.setState({ saving: true }); + setNewCodePeriod({ + project: component.key, + type, + value, + }).then( + () => { + this.setState({ + saving: false, + currentSetting: type, + currentSettingValue: value || undefined, + success: true, + }); + this.resetSuccess(); + }, + () => { + this.setState({ saving: false }); + } + ); + } + }; + + render() { + const { appState, component, branchLike } = this.props; + const { + analysis, + branchList, + currentSetting, + days, + generalSetting, + loading, + currentSettingValue, + overrideGeneralSetting, + referenceBranch, + saving, + selected, + success, + } = this.state; + const branchSupportEnabled = this.props.hasFeature(Feature.BranchSupport); + + return ( + <> + +
+ + {loading ? ( + + ) : ( +
+ {branchSupportEnabled &&

{translate('project_baseline.default_setting')}

} + + {generalSetting && overrideGeneralSetting !== undefined && ( + + )} + +
+ + + {translate('settings.state.saved')} + +
+ {generalSetting && branchSupportEnabled && ( +
+
+

{translate('project_baseline.configure_branches')}

+ +
+ )} +
+ )} +
+ + ); + } +} + +export default withComponentContext(withAvailableFeatures(withAppStateContext(ProjectBaselineApp))); diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/ProjectBaselineSelector.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/components/ProjectBaselineSelector.tsx index 76aa0647165..ac0eb18dbef 100644 --- a/server/sonar-web/src/main/js/apps/projectBaseline/components/ProjectBaselineSelector.tsx +++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/ProjectBaselineSelector.tsx @@ -60,7 +60,7 @@ export interface ProjectBaselineSelectorProps { function renderGeneralSetting(generalSetting: NewCodePeriod) { let setting: string; let description: string; - if (generalSetting.type === 'NUMBER_OF_DAYS') { + if (generalSetting.type === NewCodePeriodSettingType.NUMBER_OF_DAYS) { setting = `${translate('baseline.number_days')} (${translateWithParameters( 'duration.days', generalSetting.value || '?' @@ -137,7 +137,9 @@ export default function ProjectBaselineSelector(props: ProjectBaselineSelectorPr {branchesEnabled ? ( ) : ( )} - {selected === 'SPECIFIC_ANALYSIS' && ( + {selected === NewCodePeriodSettingType.SPECIFIC_ANALYSIS && ( ({ - getNewCodePeriod: jest.fn().mockResolvedValue({}), - resetNewCodePeriod: jest.fn().mockResolvedValue({}), - setNewCodePeriod: jest.fn().mockResolvedValue({}), -})); - -it('should render correctly', async () => { - let wrapper = shallowRender(); - await waitAndUpdate(wrapper); - expect(wrapper).toMatchSnapshot(); - - wrapper = shallowRender({ appState: mockAppState({ canAdmin: true }), hasFeature: () => false }); - await waitAndUpdate(wrapper); - expect(wrapper).toMatchSnapshot('without branch support'); -}); - -it('should initialize correctly', async () => { - const wrapper = shallowRender({ - branchLikes: [mockBranch(), mockPullRequest(), mockMainBranch()], - }); - await waitAndUpdate(wrapper); - - expect(wrapper.state().branchList).toHaveLength(2); - expect(wrapper.state().referenceBranch).toBe('master'); -}); - -it('should not display reset button if project setting is not set', () => { - const wrapper = shallowRender(); - - expect(wrapper.find('Button')).toHaveLength(0); -}); - -it('should reset the setting correctly', async () => { - const wrapper = shallowRender(); - await waitAndUpdate(wrapper); - wrapper.instance().resetSetting(); - await waitAndUpdate(wrapper); - expect(wrapper.state('currentSetting')).toBeUndefined(); - expect(wrapper.state('selected')).toBeUndefined(); -}); - -it('should save correctly', async () => { - const component = mockComponent(); - const wrapper = shallowRender({ component }); - await waitAndUpdate(wrapper); - wrapper.setState({ selected: 'NUMBER_OF_DAYS', days: '23' }); - wrapper.instance().handleSubmit(mockEvent()); - await waitAndUpdate(wrapper); - expect(setNewCodePeriod).toHaveBeenCalledWith({ - project: component.key, - type: 'NUMBER_OF_DAYS', - value: '23', - }); - expect(wrapper.state('currentSetting')).toEqual(wrapper.state('selected')); -}); - -it('should handle errors gracefully', async () => { - (getNewCodePeriod as jest.Mock).mockRejectedValue('error'); - (setNewCodePeriod as jest.Mock).mockRejectedValue('error'); - (resetNewCodePeriod as jest.Mock).mockRejectedValue('error'); - - const wrapper = shallowRender(); - wrapper.setState({ selected: 'PREVIOUS_VERSION' }); - await waitAndUpdate(wrapper); - - expect(wrapper.state('loading')).toBe(false); - wrapper.instance().resetSetting(); - await waitAndUpdate(wrapper); - expect(wrapper.state('saving')).toBe(false); - wrapper.instance().handleSubmit(mockEvent()); - await waitAndUpdate(wrapper); - expect(wrapper.state('saving')).toBe(false); -}); - -function shallowRender(props: Partial = {}) { - return shallow( - - ); -} diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/BaselineSettingAnalysis-test.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/BaselineSettingAnalysis-test.tsx index c4b9ec1718b..f6960dbe0fb 100644 --- a/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/BaselineSettingAnalysis-test.tsx +++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/BaselineSettingAnalysis-test.tsx @@ -19,6 +19,7 @@ */ import { shallow } from 'enzyme'; import * as React from 'react'; +import { NewCodePeriodSettingType } from '../../../../types/types'; import BaselineSettingAnalysis, { Props } from '../BaselineSettingAnalysis'; it('should render correctly', () => { @@ -30,7 +31,7 @@ it('should callback when clicked', () => { const wrapper = shallowRender({ onSelect, selected: false }); wrapper.find('RadioCard').first().simulate('click'); - expect(onSelect).toHaveBeenCalledWith('SPECIFIC_ANALYSIS'); + expect(onSelect).toHaveBeenCalledWith(NewCodePeriodSettingType.SPECIFIC_ANALYSIS); }); function shallowRender(props: Partial = {}) { diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/BaselineSettingDays-test.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/BaselineSettingDays-test.tsx index 95fcbe423d4..a7aa306c6c6 100644 --- a/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/BaselineSettingDays-test.tsx +++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/BaselineSettingDays-test.tsx @@ -19,6 +19,7 @@ */ import { shallow } from 'enzyme'; import * as React from 'react'; +import { NewCodePeriodSettingType } from '../../../../types/types'; import BaselineSettingDays, { Props } from '../BaselineSettingDays'; it('should render correctly', () => { @@ -37,7 +38,7 @@ it('should callback when clicked', () => { const wrapper = shallowRender({ onSelect, selected: false }); wrapper.find('RadioCard').first().simulate('click'); - expect(onSelect).toHaveBeenCalledWith('NUMBER_OF_DAYS'); + expect(onSelect).toHaveBeenCalledWith(NewCodePeriodSettingType.NUMBER_OF_DAYS); }); it('should callback when changing days', () => { diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/BaselineSettingPreviousVersion-test.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/BaselineSettingPreviousVersion-test.tsx index b680fb9cb88..9072f671c8d 100644 --- a/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/BaselineSettingPreviousVersion-test.tsx +++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/BaselineSettingPreviousVersion-test.tsx @@ -19,6 +19,7 @@ */ import { shallow } from 'enzyme'; import * as React from 'react'; +import { NewCodePeriodSettingType } from '../../../../types/types'; import BaselineSettingPreviousVersion, { Props } from '../BaselineSettingPreviousVersion'; it('should render correctly', () => { @@ -31,7 +32,7 @@ it('should callback when clicked', () => { const wrapper = shallowRender({ onSelect, selected: false }); wrapper.find('RadioCard').first().simulate('click'); - expect(onSelect).toHaveBeenCalledWith('PREVIOUS_VERSION'); + expect(onSelect).toHaveBeenCalledWith(NewCodePeriodSettingType.PREVIOUS_VERSION); }); function shallowRender(props: Partial = {}) { diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/BaselineSettingReferenceBranch-test.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/BaselineSettingReferenceBranch-test.tsx index c240643795d..dd9f28b5a76 100644 --- a/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/BaselineSettingReferenceBranch-test.tsx +++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/BaselineSettingReferenceBranch-test.tsx @@ -22,6 +22,7 @@ import * as React from 'react'; import { OptionProps, Props as ReactSelectProps } from 'react-select'; import RadioCard from '../../../../components/controls/RadioCard'; import Select from '../../../../components/controls/Select'; +import { NewCodePeriodSettingType } from '../../../../types/types'; import BaselineSettingReferenceBranch, { BaselineSettingReferenceBranchProps, BranchOption, @@ -49,7 +50,7 @@ it('should callback when clicked', () => { const wrapper = shallowRender({ onSelect, selected: false }); wrapper.find(RadioCard).first().simulate('click'); - expect(onSelect).toHaveBeenCalledWith('REFERENCE_BRANCH'); + expect(onSelect).toHaveBeenCalledWith(NewCodePeriodSettingType.REFERENCE_BRANCH); }); it('should callback when changing selection', () => { diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/BranchBaselineSettingModal-test.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/BranchBaselineSettingModal-test.tsx index 06f71f16c33..8a0e83b8e50 100644 --- a/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/BranchBaselineSettingModal-test.tsx +++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/BranchBaselineSettingModal-test.tsx @@ -22,6 +22,7 @@ import * as React from 'react'; import { setNewCodePeriod } from '../../../../api/newCodePeriod'; import { mockBranch, mockMainBranch } from '../../../../helpers/mocks/branch-like'; import { mockEvent, waitAndUpdate } from '../../../../helpers/testUtils'; +import { NewCodePeriodSettingType } from '../../../../types/types'; import BranchBaselineSettingModal from '../BranchBaselineSettingModal'; jest.mock('../../../../api/newCodePeriod', () => ({ @@ -38,7 +39,7 @@ it('should render correctly', () => { it('should display the branch analysis list when necessary', () => { const wrapper = shallowRender(); - wrapper.setState({ selected: 'SPECIFIC_ANALYSIS' }); + wrapper.setState({ selected: NewCodePeriodSettingType.SPECIFIC_ANALYSIS }); expect(wrapper.find('BranchAnalysisList')).toHaveLength(1); }); @@ -51,7 +52,10 @@ it('should save correctly', async () => { component, }); - wrapper.setState({ analysis: 'analysis572893', selected: 'SPECIFIC_ANALYSIS' }); + wrapper.setState({ + analysis: 'analysis572893', + selected: NewCodePeriodSettingType.SPECIFIC_ANALYSIS, + }); await waitAndUpdate(wrapper); wrapper.instance().handleSubmit(mockEvent()); @@ -59,7 +63,7 @@ it('should save correctly', async () => { expect(setNewCodePeriod).toHaveBeenCalledWith({ project: component, - type: 'SPECIFIC_ANALYSIS', + type: NewCodePeriodSettingType.SPECIFIC_ANALYSIS, value: 'analysis572893', branch: 'branchname', }); diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/BranchList-test.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/BranchList-test.tsx index df935426e90..3fab1ff976f 100644 --- a/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/BranchList-test.tsx +++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/BranchList-test.tsx @@ -23,6 +23,7 @@ import { listBranchesNewCodePeriod, resetNewCodePeriod } from '../../../../api/n import { mockBranch, mockMainBranch } from '../../../../helpers/mocks/branch-like'; import { mockComponent } from '../../../../helpers/mocks/component'; import { waitAndUpdate } from '../../../../helpers/testUtils'; +import { NewCodePeriodSettingType } from '../../../../types/types'; import BranchBaselineSettingModal from '../BranchBaselineSettingModal'; import BranchList from '../BranchList'; @@ -35,7 +36,7 @@ const newCodePeriods = [ { projectKey: '', branchKey: 'master', - type: 'NUMBER_OF_DAYS', + type: NewCodePeriodSettingType.NUMBER_OF_DAYS, value: '27', }, ]; @@ -73,7 +74,9 @@ it('should toggle popup', async () => { expect(nodes).toHaveLength(1); expect(nodes.first().props().branch).toEqual(mockMainBranch()); - wrapper.instance().closeEditModal('master', { type: 'NUMBER_OF_DAYS', value: '23' }); + wrapper + .instance() + .closeEditModal('master', { type: NewCodePeriodSettingType.NUMBER_OF_DAYS, value: '23' }); expect(wrapper.find('BranchBaselineSettingModal')).toHaveLength(0); expect(wrapper.state().branches.find((b) => b.name === 'master')).toEqual({ @@ -82,7 +85,7 @@ it('should toggle popup', async () => { isMain: true, name: 'master', newCodePeriod: { - type: 'NUMBER_OF_DAYS', + type: NewCodePeriodSettingType.NUMBER_OF_DAYS, value: '23', }, }); @@ -93,7 +96,7 @@ function shallowRender(props: Partial = {}) { ); diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/BranchListRow-test.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/BranchListRow-test.tsx index 66f6593bb16..12f2bb83be1 100644 --- a/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/BranchListRow-test.tsx +++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/BranchListRow-test.tsx @@ -21,6 +21,7 @@ import { shallow } from 'enzyme'; import * as React from 'react'; import { ActionsDropdownItem } from '../../../../components/controls/ActionsDropdown'; import { mockBranch, mockMainBranch } from '../../../../helpers/mocks/branch-like'; +import { NewCodePeriodSettingType } from '../../../../types/types'; import BranchListRow, { BranchListRowProps } from '../BranchListRow'; it('should render correctly', () => { @@ -28,17 +29,23 @@ it('should render correctly', () => { expect( shallowRender({ branch: mockBranch({ name: 'branch-7.3' }), - inheritedSetting: { type: 'REFERENCE_BRANCH', value: 'branch-7.3' }, + inheritedSetting: { type: NewCodePeriodSettingType.REFERENCE_BRANCH, value: 'branch-7.3' }, }) ).toMatchSnapshot('faulty branch'); expect( shallowRender({ - branch: { ...mockBranch(), newCodePeriod: { type: 'NUMBER_OF_DAYS', value: '21' } }, + branch: { + ...mockBranch(), + newCodePeriod: { type: NewCodePeriodSettingType.NUMBER_OF_DAYS, value: '21' }, + }, }) ).toMatchSnapshot('branch with number of days'); expect( shallowRender({ - branch: { ...mockBranch(), newCodePeriod: { type: 'PREVIOUS_VERSION' } }, + branch: { + ...mockBranch(), + newCodePeriod: { type: NewCodePeriodSettingType.PREVIOUS_VERSION }, + }, }) ).toMatchSnapshot('branch with previous version'); expect( @@ -46,7 +53,7 @@ it('should render correctly', () => { branch: { ...mockBranch(), newCodePeriod: { - type: 'SPECIFIC_ANALYSIS', + type: NewCodePeriodSettingType.SPECIFIC_ANALYSIS, value: 'A85835', effectiveValue: '2018-12-02T13:01:12', }, @@ -55,7 +62,10 @@ it('should render correctly', () => { ).toMatchSnapshot('branch with specific analysis'); expect( shallowRender({ - branch: { ...mockBranch(), newCodePeriod: { type: 'REFERENCE_BRANCH', value: 'master' } }, + branch: { + ...mockBranch(), + newCodePeriod: { type: NewCodePeriodSettingType.REFERENCE_BRANCH, value: 'master' }, + }, }) ).toMatchSnapshot('branch with reference branch'); }); @@ -74,7 +84,10 @@ it('should callback to reset when clicked', () => { const resetToDefault = jest.fn(); const branchName = 'branch-6.5'; const wrapper = shallowRender({ - branch: { ...mockBranch({ name: branchName }), newCodePeriod: { type: 'REFERENCE_BRANCH' } }, + branch: { + ...mockBranch({ name: branchName }), + newCodePeriod: { type: NewCodePeriodSettingType.REFERENCE_BRANCH }, + }, onResetToDefault: resetToDefault, }); diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/ProjectBaselineApp-test.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/ProjectBaselineApp-test.tsx new file mode 100644 index 00000000000..1f368a4060e --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/ProjectBaselineApp-test.tsx @@ -0,0 +1,119 @@ +/* + * 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 { + getNewCodePeriod, + resetNewCodePeriod, + setNewCodePeriod, +} from '../../../../api/newCodePeriod'; +import { mockBranch, mockMainBranch, mockPullRequest } from '../../../../helpers/mocks/branch-like'; +import { mockComponent } from '../../../../helpers/mocks/component'; +import { mockAppState } from '../../../../helpers/testMocks'; +import { mockEvent, waitAndUpdate } from '../../../../helpers/testUtils'; +import { NewCodePeriodSettingType } from '../../../../types/types'; +import { ProjectBaselineApp } from '../ProjectBaselineApp'; + +jest.mock('../../../../api/newCodePeriod', () => ({ + getNewCodePeriod: jest.fn().mockResolvedValue({}), + resetNewCodePeriod: jest.fn().mockResolvedValue({}), + setNewCodePeriod: jest.fn().mockResolvedValue({}), +})); + +it('should render correctly', async () => { + let wrapper = shallowRender(); + await waitAndUpdate(wrapper); + expect(wrapper).toMatchSnapshot(); + + wrapper = shallowRender({ appState: mockAppState({ canAdmin: true }), hasFeature: () => false }); + await waitAndUpdate(wrapper); + expect(wrapper).toMatchSnapshot('without branch support'); +}); + +it('should initialize correctly', async () => { + const wrapper = shallowRender({ + branchLikes: [mockBranch(), mockPullRequest(), mockMainBranch()], + }); + await waitAndUpdate(wrapper); + + expect(wrapper.state().branchList).toHaveLength(2); + expect(wrapper.state().referenceBranch).toBe('master'); +}); + +it('should not display reset button if project setting is not set', () => { + const wrapper = shallowRender(); + + expect(wrapper.find('Button')).toHaveLength(0); +}); + +it('should reset the setting correctly', async () => { + const wrapper = shallowRender(); + await waitAndUpdate(wrapper); + wrapper.instance().resetSetting(); + await waitAndUpdate(wrapper); + expect(wrapper.state('currentSetting')).toBeUndefined(); + expect(wrapper.state('selected')).toBeUndefined(); +}); + +it('should save correctly', async () => { + const component = mockComponent(); + const wrapper = shallowRender({ component }); + await waitAndUpdate(wrapper); + wrapper.setState({ selected: NewCodePeriodSettingType.NUMBER_OF_DAYS, days: '23' }); + wrapper.instance().handleSubmit(mockEvent()); + await waitAndUpdate(wrapper); + expect(setNewCodePeriod).toHaveBeenCalledWith({ + project: component.key, + type: NewCodePeriodSettingType.NUMBER_OF_DAYS, + value: '23', + }); + expect(wrapper.state('currentSetting')).toEqual(wrapper.state('selected')); +}); + +it('should handle errors gracefully', async () => { + (getNewCodePeriod as jest.Mock).mockRejectedValue('error'); + (setNewCodePeriod as jest.Mock).mockRejectedValue('error'); + (resetNewCodePeriod as jest.Mock).mockRejectedValue('error'); + + const wrapper = shallowRender(); + wrapper.setState({ selected: NewCodePeriodSettingType.PREVIOUS_VERSION }); + await waitAndUpdate(wrapper); + + expect(wrapper.state('loading')).toBe(false); + wrapper.instance().resetSetting(); + await waitAndUpdate(wrapper); + expect(wrapper.state('saving')).toBe(false); + wrapper.instance().handleSubmit(mockEvent()); + await waitAndUpdate(wrapper); + expect(wrapper.state('saving')).toBe(false); +}); + +function shallowRender(props: Partial = {}) { + return shallow( + + ); +} diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/ProjectBaselineSelector-test.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/ProjectBaselineSelector-test.tsx index 09b2ecdfe1d..740f830be9c 100644 --- a/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/ProjectBaselineSelector-test.tsx +++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/ProjectBaselineSelector-test.tsx @@ -20,6 +20,7 @@ import { shallow } from 'enzyme'; import * as React from 'react'; import { mockBranch, mockMainBranch } from '../../../../helpers/mocks/branch-like'; +import { NewCodePeriodSettingType } from '../../../../types/types'; import ProjectBaselineSelector, { ProjectBaselineSelectorProps } from '../ProjectBaselineSelector'; it('should render correctly', () => { @@ -27,18 +28,21 @@ it('should render correctly', () => { expect( shallowRender({ branchesEnabled: false, - generalSetting: { type: 'NUMBER_OF_DAYS', value: '23' }, + generalSetting: { type: NewCodePeriodSettingType.NUMBER_OF_DAYS, value: '23' }, }) ).toMatchSnapshot(); expect( - shallowRender({ branchesEnabled: false, generalSetting: { type: 'NUMBER_OF_DAYS', value: '' } }) + shallowRender({ + branchesEnabled: false, + generalSetting: { type: NewCodePeriodSettingType.NUMBER_OF_DAYS, value: '' }, + }) ).toMatchSnapshot(); }); it('should not show save button when unchanged', () => { const wrapper = shallowRender({ - currentSetting: 'PREVIOUS_VERSION', - selected: 'PREVIOUS_VERSION', + currentSetting: NewCodePeriodSettingType.PREVIOUS_VERSION, + selected: NewCodePeriodSettingType.PREVIOUS_VERSION, overrideGeneralSetting: true, }); expect(wrapper.find('SubmitButton').parent().hasClass('invisible')).toBe(true); @@ -46,8 +50,8 @@ it('should not show save button when unchanged', () => { it('should show save button when changed', () => { const wrapper = shallowRender({ - currentSetting: 'PREVIOUS_VERSION', - selected: 'NUMBER_OF_DAYS', + currentSetting: NewCodePeriodSettingType.PREVIOUS_VERSION, + selected: NewCodePeriodSettingType.NUMBER_OF_DAYS, overrideGeneralSetting: true, }); expect(wrapper.find('SubmitButton')).toHaveLength(1); @@ -55,10 +59,10 @@ it('should show save button when changed', () => { it('should show save button when value changed', () => { const wrapper = shallowRender({ - currentSetting: 'NUMBER_OF_DAYS', + currentSetting: NewCodePeriodSettingType.NUMBER_OF_DAYS, currentSettingValue: '23', days: '25', - selected: 'NUMBER_OF_DAYS', + selected: NewCodePeriodSettingType.NUMBER_OF_DAYS, overrideGeneralSetting: true, }); expect(wrapper.find('SubmitButton')).toHaveLength(1); @@ -66,10 +70,10 @@ it('should show save button when value changed', () => { it('should disable the save button when saving', () => { const wrapper = shallowRender({ - currentSetting: 'NUMBER_OF_DAYS', + currentSetting: NewCodePeriodSettingType.NUMBER_OF_DAYS, currentSettingValue: '25', saving: true, - selected: 'PREVIOUS_VERSION', + selected: NewCodePeriodSettingType.PREVIOUS_VERSION, overrideGeneralSetting: true, }); @@ -78,9 +82,9 @@ it('should disable the save button when saving', () => { it('should disable the save button when date is invalid', () => { const wrapper = shallowRender({ - currentSetting: 'PREVIOUS_VERSION', + currentSetting: NewCodePeriodSettingType.PREVIOUS_VERSION, days: 'hello', - selected: 'NUMBER_OF_DAYS', + selected: NewCodePeriodSettingType.NUMBER_OF_DAYS, overrideGeneralSetting: true, }); diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/App-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/App-test.tsx.snap deleted file mode 100644 index 847b62f98de..00000000000 --- a/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/App-test.tsx.snap +++ /dev/null @@ -1,196 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render correctly 1`] = ` - - -
- -
-

- project_baseline.default_setting -

- -
- - - settings.state.saved - -
-
-
-

- project_baseline.configure_branches -

- -
-
-
-
-`; - -exports[`should render correctly: without branch support 1`] = ` - - -
- -
- -
- - - settings.state.saved - -
-
-
-
-`; diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/ProjectBaselineApp-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/ProjectBaselineApp-test.tsx.snap new file mode 100644 index 00000000000..847b62f98de --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/ProjectBaselineApp-test.tsx.snap @@ -0,0 +1,196 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render correctly 1`] = ` + + +
+ +
+

+ project_baseline.default_setting +

+ +
+ + + settings.state.saved + +
+
+
+

+ project_baseline.configure_branches +

+ +
+
+
+
+`; + +exports[`should render correctly: without branch support 1`] = ` + + +
+ +
+ +
+ + + settings.state.saved + +
+
+
+
+`; diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/utils-test.ts b/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/utils-test.ts index 044e63b9da6..2d53227ef9f 100644 --- a/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/utils-test.ts +++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/utils-test.ts @@ -17,6 +17,7 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { NewCodePeriodSettingType } from '../../../../types/types'; import { getSettingValue, validateSetting } from '../../utils'; describe('getSettingValue', () => { @@ -27,19 +28,27 @@ describe('getSettingValue', () => { }; it('should work for Days', () => { - expect(getSettingValue({ ...state, type: 'NUMBER_OF_DAYS' })).toBe(state.days); + expect(getSettingValue({ ...state, type: NewCodePeriodSettingType.NUMBER_OF_DAYS })).toBe( + state.days + ); }); it('should work for Analysis', () => { - expect(getSettingValue({ ...state, type: 'SPECIFIC_ANALYSIS' })).toBe(state.analysis); + expect(getSettingValue({ ...state, type: NewCodePeriodSettingType.SPECIFIC_ANALYSIS })).toBe( + state.analysis + ); }); it('should work for Previous version', () => { - expect(getSettingValue({ ...state, type: 'PREVIOUS_VERSION' })).toBeUndefined(); + expect( + getSettingValue({ ...state, type: NewCodePeriodSettingType.PREVIOUS_VERSION }) + ).toBeUndefined(); }); it('should work for Reference branch', () => { - expect(getSettingValue({ ...state, type: 'REFERENCE_BRANCH' })).toBe(state.referenceBranch); + expect(getSettingValue({ ...state, type: NewCodePeriodSettingType.REFERENCE_BRANCH })).toBe( + state.referenceBranch + ); }); }); @@ -48,68 +57,68 @@ describe('validateSettings', () => { expect(validateSetting({ days: '' })).toEqual({ isChanged: false, isValid: false }); expect( validateSetting({ - currentSetting: 'PREVIOUS_VERSION', + currentSetting: NewCodePeriodSettingType.PREVIOUS_VERSION, days: '12', - selected: 'NUMBER_OF_DAYS', + selected: NewCodePeriodSettingType.NUMBER_OF_DAYS, }) ).toEqual({ isChanged: true, isValid: true }); expect( validateSetting({ - currentSetting: 'PREVIOUS_VERSION', + currentSetting: NewCodePeriodSettingType.PREVIOUS_VERSION, days: 'nope', - selected: 'NUMBER_OF_DAYS', + selected: NewCodePeriodSettingType.NUMBER_OF_DAYS, }) ).toEqual({ isChanged: true, isValid: false }); expect( validateSetting({ - currentSetting: 'NUMBER_OF_DAYS', + currentSetting: NewCodePeriodSettingType.NUMBER_OF_DAYS, currentSettingValue: '15', days: '15', - selected: 'NUMBER_OF_DAYS', + selected: NewCodePeriodSettingType.NUMBER_OF_DAYS, }) ).toEqual({ isChanged: false, isValid: true }); expect( validateSetting({ - currentSetting: 'NUMBER_OF_DAYS', + currentSetting: NewCodePeriodSettingType.NUMBER_OF_DAYS, currentSettingValue: '15', days: '13', - selected: 'NUMBER_OF_DAYS', + selected: NewCodePeriodSettingType.NUMBER_OF_DAYS, }) ).toEqual({ isChanged: true, isValid: true }); expect( validateSetting({ analysis: 'analysis1', - currentSetting: 'SPECIFIC_ANALYSIS', + currentSetting: NewCodePeriodSettingType.SPECIFIC_ANALYSIS, currentSettingValue: 'analysis1', days: '', - selected: 'SPECIFIC_ANALYSIS', + selected: NewCodePeriodSettingType.SPECIFIC_ANALYSIS, }) ).toEqual({ isChanged: false, isValid: true }); expect( validateSetting({ analysis: 'analysis2', - currentSetting: 'SPECIFIC_ANALYSIS', + currentSetting: NewCodePeriodSettingType.SPECIFIC_ANALYSIS, currentSettingValue: 'analysis1', days: '', - selected: 'SPECIFIC_ANALYSIS', + selected: NewCodePeriodSettingType.SPECIFIC_ANALYSIS, }) ).toEqual({ isChanged: true, isValid: true }); expect( validateSetting({ - currentSetting: 'REFERENCE_BRANCH', + currentSetting: NewCodePeriodSettingType.REFERENCE_BRANCH, currentSettingValue: 'master', days: '', referenceBranch: 'master', - selected: 'REFERENCE_BRANCH', + selected: NewCodePeriodSettingType.REFERENCE_BRANCH, }) ).toEqual({ isChanged: false, isValid: true }); expect( validateSetting({ - currentSetting: 'REFERENCE_BRANCH', + currentSetting: NewCodePeriodSettingType.REFERENCE_BRANCH, currentSettingValue: 'master', days: '', referenceBranch: '', - selected: 'REFERENCE_BRANCH', + selected: NewCodePeriodSettingType.REFERENCE_BRANCH, }) ).toEqual({ isChanged: true, isValid: false }); }); @@ -125,7 +134,7 @@ describe('validateSettings', () => { }); expect( validateSetting({ - currentSetting: 'PREVIOUS_VERSION', + currentSetting: NewCodePeriodSettingType.PREVIOUS_VERSION, days: '', overrideGeneralSetting: false, }) diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/constants.ts b/server/sonar-web/src/main/js/apps/projectBaseline/constants.ts new file mode 100644 index 00000000000..00c2c82e24b --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectBaseline/constants.ts @@ -0,0 +1,24 @@ +/* + * 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 { NewCodePeriodSettingType } from '../../types/types'; + +export const DEFAULT_GENERAL_SETTING_TYPE: NewCodePeriodSettingType = + NewCodePeriodSettingType.PREVIOUS_VERSION; diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/routes.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/routes.tsx index 5397eae9553..2235fe7aabf 100644 --- a/server/sonar-web/src/main/js/apps/projectBaseline/routes.tsx +++ b/server/sonar-web/src/main/js/apps/projectBaseline/routes.tsx @@ -19,8 +19,8 @@ */ import React from 'react'; import { Route } from 'react-router-dom'; -import App from './components/App'; +import ProjectBaselineApp from './components/ProjectBaselineApp'; -const routes = () => } />; +const routes = () => } />; export default routes; diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/utils.ts b/server/sonar-web/src/main/js/apps/projectBaseline/utils.ts index 3f249e2fd77..2ca20b85b1e 100644 --- a/server/sonar-web/src/main/js/apps/projectBaseline/utils.ts +++ b/server/sonar-web/src/main/js/apps/projectBaseline/utils.ts @@ -37,11 +37,11 @@ export function getSettingValue({ type?: NewCodePeriodSettingType; }) { switch (type) { - case 'NUMBER_OF_DAYS': + case NewCodePeriodSettingType.NUMBER_OF_DAYS: return days; - case 'REFERENCE_BRANCH': + case NewCodePeriodSettingType.REFERENCE_BRANCH: return referenceBranch; - case 'SPECIFIC_ANALYSIS': + case NewCodePeriodSettingType.SPECIFIC_ANALYSIS: return analysis; default: return undefined; @@ -74,17 +74,19 @@ export function validateSetting(state: { isChanged = overrideGeneralSetting === false || selected !== currentSetting || - (selected === 'NUMBER_OF_DAYS' && days !== currentSettingValue) || - (selected === 'SPECIFIC_ANALYSIS' && analysis !== currentSettingValue) || - (selected === 'REFERENCE_BRANCH' && referenceBranch !== currentSettingValue); + (selected === NewCodePeriodSettingType.NUMBER_OF_DAYS && days !== currentSettingValue) || + (selected === NewCodePeriodSettingType.SPECIFIC_ANALYSIS && + analysis !== currentSettingValue) || + (selected === NewCodePeriodSettingType.REFERENCE_BRANCH && + referenceBranch !== currentSettingValue); } const isValid = overrideGeneralSetting === false || - selected === 'PREVIOUS_VERSION' || - (selected === 'SPECIFIC_ANALYSIS' && analysis.length > 0) || - (selected === 'NUMBER_OF_DAYS' && validateDays(days)) || - (selected === 'REFERENCE_BRANCH' && referenceBranch.length > 0); + selected === NewCodePeriodSettingType.PREVIOUS_VERSION || + (selected === NewCodePeriodSettingType.SPECIFIC_ANALYSIS && analysis.length > 0) || + (selected === NewCodePeriodSettingType.NUMBER_OF_DAYS && validateDays(days)) || + (selected === NewCodePeriodSettingType.REFERENCE_BRANCH && referenceBranch.length > 0); return { isChanged, isValid }; } diff --git a/server/sonar-web/src/main/js/apps/settings/components/Definition.tsx b/server/sonar-web/src/main/js/apps/settings/components/Definition.tsx index c0167864efc..ca7d3221869 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/Definition.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/Definition.tsx @@ -90,10 +90,9 @@ export default class Definition extends React.PureComponent { settingValue, }); - this.timeout = window.setTimeout( - () => this.setState({ success: false }), - SAFE_SET_STATE_DELAY - ); + this.timeout = window.setTimeout(() => { + this.setState({ success: false }); + }, SAFE_SET_STATE_DELAY); } catch (e) { const validationMessage = await parseError(e as Response); this.setState({ loading: false, validationMessage }); @@ -179,10 +178,9 @@ export default class Definition extends React.PureComponent { settingValue, }); - this.timeout = window.setTimeout( - () => this.setState({ success: false }), - SAFE_SET_STATE_DELAY - ); + this.timeout = window.setTimeout(() => { + this.setState({ success: false }); + }, SAFE_SET_STATE_DELAY); } catch (e) { const validationMessage = await parseError(e as Response); this.setState({ loading: false, validationMessage }); diff --git a/server/sonar-web/src/main/js/apps/settings/components/NewCodePeriod.tsx b/server/sonar-web/src/main/js/apps/settings/components/NewCodePeriod.tsx index d6c260352c6..d59c3bf8775 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/NewCodePeriod.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/NewCodePeriod.tsx @@ -40,8 +40,6 @@ interface State { success: boolean; } -const DEFAULT_SETTING = 'PREVIOUS_VERSION'; - export default class NewCodePeriod extends React.PureComponent<{}, State> { mounted = false; state: State = { @@ -63,14 +61,12 @@ export default class NewCodePeriod extends React.PureComponent<{}, State> { fetchNewCodePeriodSetting() { getNewCodePeriod() .then(({ type, value }) => { - const currentSetting = type || DEFAULT_SETTING; - this.setState(({ days }) => ({ - currentSetting, - days: currentSetting === 'NUMBER_OF_DAYS' ? String(value) : days, + currentSetting: type, + days: type === NewCodePeriodSettingType.NUMBER_OF_DAYS ? String(value) : days, loading: false, currentSettingValue: value, - selected: currentSetting, + selected: type, })); }) .catch(() => { @@ -89,7 +85,10 @@ export default class NewCodePeriod extends React.PureComponent<{}, State> { onCancel = () => { this.setState(({ currentSetting, currentSettingValue, days }) => ({ selected: currentSetting, - days: currentSetting === 'NUMBER_OF_DAYS' ? String(currentSettingValue) : days, + days: + currentSetting === NewCodePeriodSettingType.NUMBER_OF_DAYS + ? String(currentSettingValue) + : days, })); }; @@ -99,29 +98,27 @@ export default class NewCodePeriod extends React.PureComponent<{}, State> { const { days, selected } = this.state; const type = selected; - const value = type === 'NUMBER_OF_DAYS' ? days : undefined; - - if (type) { - this.setState({ saving: true, success: false }); - setNewCodePeriod({ - type, - value, - }).then( - () => { - this.setState({ - saving: false, - currentSetting: type, - currentSettingValue: value || undefined, - success: true, - }); - }, - () => { - this.setState({ - saving: false, - }); - } - ); - } + const value = type === NewCodePeriodSettingType.NUMBER_OF_DAYS ? days : undefined; + + this.setState({ saving: true, success: false }); + setNewCodePeriod({ + type: type as NewCodePeriodSettingType, + value, + }).then( + () => { + this.setState({ + saving: false, + currentSetting: type, + currentSettingValue: value || undefined, + success: true, + }); + }, + () => { + this.setState({ + saving: false, + }); + } + ); }; render() { @@ -130,9 +127,10 @@ export default class NewCodePeriod extends React.PureComponent<{}, State> { const isChanged = selected !== currentSetting || - (selected === 'NUMBER_OF_DAYS' && String(days) !== currentSettingValue); + (selected === NewCodePeriodSettingType.NUMBER_OF_DAYS && + String(days) !== currentSettingValue); - const isValid = selected !== 'NUMBER_OF_DAYS' || validateDays(days); + const isValid = selected !== NewCodePeriodSettingType.NUMBER_OF_DAYS || validateDays(days); return (
    @@ -174,7 +172,7 @@ export default class NewCodePeriod extends React.PureComponent<{}, State> { { isValid={isValid} onChangeDays={this.onSelectDays} onSelect={this.onSelectSetting} - selected={selected === 'NUMBER_OF_DAYS'} + selected={selected === NewCodePeriodSettingType.NUMBER_OF_DAYS} /> {isChanged && (
    diff --git a/server/sonar-web/src/main/js/apps/settings/components/__tests__/AnalysisScope-test.tsx b/server/sonar-web/src/main/js/apps/settings/components/__tests__/AnalysisScope-test.tsx new file mode 100644 index 00000000000..a6df6000d50 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/settings/components/__tests__/AnalysisScope-test.tsx @@ -0,0 +1,49 @@ +/* + * 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 { uniq } from 'lodash'; +import * as React from 'react'; +import { byRole, byText } from 'testing-library-selector'; +import { DEFAULT_DEFINITIONS_MOCK } from '../../../../api/mocks/SettingsServiceMock'; +import { mockComponent } from '../../../../helpers/mocks/component'; +import { renderComponent } from '../../../../helpers/testReactTestingUtils'; +import { AdditionalCategoryComponentProps, ADDITIONAL_CATEGORIES } from '../AdditionalCategories'; + +const ui = { + introduction: byText('settings.analysis_scope.wildcards.introduction'), + docLink: byRole('link', { name: /learn_more/ }), +}; + +it('renders correctly', async () => { + renderAnalysisScope(); + + expect(await ui.introduction.find()).toBeInTheDocument(); + expect(ui.docLink.get()).toBeInTheDocument(); +}); + +function renderAnalysisScope(overrides: Partial = {}) { + const props = { + definitions: DEFAULT_DEFINITIONS_MOCK, + categories: uniq(DEFAULT_DEFINITIONS_MOCK.map((d) => d.category)), + selectedCategory: 'general', + component: mockComponent(), + ...overrides, + }; + return renderComponent(<>{ADDITIONAL_CATEGORIES[2].renderComponent(props)}); +} diff --git a/server/sonar-web/src/main/js/apps/settings/components/__tests__/Definition-it.tsx b/server/sonar-web/src/main/js/apps/settings/components/__tests__/Definition-it.tsx index 40869c50470..98d85ea341e 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/__tests__/Definition-it.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/__tests__/Definition-it.tsx @@ -17,156 +17,378 @@ * 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 { getValue, resetSettingValue, setSettingValue } from '../../../../api/settings'; -import { mockDefinition, mockSettingValue } from '../../../../helpers/mocks/settings'; -import { waitAndUpdate } from '../../../../helpers/testUtils'; -import { SettingType } from '../../../../types/settings'; +import { screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { last } from 'lodash'; +import React from 'react'; +import selectEvent from 'react-select-event'; +import { byLabelText, byRole, byText } from 'testing-library-selector'; +import SettingsServiceMock, { + DEFAULT_DEFINITIONS_MOCK, +} from '../../../../api/mocks/SettingsServiceMock'; +import { mockComponent } from '../../../../helpers/mocks/component'; +import { mockDefinition } from '../../../../helpers/mocks/settings'; +import { renderComponent } from '../../../../helpers/testReactTestingUtils'; +import { ExtendedSettingDefinition, SettingType, SettingValue } from '../../../../types/settings'; +import { Component } from '../../../../types/types'; import Definition from '../Definition'; -jest.mock('../../../../api/settings', () => ({ - getValue: jest.fn().mockResolvedValue({}), - resetSettingValue: jest.fn().mockResolvedValue(undefined), - setSettingValue: jest.fn().mockResolvedValue(undefined), -})); +jest.mock('../../../../api/settings'); + +let settingsMock: SettingsServiceMock; beforeAll(() => { - jest.useFakeTimers(); + settingsMock = new SettingsServiceMock(); }); -afterAll(() => { - jest.runOnlyPendingTimers(); - jest.useRealTimers(); +afterEach(() => { + settingsMock.reset(); }); -beforeEach(() => { - jest.clearAllMocks(); +beforeEach(jest.clearAllMocks); + +const ui = { + nameHeading: (name: string) => byRole('heading', { name }), + announcementInput: byLabelText('property.sonar.announcement.message.name'), + securedInput: byRole('textbox', { name: 'property.sonar.announcement.message.secured.name' }), + multiValuesInput: byRole('textbox', { name: 'property.sonar.javascript.globals.name' }), + urlKindInput: byRole('textbox', { name: /sonar.auth.gitlab.url/ }), + fieldsInput: (name: string) => byRole('textbox', { name: `property.${name}.name` }), + savedMsg: byText('settings.state.saved'), + validationMsg: byText(/settings.state.validation_failed/), + jsonFormatStatus: byRole('status', { name: 'alert.tooltip.info' }), + jsonFormatButton: byRole('button', { name: 'settings.json.format' }), + toggleButton: byRole('switch'), + selectOption: (value: string) => byText(value), + saveButton: byRole('button', { name: 'save' }), + cancelButton: byRole('button', { name: 'cancel' }), + changeButton: byRole('button', { name: 'change_verb' }), + resetButton: (name: string | RegExp = 'reset_verb') => byRole('button', { name }), + deleteValueButton: byRole('button', { + name: /settings.definition.delete_value/, + }), + deleteFieldsButton: byRole('button', { + name: /settings.definitions.delete_fields/, + }), +}; + +it.each([ + SettingType.TEXT, + SettingType.STRING, + SettingType.PASSWORD, + SettingType.INTEGER, + SettingType.LONG, + SettingType.FLOAT, + 'uknown type', +])( + 'renders definition for SettingType = %s and can do operations', + async (settingType: SettingType) => { + const user = userEvent.setup(); + renderDefinition({ type: settingType }); + + expect( + await ui.nameHeading('property.sonar.announcement.message.name').find() + ).toBeInTheDocument(); + + // Should see no empty validation message + await user.type(ui.announcementInput.get(), ' '); + await user.click(ui.saveButton.get()); + expect(await ui.validationMsg.find()).toBeInTheDocument(); + + // Should save variable + await user.type(ui.announcementInput.get(), 'Testing'); + await user.click(await ui.saveButton.find()); + expect(ui.validationMsg.query()).not.toBeInTheDocument(); + expect(ui.announcementInput.get()).toHaveValue(' Testing'); + expect(ui.savedMsg.get()).toBeInTheDocument(); + + // Validation message when clearing input to empty + await user.clear(ui.announcementInput.get()); + expect(ui.validationMsg.get()).toBeInTheDocument(); + + // Should reset to previous state on clicking cancel + await user.type(ui.announcementInput.get(), 'Testing2'); + await user.click(ui.cancelButton.get()); + expect(ui.announcementInput.get()).toHaveValue(' Testing'); + + // Clicking reset opens dialog and reset to default on confirm + await user.click( + ui.resetButton('settings.definition.reset.property.sonar.announcement.message.name').get() + ); + await user.click(ui.resetButton().get()); + expect(ui.announcementInput.get()).toHaveValue(''); + } +); + +it('renders definition for SettingType = JSON and can do operations', async () => { + const user = userEvent.setup(); + renderDefinition({ type: SettingType.JSON }); + + expect( + await ui.nameHeading('property.sonar.announcement.message.name').find() + ).toBeInTheDocument(); + + // Should show error message if JSON format is not valid + await user.type(ui.announcementInput.get(), 'invalid format'); + expect(ui.validationMsg.get()).toBeInTheDocument(); + await user.click(ui.jsonFormatButton.get()); + expect(ui.jsonFormatStatus.get()).toBeInTheDocument(); + + // Can save valid json and format it + await user.clear(ui.announcementInput.get()); + await user.type(ui.announcementInput.get(), '1'); + await user.click(ui.jsonFormatButton.get()); + expect(ui.jsonFormatStatus.query()).not.toBeInTheDocument(); + + await user.click(ui.saveButton.get()); + expect(ui.savedMsg.get()).toBeInTheDocument(); }); -describe('Handle change (and check)', () => { - it.each([ - ['empty, no default', mockDefinition(), '', 'settings.state.value_cant_be_empty_no_default'], - [ - 'empty, default', - mockDefinition({ defaultValue: 'dflt' }), - '', - 'settings.state.value_cant_be_empty', - ], - [ - 'invalid url', - mockDefinition({ key: 'sonar.core.serverBaseURL' }), - '%invalid', - 'settings.state.url_not_valid.%invalid', - ], - [ - 'valid url', - mockDefinition({ key: 'sonar.core.serverBaseURL' }), - 'http://www.sonarqube.org', - undefined, - ], - [ - 'invalid JSON', - mockDefinition({ type: SettingType.JSON }), - '{{broken: "json}', - 'Unexpected token { in JSON at position 1', - ], - ['valid JSON', mockDefinition({ type: SettingType.JSON }), '{"validJson": true}', undefined], - ])( - 'should handle change (and check value): %s', - (_caseName, definition, changedValue, expectedValidationMessage) => { - const wrapper = shallowRender({ definition }); - - wrapper.instance().handleChange(changedValue); - - expect(wrapper.state().changedValue).toBe(changedValue); - expect(wrapper.state().success).toBe(false); - expect(wrapper.state().validationMessage).toBe(expectedValidationMessage); - } +it('renders definition for SettingType = BOOLEAN and can do operations', async () => { + const user = userEvent.setup(); + renderDefinition({ + type: SettingType.BOOLEAN, + }); + + expect( + await ui.nameHeading('property.sonar.announcement.message.name').find() + ).toBeInTheDocument(); + + // Can toggle + await user.click(ui.toggleButton.get()); + expect(ui.toggleButton.get()).toBeChecked(); + + // Can cancel toggle + await user.click(ui.cancelButton.get()); + expect(ui.toggleButton.get()).not.toBeChecked(); + + // Can save toggle + await user.click(ui.toggleButton.get()); + await user.click(ui.saveButton.get()); + expect(ui.toggleButton.get()).toBeChecked(); + expect(ui.savedMsg.get()).toBeInTheDocument(); + + // Can reset toggle + await user.click( + ui.resetButton('settings.definition.reset.property.sonar.announcement.message.name').get() ); + await user.click(ui.resetButton().get()); + expect(ui.toggleButton.get()).not.toBeChecked(); }); -it('should handle cancel', () => { - const wrapper = shallowRender(); - wrapper.setState({ changedValue: 'whatever', validationMessage: 'something wrong' }); +it('renders definition for SettingType = SINGLE_SELECT_LIST and can do operations', async () => { + const user = userEvent.setup(); + renderDefinition({ + type: SettingType.SINGLE_SELECT_LIST, + options: ['first', 'second'], + }); - wrapper.instance().handleCancel(); + expect( + await ui.nameHeading('property.sonar.announcement.message.name').find() + ).toBeInTheDocument(); - expect(wrapper.state().changedValue).toBeUndefined(); - expect(wrapper.state().validationMessage).toBeUndefined(); -}); + // Can select option + expect(ui.selectOption('Select...').get()).toBeInTheDocument(); + await selectEvent.select(ui.announcementInput.get(), 'first'); + expect(ui.selectOption('first').get()).toBeInTheDocument(); -describe('handleSave', () => { - it('should ignore when value unchanged', () => { - const wrapper = shallowRender(); + // Can cancel action + await user.click(ui.cancelButton.get()); + expect(ui.selectOption('Select...').get()).toBeInTheDocument(); - wrapper.instance().handleSave(); + // Can save + await selectEvent.select(ui.announcementInput.get(), 'second'); + await user.click(ui.saveButton.get()); + expect(ui.savedMsg.get()).toBeInTheDocument(); + + // Can reset + await user.click( + ui.resetButton('settings.definition.reset.property.sonar.announcement.message.name').get() + ); + await user.click(ui.resetButton().get()); + expect(ui.selectOption('Select...').get()).toBeInTheDocument(); +}); - expect(wrapper.state().loading).toBe(false); - expect(setSettingValue).not.toHaveBeenCalled(); +it('renders definition for SettingType = FORMATTED_TEXT and can do operations', async () => { + const user = userEvent.setup(); + renderDefinition({ + type: SettingType.FORMATTED_TEXT, }); - it('should handle an empty value', () => { - const wrapper = shallowRender(); + expect( + await ui.nameHeading('property.sonar.announcement.message.name').find() + ).toBeInTheDocument(); - wrapper.setState({ changedValue: '' }); + // Should see no empty validation message + await user.type(ui.announcementInput.get(), ' '); + await user.click(ui.saveButton.get()); + expect(await ui.validationMsg.find()).toBeInTheDocument(); - wrapper.instance().handleSave(); + // Can cancel message + await user.clear(ui.announcementInput.get()); + await user.type(ui.announcementInput.get(), 'msg'); + await user.click(ui.cancelButton.get()); + expect(ui.announcementInput.get()).toHaveValue(''); - expect(wrapper.state().loading).toBe(false); - expect(wrapper.state().validationMessage).toBe('settings.state.value_cant_be_empty'); - expect(setSettingValue).not.toHaveBeenCalled(); - }); + // Can save formatted message + await user.type(ui.announcementInput.get(), 'https://ok.com'); + await user.click(ui.saveButton.get()); + expect(ui.savedMsg.get()).toBeInTheDocument(); + expect(ui.announcementInput.query()).not.toBeInTheDocument(); +}); - it('should save and update setting value', async () => { - const settingValue = mockSettingValue(); - (getValue as jest.Mock).mockResolvedValueOnce(settingValue); - const definition = mockDefinition(); - const wrapper = shallowRender({ definition }); +it('renders definition for multiValues type and can do operations', async () => { + const user = userEvent.setup(); + renderDefinition( + DEFAULT_DEFINITIONS_MOCK[2], + { + key: DEFAULT_DEFINITIONS_MOCK[2].key, + values: DEFAULT_DEFINITIONS_MOCK[2].defaultValue?.split(','), + }, + mockComponent() + ); - wrapper.setState({ changedValue: 'new value' }); + expect(await ui.nameHeading('property.sonar.javascript.globals.name').find()).toBeInTheDocument(); + expect(ui.multiValuesInput.getAll()).toHaveLength(4); - wrapper.instance().handleSave(); + // Should show validation message if no values + await user.click(ui.deleteValueButton.getAll()[0]); + await user.click(ui.deleteValueButton.getAll()[0]); + await user.click(ui.deleteValueButton.getAll()[0]); - expect(wrapper.state().loading).toBe(true); + expect(await ui.multiValuesInput.findAll()).toHaveLength(1); + expect(ui.validationMsg.get()).toBeInTheDocument(); - await waitAndUpdate(wrapper); + // Can cancel and return to previous + await user.click(ui.cancelButton.get()); + expect(ui.multiValuesInput.getAll()).toHaveLength(4); - expect(setSettingValue).toHaveBeenCalledWith(definition, 'new value', undefined); - expect(getValue).toHaveBeenCalledWith({ key: definition.key, component: undefined }); - expect(wrapper.state().changedValue).toBeUndefined(); - expect(wrapper.state().loading).toBe(false); - expect(wrapper.state().success).toBe(true); - expect(wrapper.state().settingValue).toBe(settingValue); + // Can update values and save + await user.type(last(ui.multiValuesInput.getAll()) as HTMLElement, 'new value'); + await user.click(ui.saveButton.get()); + expect(ui.savedMsg.get()).toBeInTheDocument(); + expect(ui.multiValuesInput.getAll()).toHaveLength(5); - jest.runAllTimers(); - expect(wrapper.state().success).toBe(false); - }); + // Can reset to default + await user.click( + ui.resetButton('settings.definition.reset.property.sonar.javascript.globals.name').get() + ); + await user.click(ui.resetButton().get()); + expect(ui.multiValuesInput.getAll()).toHaveLength(4); }); -it('should reset and update setting value', async () => { - const settingValue = mockSettingValue(); - (getValue as jest.Mock).mockResolvedValueOnce(settingValue); - const definition = mockDefinition(); - const wrapper = shallowRender({ definition }); +it('renders definition for SettingType = PROPERTY_SET and can do operations', async () => { + const user = userEvent.setup(); + renderDefinition(DEFAULT_DEFINITIONS_MOCK[5]); + + expect( + await ui.nameHeading('property.sonar.cobol.compilationConstants.name').find() + ).toBeInTheDocument(); + expect(screen.getByRole('columnheader', { name: 'Name' })).toBeInTheDocument(); + expect(screen.getByRole('columnheader', { name: 'Value' })).toBeInTheDocument(); + + // Should type new values + await user.type(ui.fieldsInput('name').get(), 'any name'); + expect(ui.fieldsInput('name').getAll()).toHaveLength(2); - wrapper.instance().handleReset(); + // Can cancel changes + await user.click(ui.cancelButton.get()); + expect(ui.fieldsInput('name').getAll()).toHaveLength(1); + expect(ui.fieldsInput('name').get()).toHaveValue(''); - expect(wrapper.state().loading).toBe(true); + // Can save new values + await user.type(ui.fieldsInput('name').get(), 'any name'); + await user.type(ui.fieldsInput('value').getAll()[0], 'any value'); + await user.click(ui.saveButton.get()); - await waitAndUpdate(wrapper); + expect(ui.savedMsg.get()).toBeInTheDocument(); + expect(ui.fieldsInput('name').getAll()[0]).toHaveValue('any name'); + expect(ui.fieldsInput('value').getAll()[0]).toHaveValue('any value'); - expect(resetSettingValue).toHaveBeenCalledWith({ keys: definition.key, component: undefined }); - expect(getValue).toHaveBeenCalledWith({ key: definition.key, component: undefined }); - expect(wrapper.state().changedValue).toBeUndefined(); - expect(wrapper.state().loading).toBe(false); - expect(wrapper.state().success).toBe(true); - expect(wrapper.state().settingValue).toBe(settingValue); + // Deleting previous value show validation message + await user.click(ui.deleteFieldsButton.get()); + expect(ui.validationMsg.get()).toBeInTheDocument(); - jest.runAllTimers(); - expect(wrapper.state().success).toBe(false); + // Can reset to default + await user.click(ui.resetButton(/settings.definition.reset/).get()); + await user.click(ui.resetButton().get()); + + expect(ui.savedMsg.get()).toBeInTheDocument(); + expect(ui.fieldsInput('name').get()).toHaveValue(''); + expect(ui.fieldsInput('value').get()).toHaveValue(''); }); -function shallowRender(props: Partial = {}) { - return shallow(); +it('renders secured definition and can do operations', async () => { + const user = userEvent.setup(); + const key = `${DEFAULT_DEFINITIONS_MOCK[0].key}.secured`; + settingsMock.setDefinition( + mockDefinition({ + ...DEFAULT_DEFINITIONS_MOCK[0], + key, + }) + ); + renderDefinition({ + key, + }); + + expect( + await ui.nameHeading('property.sonar.announcement.message.secured.name').find() + ).toBeInTheDocument(); + + // Can type new value and cancel change + await user.type(ui.securedInput.get(), 'Anything'); + expect(ui.securedInput.get()).toHaveValue('Anything'); + + // Can see validation message + await user.clear(ui.securedInput.get()); + expect(ui.validationMsg.get()).toBeInTheDocument(); + + // Can cancel change + await user.click(ui.cancelButton.get()); + expect(ui.securedInput.get()).toHaveValue(''); + + // Can save new value + await user.type(ui.securedInput.get(), 'Anything'); + await user.click(ui.saveButton.get()); + expect(ui.savedMsg.get()).toBeInTheDocument(); + expect(ui.securedInput.query()).not.toBeInTheDocument(); + + // Can change value by unlocking input + await user.click(ui.changeButton.get()); + expect(ui.securedInput.get()).toBeInTheDocument(); + + // Cam reset to default + await user.click(ui.resetButton(/settings.definition.reset/).get()); + await user.click(ui.resetButton().get()); + + expect(ui.savedMsg.get()).toBeInTheDocument(); +}); + +it('renders correctly for URL kind definition', async () => { + const user = userEvent.setup(); + renderDefinition({ key: 'sonar.auth.gitlab.url' }); + + // Show validation message + await user.type(ui.urlKindInput.get(), 'wrongurl'); + expect(ui.validationMsg.get()).toBeInTheDocument(); + expect(ui.saveButton.get()).toBeDisabled(); + + // Hides validation msg with correct url + await user.type(ui.urlKindInput.get(), 'http://hi.there'); + expect(ui.validationMsg.query()).not.toBeInTheDocument(); + expect(ui.saveButton.get()).toBeEnabled(); +}); + +function renderDefinition( + definition: Partial = {}, + initialSetting?: SettingValue, + component?: Component +) { + return renderComponent( + + ); } diff --git a/server/sonar-web/src/main/js/apps/settings/components/__tests__/NewCodePeriod-it.tsx b/server/sonar-web/src/main/js/apps/settings/components/__tests__/NewCodePeriod-it.tsx index c95f2e18bb8..8d355a23ccf 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/__tests__/NewCodePeriod-it.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/__tests__/NewCodePeriod-it.tsx @@ -17,110 +17,76 @@ * 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 userEvent from '@testing-library/user-event'; import * as React from 'react'; -import { getNewCodePeriod, setNewCodePeriod } from '../../../../api/newCodePeriod'; -import { waitAndUpdate } from '../../../../helpers/testUtils'; +import { byRole, byText } from 'testing-library-selector'; +import NewCodePeriodsServiceMock from '../../../../api/mocks/NewCodePeriodsServiceMock'; +import { renderComponent } from '../../../../helpers/testReactTestingUtils'; import NewCodePeriod from '../NewCodePeriod'; -jest.mock('../../../../api/newCodePeriod', () => ({ - getNewCodePeriod: jest.fn().mockResolvedValue({}), - setNewCodePeriod: jest.fn(() => Promise.resolve()), -})); +let newCodeMock: NewCodePeriodsServiceMock; -beforeEach(() => { - jest.clearAllMocks(); +beforeAll(() => { + newCodeMock = new NewCodePeriodsServiceMock(); }); -it('should render correctly', () => { - expect(shallowRender()).toMatchSnapshot(); +afterEach(() => { + newCodeMock.reset(); }); -it('should load the current new code period on mount', async () => { - const wrapper = shallowRender(); - await waitAndUpdate(wrapper); - expect(getNewCodePeriod).toHaveBeenCalledTimes(1); - expect(wrapper.state('currentSetting')).toBe('PREVIOUS_VERSION'); +const ui = { + newCodeTitle: byRole('heading', { name: 'settings.new_code_period.title' }), + savedMsg: byText('settings.state.saved'), + prevVersionRadio: byRole('radio', { name: /baseline.previous_version/ }), + daysNumberRadio: byRole('radio', { name: /baseline.number_days/ }), + daysInput: byRole('textbox'), + saveButton: byRole('button', { name: 'save' }), + cancelButton: byRole('button', { name: 'cancel' }), +}; + +it('renders and behaves as expected', async () => { + const user = userEvent.setup(); + renderNewCodePeriod(); + + expect(await ui.newCodeTitle.find()).toBeInTheDocument(); + // Previous version should be checked by default + expect(ui.prevVersionRadio.get()).toBeChecked(); + + // Can select number of days + await user.click(ui.daysNumberRadio.get()); + expect(ui.daysNumberRadio.get()).toBeChecked(); + + // Save should be disabled for zero or NaN + expect(ui.daysInput.get()).toHaveValue('30'); + await user.clear(ui.daysInput.get()); + await user.type(ui.daysInput.get(), '0'); + expect(await ui.saveButton.find()).toBeDisabled(); + await user.clear(ui.daysInput.get()); + await user.type(ui.daysInput.get(), 'asdas'); + expect(ui.saveButton.get()).toBeDisabled(); + await user.clear(ui.daysInput.get()); + + // Save enabled for valid days number + await user.type(ui.daysInput.get(), '10'); + expect(ui.saveButton.get()).toBeEnabled(); + + // Can cancel action + await user.click(ui.cancelButton.get()); + expect(ui.prevVersionRadio.get()).toBeChecked(); + + // Can save change + await user.click(ui.daysNumberRadio.get()); + await user.type(ui.daysInput.get(), '10'); + await user.click(ui.saveButton.get()); + expect(ui.savedMsg.get()).toBeInTheDocument(); + + await user.click(ui.prevVersionRadio.get()); + await user.click(ui.cancelButton.get()); + await user.click(ui.prevVersionRadio.get()); + await user.click(ui.saveButton.get()); + expect(ui.savedMsg.get()).toBeInTheDocument(); }); -it('should load the current new code period with value on mount', async () => { - (getNewCodePeriod as jest.Mock).mockResolvedValue({ type: 'NUMBER_OF_DAYS', value: '42' }); - - const wrapper = shallowRender(); - await waitAndUpdate(wrapper); - expect(getNewCodePeriod).toHaveBeenCalledTimes(1); - expect(wrapper.state('currentSetting')).toBe('NUMBER_OF_DAYS'); - expect(wrapper.state('days')).toBe('42'); -}); - -it('should only show the save button after changing the setting', async () => { - (getNewCodePeriod as jest.Mock).mockResolvedValue({ type: 'PREVIOUS_VERSION' }); - - const wrapper = shallowRender(); - await waitAndUpdate(wrapper); - - expect(wrapper.state('selected')).toBe('PREVIOUS_VERSION'); - expect(wrapper.find('SubmitButton')).toHaveLength(0); - - wrapper.instance().onSelectSetting('NUMBER_OF_DAYS'); - await waitAndUpdate(wrapper); - - expect(wrapper.find('SubmitButton')).toHaveLength(1); -}); - -it('should disable the button if the days are invalid', async () => { - (getNewCodePeriod as jest.Mock).mockResolvedValue({ type: 'NUMBER_OF_DAYS', value: '42' }); - - const wrapper = shallowRender(); - await waitAndUpdate(wrapper); - - wrapper.instance().onSelectDays('asd'); - await waitAndUpdate(wrapper); - - expect(wrapper.find('SubmitButton').first().prop('disabled')).toBe(true); - - wrapper.instance().onSelectDays('23'); - await waitAndUpdate(wrapper); - - expect(wrapper.find('SubmitButton').first().prop('disabled')).toBe(false); -}); - -it('should submit correctly', async () => { - (getNewCodePeriod as jest.Mock).mockResolvedValue({ type: 'NUMBER_OF_DAYS', value: '42' }); - - const preventDefault = jest.fn(); - - const wrapper = shallowRender(); - await waitAndUpdate(wrapper); - wrapper.instance().onSelectSetting('PREVIOUS_VERSION'); - await waitAndUpdate(wrapper); - - wrapper.find('form').simulate('submit', { preventDefault }); - - expect(preventDefault).toHaveBeenCalledTimes(1); - expect(setNewCodePeriod).toHaveBeenCalledWith({ type: 'PREVIOUS_VERSION', value: undefined }); - await waitAndUpdate(wrapper); - expect(wrapper.state('currentSetting')).toEqual(wrapper.state('selected')); -}); - -it('should submit correctly with days', async () => { - (getNewCodePeriod as jest.Mock).mockResolvedValue({ type: 'NUMBER_OF_DAYS', value: '42' }); - - const preventDefault = jest.fn(); - - const wrapper = shallowRender(); - await waitAndUpdate(wrapper); - wrapper.instance().onSelectDays('66'); - await waitAndUpdate(wrapper); - - wrapper.find('form').simulate('submit', { preventDefault }); - - expect(preventDefault).toHaveBeenCalledTimes(1); - expect(setNewCodePeriod).toHaveBeenCalledWith({ type: 'NUMBER_OF_DAYS', value: '66' }); - await waitAndUpdate(wrapper); - expect(wrapper.state('currentSetting')).toEqual(wrapper.state('selected')); -}); - -function shallowRender() { - return shallow(); +function renderNewCodePeriod() { + return renderComponent(); } diff --git a/server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/NewCodePeriod-it.tsx.snap b/server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/NewCodePeriod-it.tsx.snap deleted file mode 100644 index 24a811bedac..00000000000 --- a/server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/NewCodePeriod-it.tsx.snap +++ /dev/null @@ -1,57 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render correctly 1`] = ` -
      -
    • -
        -
      • -
        -
        -

        - settings.new_code_period.title -

        -
        - - learn_more - , - } - } - /> -

        - settings.new_code_period.description2 -

        -
        -
        -
        - -
        -
        -
      • -
      -
    • -
    -`; diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-it.tsx b/server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-it.tsx index 1070420b649..463e2b9666d 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-it.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-it.tsx @@ -75,7 +75,7 @@ afterEach(() => handler.resetValues()); const ui = { saveButton: byRole('button', { name: 'settings.authentication.saml.form.save' }), customMessageInformation: byText('settings.authentication.custom_message_information'), - enabledToggle: byRole('button', { name: 'off' }), + enabledToggle: byRole('switch'), testButton: byText('settings.authentication.saml.form.test'), textbox1: byRole('textbox', { name: 'test1' }), textbox2: byRole('textbox', { name: 'test2' }), @@ -172,7 +172,7 @@ describe('SAML tab', () => { await user.keyboard('new certificate'); // enable the configuration await user.click(ui.enabledToggle.get()); - expect(screen.getByRole('button', { name: 'on' })).toBeInTheDocument(); + expect(ui.enabledToggle.get()).toBeChecked(); await user.click(ui.saveButton.get()); expect(screen.getByText('settings.authentication.saml.form.save_success')).toBeInTheDocument(); @@ -180,7 +180,7 @@ describe('SAML tab', () => { await user.click(screen.getByRole('tab', { name: 'github GitHub' })); await user.click(screen.getByRole('tab', { name: 'SAML' })); - expect(screen.getByRole('button', { name: 'on' })).toBeInTheDocument(); + expect(ui.enabledToggle.get()).toBeChecked(); }); it('should handle and show errors to the user', async () => { diff --git a/server/sonar-web/src/main/js/apps/settings/components/inputs/InputForBoolean.tsx b/server/sonar-web/src/main/js/apps/settings/components/inputs/InputForBoolean.tsx index d752a6d397b..1c1643fcc00 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/inputs/InputForBoolean.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/inputs/InputForBoolean.tsx @@ -18,19 +18,25 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import Toggle from '../../../../components/controls/Toggle'; +import Toggle, { getToggleValue } from '../../../../components/controls/Toggle'; import { translate } from '../../../../helpers/l10n'; -import { DefaultSpecializedInputProps } from '../../utils'; +import { DefaultSpecializedInputProps, getPropertyName } from '../../utils'; interface Props extends DefaultSpecializedInputProps { value: string | boolean | undefined; } -export default function InputForBoolean({ onChange, name, value }: Props) { - const displayedValue = value != null ? value : false; +export default function InputForBoolean({ onChange, name, value, setting }: Props) { + const toggleValue = getToggleValue(value != null ? value : false); + return (
    - + {value == null && {translate('settings.not_set')}}
    ); diff --git a/server/sonar-web/src/main/js/apps/settings/components/inputs/InputForFormattedText.tsx b/server/sonar-web/src/main/js/apps/settings/components/inputs/InputForFormattedText.tsx index d2c57097c43..a1ef8a82097 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/inputs/InputForFormattedText.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/inputs/InputForFormattedText.tsx @@ -23,7 +23,7 @@ import { Button } from '../../../../components/controls/buttons'; import EditIcon from '../../../../components/icons/EditIcon'; import { translate } from '../../../../helpers/l10n'; import { sanitizeUserInput } from '../../../../helpers/sanitize'; -import { DefaultSpecializedInputProps } from '../../utils'; +import { DefaultSpecializedInputProps, getPropertyName } from '../../utils'; export default function InputForFormattedText(props: DefaultSpecializedInputProps) { const { isEditing, setting, name, value } = props; @@ -41,6 +41,7 @@ export default function InputForFormattedText(props: DefaultSpecializedInputProp {editMode ? (