Browse Source

SONAR-18593 Migrate rtl for settings

tags/10.0.0.68432
stanislavh 1 year ago
parent
commit
2ef9e45cf4
69 changed files with 859 additions and 1733 deletions
  1. 60
    0
      server/sonar-web/src/main/js/api/mocks/NewCodePeriodsServiceMock.ts
  2. 28
    9
      server/sonar-web/src/main/js/api/mocks/SettingsServiceMock.ts
  3. 1
    1
      server/sonar-web/src/main/js/apps/account/__tests__/Account-it.tsx
  4. 2
    2
      server/sonar-web/src/main/js/apps/component-measures/components/LeakPeriodLegend.tsx
  5. 2
    2
      server/sonar-web/src/main/js/apps/overview/branches/MeasuresPanelNoNewCode.tsx
  6. 4
    4
      server/sonar-web/src/main/js/apps/overview/branches/ProjectLeakPeriodInfo.tsx
  7. 2
    2
      server/sonar-web/src/main/js/apps/overview/branches/__tests__/ProjectLeakPeriodInfo-test.tsx
  8. 2
    2
      server/sonar-web/src/main/js/apps/overview/components/LeakPeriodLegend.tsx
  9. 1
    1
      server/sonar-web/src/main/js/apps/projectBaseline/components/BaselineSettingAnalysis.tsx
  10. 1
    1
      server/sonar-web/src/main/js/apps/projectBaseline/components/BaselineSettingDays.tsx
  11. 1
    1
      server/sonar-web/src/main/js/apps/projectBaseline/components/BaselineSettingPreviousVersion.tsx
  12. 1
    1
      server/sonar-web/src/main/js/apps/projectBaseline/components/BaselineSettingReferenceBranch.tsx
  13. 9
    8
      server/sonar-web/src/main/js/apps/projectBaseline/components/BranchBaselineSettingModal.tsx
  14. 2
    1
      server/sonar-web/src/main/js/apps/projectBaseline/components/BranchList.tsx
  15. 7
    7
      server/sonar-web/src/main/js/apps/projectBaseline/components/BranchListRow.tsx
  16. 22
    14
      server/sonar-web/src/main/js/apps/projectBaseline/components/ProjectBaselineApp.tsx
  17. 14
    6
      server/sonar-web/src/main/js/apps/projectBaseline/components/ProjectBaselineSelector.tsx
  18. 2
    1
      server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/BaselineSettingAnalysis-test.tsx
  19. 2
    1
      server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/BaselineSettingDays-test.tsx
  20. 2
    1
      server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/BaselineSettingPreviousVersion-test.tsx
  21. 2
    1
      server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/BaselineSettingReferenceBranch-test.tsx
  22. 7
    3
      server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/BranchBaselineSettingModal-test.tsx
  23. 7
    4
      server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/BranchList-test.tsx
  24. 19
    6
      server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/BranchListRow-test.tsx
  25. 8
    7
      server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/ProjectBaselineApp-test.tsx
  26. 16
    12
      server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/ProjectBaselineSelector-test.tsx
  27. 0
    0
      server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/ProjectBaselineApp-test.tsx.snap
  28. 30
    21
      server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/utils-test.ts
  29. 4
    22
      server/sonar-web/src/main/js/apps/projectBaseline/constants.ts
  30. 2
    2
      server/sonar-web/src/main/js/apps/projectBaseline/routes.tsx
  31. 12
    10
      server/sonar-web/src/main/js/apps/projectBaseline/utils.ts
  32. 6
    8
      server/sonar-web/src/main/js/apps/settings/components/Definition.tsx
  33. 33
    35
      server/sonar-web/src/main/js/apps/settings/components/NewCodePeriod.tsx
  34. 49
    0
      server/sonar-web/src/main/js/apps/settings/components/__tests__/AnalysisScope-test.tsx
  35. 335
    113
      server/sonar-web/src/main/js/apps/settings/components/__tests__/Definition-it.tsx
  36. 62
    96
      server/sonar-web/src/main/js/apps/settings/components/__tests__/NewCodePeriod-it.tsx
  37. 0
    57
      server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/NewCodePeriod-it.tsx.snap
  38. 3
    3
      server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-it.tsx
  39. 11
    5
      server/sonar-web/src/main/js/apps/settings/components/inputs/InputForBoolean.tsx
  40. 2
    1
      server/sonar-web/src/main/js/apps/settings/components/inputs/InputForFormattedText.tsx
  41. 6
    3
      server/sonar-web/src/main/js/apps/settings/components/inputs/InputForJSON.tsx
  42. 2
    0
      server/sonar-web/src/main/js/apps/settings/components/inputs/InputForSecured.tsx
  43. 3
    2
      server/sonar-web/src/main/js/apps/settings/components/inputs/InputForSingleSelectList.tsx
  44. 5
    3
      server/sonar-web/src/main/js/apps/settings/components/inputs/InputForText.tsx
  45. 1
    0
      server/sonar-web/src/main/js/apps/settings/components/inputs/MultiValueInput.tsx
  46. 7
    0
      server/sonar-web/src/main/js/apps/settings/components/inputs/PropertySetInput.tsx
  47. 0
    80
      server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/Input-test.tsx
  48. 0
    70
      server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForBoolean-test.tsx
  49. 0
    86
      server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForFormattedText-test.tsx
  50. 0
    80
      server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForJSON-test.tsx
  51. 0
    42
      server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForPassword-test.tsx
  52. 0
    96
      server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForSecured-test.tsx
  53. 0
    63
      server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForSingleSelectList-test.tsx
  54. 0
    57
      server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForText-test.tsx
  55. 0
    96
      server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/MultiValueInput-test.tsx
  56. 0
    46
      server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/PrimitiveInput-test.tsx
  57. 0
    86
      server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/SimpleInput-test.tsx
  58. 0
    51
      server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/__snapshots__/InputForJSON-test.tsx.snap
  59. 0
    349
      server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/__snapshots__/PrimitiveInput-test.tsx.snap
  60. 15
    7
      server/sonar-web/src/main/js/components/controls/Toggle.tsx
  61. 6
    3
      server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/Toggle-test.tsx.snap
  62. 1
    0
      server/sonar-web/src/main/js/components/controls/buttons.tsx
  63. 15
    8
      server/sonar-web/src/main/js/helpers/__tests__/periods-test.ts
  64. 8
    22
      server/sonar-web/src/main/js/helpers/mocks/new-code-period.ts
  65. 7
    0
      server/sonar-web/src/main/js/helpers/mocks/settings.ts
  66. 3
    3
      server/sonar-web/src/main/js/helpers/periods.ts
  67. 0
    1
      server/sonar-web/src/main/js/types/settings.ts
  68. 6
    5
      server/sonar-web/src/main/js/types/types.ts
  69. 1
    3
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 60
- 0
server/sonar-web/src/main/js/api/mocks/NewCodePeriodsServiceMock.ts View File

@@ -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<T>(response: T): Promise<T> {
return Promise.resolve(cloneDeep(response));
}
}

+ 28
- 9
server/sonar-web/src/main/js/api/mocks/SettingsServiceMock.ts View File

@@ -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<void> => {
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);

+ 1
- 1
server/sonar-web/src/main/js/apps/account/__tests__/Account-it.tsx View File

@@ -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();

+ 2
- 2
server/sonar-web/src/main/js/apps/component-measures/components/LeakPeriodLegend.tsx View File

@@ -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<Props & WrappedCompone
</div>
);

if (period.mode === 'days' || period.mode === 'NUMBER_OF_DAYS') {
if (period.mode === 'days' || period.mode === NewCodePeriodSettingType.NUMBER_OF_DAYS) {
return label;
}


+ 2
- 2
server/sonar-web/src/main/js/apps/overview/branches/MeasuresPanelNoNewCode.tsx View File

@@ -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

+ 4
- 4
server/sonar-web/src/main/js/apps/overview/branches/ProjectLeakPeriodInfo.tsx View File

@@ -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 <div className="note spacer-top">{leakPeriodLabel} </div>;
}

+ 2
- 2
server/sonar-web/src/main/js/apps/overview/branches/__tests__/ProjectLeakPeriodInfo-test.tsx View File

@@ -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();

+ 2
- 2
server/sonar-web/src/main/js/apps/overview/components/LeakPeriodLegend.tsx View File

@@ -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<Props & WrappedCompone
return null;
}

if (period.mode === 'days' || period.mode === 'NUMBER_OF_DAYS') {
if (period.mode === 'days' || period.mode === NewCodePeriodSettingType.NUMBER_OF_DAYS) {
return (
<div className="overview-legend overview-legend-spaced-line">
{translateWithParameters('overview.new_code_period_x', leakPeriodLabel)}

+ 1
- 1
server/sonar-web/src/main/js/apps/projectBaseline/components/BaselineSettingAnalysis.tsx View File

@@ -32,7 +32,7 @@ export default function BaselineSettingAnalysis({ disabled, onSelect, selected }
return (
<RadioCard
disabled={disabled}
onClick={() => onSelect('SPECIFIC_ANALYSIS')}
onClick={() => onSelect(NewCodePeriodSettingType.SPECIFIC_ANALYSIS)}
selected={selected}
title={translate('baseline.specific_analysis')}
>

+ 1
- 1
server/sonar-web/src/main/js/apps/projectBaseline/components/BaselineSettingDays.tsx View File

@@ -41,7 +41,7 @@ export default function BaselineSettingDays(props: Props) {
<RadioCard
className={className}
disabled={disabled}
onClick={() => onSelect('NUMBER_OF_DAYS')}
onClick={() => onSelect(NewCodePeriodSettingType.NUMBER_OF_DAYS)}
selected={selected}
title={translate('baseline.number_days')}
>

+ 1
- 1
server/sonar-web/src/main/js/apps/projectBaseline/components/BaselineSettingPreviousVersion.tsx View File

@@ -34,7 +34,7 @@ export default function BaselineSettingPreviousVersion(props: Props) {
return (
<RadioCard
disabled={disabled}
onClick={() => onSelect('PREVIOUS_VERSION')}
onClick={() => onSelect(NewCodePeriodSettingType.PREVIOUS_VERSION)}
selected={selected}
title={
translate('baseline.previous_version') + (isDefault ? ` (${translate('default')})` : '')

+ 1
- 1
server/sonar-web/src/main/js/apps/projectBaseline/components/BaselineSettingReferenceBranch.tsx View File

@@ -98,7 +98,7 @@ export default function BaselineSettingReferenceBranch(props: BaselineSettingRef
<RadioCard
className={className}
disabled={disabled}
onClick={() => props.onSelect('REFERENCE_BRANCH')}
onClick={() => props.onSelect(NewCodePeriodSettingType.REFERENCE_BRANCH)}
selected={selected}
title={translate('baseline.reference_branch')}
>

+ 9
- 8
server/sonar-web/src/main/js/apps/projectBaseline/components/BranchBaselineSettingModal.tsx View File

@@ -60,9 +60,10 @@ export default class BranchBaselineSettingModal extends React.PureComponent<Prop
const defaultBranch = otherBranches.length > 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<Prop
<BaselineSettingPreviousVersion
isDefault={false}
onSelect={this.handleSelectSetting}
selected={selected === 'PREVIOUS_VERSION'}
selected={selected === NewCodePeriodSettingType.PREVIOUS_VERSION}
/>
<BaselineSettingDays
days={days}
@@ -172,22 +173,22 @@ export default class BranchBaselineSettingModal extends React.PureComponent<Prop
isValid={isValid}
onChangeDays={this.handleSelectDays}
onSelect={this.handleSelectSetting}
selected={selected === 'NUMBER_OF_DAYS'}
selected={selected === NewCodePeriodSettingType.NUMBER_OF_DAYS}
/>
<BaselineSettingAnalysis
onSelect={this.handleSelectSetting}
selected={selected === 'SPECIFIC_ANALYSIS'}
selected={selected === NewCodePeriodSettingType.SPECIFIC_ANALYSIS}
/>
<BaselineSettingReferenceBranch
branchList={branchList.map(this.branchToOption)}
onChangeReferenceBranch={this.handleSelectReferenceBranch}
onSelect={this.handleSelectSetting}
referenceBranch={referenceBranch}
selected={selected === 'REFERENCE_BRANCH'}
selected={selected === NewCodePeriodSettingType.REFERENCE_BRANCH}
settingLevel="branch"
/>
</div>
{selected === 'SPECIFIC_ANALYSIS' && (
{selected === NewCodePeriodSettingType.SPECIFIC_ANALYSIS && (
<BranchAnalysisList
analysis={analysis}
branch={branch.name}

+ 2
- 1
server/sonar-web/src/main/js/apps/projectBaseline/components/BranchList.tsx View File

@@ -24,6 +24,7 @@ import { isBranch, sortBranches } from '../../../helpers/branch-like';
import { translate } from '../../../helpers/l10n';
import { Branch, BranchLike, BranchWithNewCodePeriod } from '../../../types/branch-like';
import { Component, NewCodePeriod } from '../../../types/types';
import { DEFAULT_GENERAL_SETTING_TYPE } from '../constants';
import BranchBaselineSettingModal from './BranchBaselineSettingModal';
import BranchListRow from './BranchListRow';

@@ -74,7 +75,7 @@ export default class BranchList extends React.PureComponent<Props, State> {
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 },

+ 7
- 7
server/sonar-web/src/main/js/apps/projectBaseline/components/BranchListRow.tsx View File

@@ -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)
);
}

server/sonar-web/src/main/js/apps/projectBaseline/components/App.tsx → server/sonar-web/src/main/js/apps/projectBaseline/components/ProjectBaselineApp.tsx View File

@@ -36,6 +36,7 @@ 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';
@@ -66,11 +67,7 @@ interface State {

const DEFAULT_NUMBER_OF_DAYS = '30';

const DEFAULT_GENERAL_SETTING: { type: NewCodePeriodSettingType } = {
type: 'PREVIOUS_VERSION',
};

export class App extends React.PureComponent<Props, State> {
export class ProjectBaselineApp extends React.PureComponent<Props, State> {
mounted = false;
state: State = {
branchList: [],
@@ -107,7 +104,8 @@ export class App extends React.PureComponent<Props, State> {
const { referenceBranch } = this.state;

const defaultDays =
(generalSetting.type === 'NUMBER_OF_DAYS' && generalSetting.value) || DEFAULT_NUMBER_OF_DAYS;
(generalSetting.type === NewCodePeriodSettingType.NUMBER_OF_DAYS && generalSetting.value) ||
DEFAULT_NUMBER_OF_DAYS;

return {
loading: false,
@@ -116,10 +114,15 @@ export class App extends React.PureComponent<Props, State> {
generalSetting,
selected: currentSetting || generalSetting.type,
overrideGeneralSetting: Boolean(currentSetting),
days: (currentSetting === 'NUMBER_OF_DAYS' && currentSettingValue) || defaultDays,
analysis: (currentSetting === 'SPECIFIC_ANALYSIS' && currentSettingValue) || '',
days:
(currentSetting === NewCodePeriodSettingType.NUMBER_OF_DAYS && currentSettingValue) ||
defaultDays,
analysis:
(currentSetting === NewCodePeriodSettingType.SPECIFIC_ANALYSIS && currentSettingValue) ||
'',
referenceBranch:
(currentSetting === 'REFERENCE_BRANCH' && currentSettingValue) || referenceBranch,
(currentSetting === NewCodePeriodSettingType.REFERENCE_BRANCH && currentSettingValue) ||
referenceBranch,
};
}

@@ -143,10 +146,12 @@ export class App extends React.PureComponent<Props, State> {
([generalSetting, setting]) => {
if (this.mounted) {
if (!generalSetting.type) {
generalSetting = DEFAULT_GENERAL_SETTING;
generalSetting = { type: DEFAULT_GENERAL_SETTING_TYPE };
}
const currentSettingValue = setting.value;
const currentSetting = setting.inherited ? undefined : setting.type || 'PREVIOUS_VERSION';
const currentSetting = setting.inherited
? undefined
: setting.type || DEFAULT_GENERAL_SETTING_TYPE;

this.setState(
this.getUpdatedState({
@@ -191,8 +196,11 @@ export class App extends React.PureComponent<Props, State> {

handleCancel = () =>
this.setState(
({ generalSetting = DEFAULT_GENERAL_SETTING, currentSetting, currentSettingValue }) =>
this.getUpdatedState({ generalSetting, currentSetting, currentSettingValue })
({
generalSetting = { type: DEFAULT_GENERAL_SETTING_TYPE },
currentSetting,
currentSettingValue,
}) => this.getUpdatedState({ generalSetting, currentSetting, currentSettingValue })
);

handleSelectSetting = (selected?: NewCodePeriodSettingType) => this.setState({ selected });
@@ -322,4 +330,4 @@ export class App extends React.PureComponent<Props, State> {
}
}

export default withComponentContext(withAvailableFeatures(withAppStateContext(App)));
export default withComponentContext(withAvailableFeatures(withAppStateContext(ProjectBaselineApp)));

+ 14
- 6
server/sonar-web/src/main/js/apps/projectBaseline/components/ProjectBaselineSelector.tsx View File

@@ -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
<BaselineSettingPreviousVersion
disabled={!overrideGeneralSetting}
onSelect={props.onSelectSetting}
selected={overrideGeneralSetting && selected === 'PREVIOUS_VERSION'}
selected={
overrideGeneralSetting && selected === NewCodePeriodSettingType.PREVIOUS_VERSION
}
/>
<BaselineSettingDays
days={days}
@@ -146,7 +148,9 @@ export default function ProjectBaselineSelector(props: ProjectBaselineSelectorPr
isValid={isValid}
onChangeDays={props.onSelectDays}
onSelect={props.onSelectSetting}
selected={overrideGeneralSetting && selected === 'NUMBER_OF_DAYS'}
selected={
overrideGeneralSetting && selected === NewCodePeriodSettingType.NUMBER_OF_DAYS
}
/>
{branchesEnabled ? (
<BaselineSettingReferenceBranch
@@ -155,18 +159,22 @@ export default function ProjectBaselineSelector(props: ProjectBaselineSelectorPr
onChangeReferenceBranch={props.onSelectReferenceBranch}
onSelect={props.onSelectSetting}
referenceBranch={referenceBranch || ''}
selected={overrideGeneralSetting && selected === 'REFERENCE_BRANCH'}
selected={
overrideGeneralSetting && selected === NewCodePeriodSettingType.REFERENCE_BRANCH
}
settingLevel="project"
/>
) : (
<BaselineSettingAnalysis
disabled={!overrideGeneralSetting}
onSelect={props.onSelectSetting}
selected={overrideGeneralSetting && selected === 'SPECIFIC_ANALYSIS'}
selected={
overrideGeneralSetting && selected === NewCodePeriodSettingType.SPECIFIC_ANALYSIS
}
/>
)}
</div>
{selected === 'SPECIFIC_ANALYSIS' && (
{selected === NewCodePeriodSettingType.SPECIFIC_ANALYSIS && (
<BranchAnalysisList
analysis={analysis || ''}
branch={branch.name}

+ 2
- 1
server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/BaselineSettingAnalysis-test.tsx View File

@@ -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<Props> = {}) {

+ 2
- 1
server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/BaselineSettingDays-test.tsx View File

@@ -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', () => {

+ 2
- 1
server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/BaselineSettingPreviousVersion-test.tsx View File

@@ -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<Props> = {}) {

+ 2
- 1
server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/BaselineSettingReferenceBranch-test.tsx View File

@@ -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', () => {

+ 7
- 3
server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/BranchBaselineSettingModal-test.tsx View File

@@ -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',
});

+ 7
- 4
server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/BranchList-test.tsx View File

@@ -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<BranchList['props']> = {}) {
<BranchList
branchList={[]}
component={mockComponent()}
inheritedSetting={{ type: 'PREVIOUS_VERSION' }}
inheritedSetting={{ type: NewCodePeriodSettingType.PREVIOUS_VERSION }}
{...props}
/>
);

+ 19
- 6
server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/BranchListRow-test.tsx View File

@@ -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,
});


server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/App-test.tsx → server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/ProjectBaselineApp-test.tsx View File

@@ -28,7 +28,8 @@ import { mockBranch, mockMainBranch, mockPullRequest } from '../../../../helpers
import { mockComponent } from '../../../../helpers/mocks/component';
import { mockAppState } from '../../../../helpers/testMocks';
import { mockEvent, waitAndUpdate } from '../../../../helpers/testUtils';
import { App } from '../App';
import { NewCodePeriodSettingType } from '../../../../types/types';
import { ProjectBaselineApp } from '../ProjectBaselineApp';

jest.mock('../../../../api/newCodePeriod', () => ({
getNewCodePeriod: jest.fn().mockResolvedValue({}),
@@ -75,12 +76,12 @@ it('should save correctly', async () => {
const component = mockComponent();
const wrapper = shallowRender({ component });
await waitAndUpdate(wrapper);
wrapper.setState({ selected: 'NUMBER_OF_DAYS', days: '23' });
wrapper.setState({ selected: NewCodePeriodSettingType.NUMBER_OF_DAYS, days: '23' });
wrapper.instance().handleSubmit(mockEvent());
await waitAndUpdate(wrapper);
expect(setNewCodePeriod).toHaveBeenCalledWith({
project: component.key,
type: 'NUMBER_OF_DAYS',
type: NewCodePeriodSettingType.NUMBER_OF_DAYS,
value: '23',
});
expect(wrapper.state('currentSetting')).toEqual(wrapper.state('selected'));
@@ -92,7 +93,7 @@ it('should handle errors gracefully', async () => {
(resetNewCodePeriod as jest.Mock).mockRejectedValue('error');

const wrapper = shallowRender();
wrapper.setState({ selected: 'PREVIOUS_VERSION' });
wrapper.setState({ selected: NewCodePeriodSettingType.PREVIOUS_VERSION });
await waitAndUpdate(wrapper);

expect(wrapper.state('loading')).toBe(false);
@@ -104,9 +105,9 @@ it('should handle errors gracefully', async () => {
expect(wrapper.state('saving')).toBe(false);
});

function shallowRender(props: Partial<App['props']> = {}) {
return shallow<App>(
<App
function shallowRender(props: Partial<ProjectBaselineApp['props']> = {}) {
return shallow<ProjectBaselineApp>(
<ProjectBaselineApp
branchLike={mockBranch()}
branchLikes={[mockMainBranch()]}
appState={mockAppState({ canAdmin: true })}

+ 16
- 12
server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/ProjectBaselineSelector-test.tsx View File

@@ -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,
});


server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/App-test.tsx.snap → server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/ProjectBaselineApp-test.tsx.snap View File


+ 30
- 21
server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/utils-test.ts View File

@@ -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,
})

server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForNumber-test.tsx → server/sonar-web/src/main/js/apps/projectBaseline/constants.ts View File

@@ -17,26 +17,8 @@
* 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 { mockSetting } from '../../../../../helpers/mocks/settings';
import InputForNumber from '../InputForNumber';
import SimpleInput from '../SimpleInput';

it('should render SimpleInput', () => {
const onChange = jest.fn();
const simpleInput = shallow(
<InputForNumber
isDefault={false}
name="foo"
onChange={onChange}
setting={mockSetting()}
value={17}
/>
).find(SimpleInput);
expect(simpleInput.length).toBe(1);
expect(simpleInput.prop('name')).toBe('foo');
expect(simpleInput.prop('value')).toBe(17);
expect(simpleInput.prop('type')).toBe('text');
expect(simpleInput.prop('onChange')).toBeDefined();
});
import { NewCodePeriodSettingType } from '../../types/types';

export const DEFAULT_GENERAL_SETTING_TYPE: NewCodePeriodSettingType =
NewCodePeriodSettingType.PREVIOUS_VERSION;

+ 2
- 2
server/sonar-web/src/main/js/apps/projectBaseline/routes.tsx View File

@@ -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 = () => <Route path="baseline" element={<App />} />;
const routes = () => <Route path="baseline" element={<ProjectBaselineApp />} />;

export default routes;

+ 12
- 10
server/sonar-web/src/main/js/apps/projectBaseline/utils.ts View File

@@ -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 };
}

+ 6
- 8
server/sonar-web/src/main/js/apps/settings/components/Definition.tsx View File

@@ -90,10 +90,9 @@ export default class Definition extends React.PureComponent<Props, State> {
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<Props, State> {
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 });

+ 33
- 35
server/sonar-web/src/main/js/apps/settings/components/NewCodePeriod.tsx View File

@@ -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 (
<ul className="settings-sub-categories-list">
@@ -174,7 +172,7 @@ export default class NewCodePeriod extends React.PureComponent<{}, State> {
<BaselineSettingPreviousVersion
isDefault={true}
onSelect={this.onSelectSetting}
selected={selected === 'PREVIOUS_VERSION'}
selected={selected === NewCodePeriodSettingType.PREVIOUS_VERSION}
/>
<BaselineSettingDays
className="spacer-top"
@@ -183,7 +181,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 && (
<div className="big-spacer-top">

+ 49
- 0
server/sonar-web/src/main/js/apps/settings/components/__tests__/AnalysisScope-test.tsx View File

@@ -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<AdditionalCategoryComponentProps> = {}) {
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)}</>);
}

+ 335
- 113
server/sonar-web/src/main/js/apps/settings/components/__tests__/Definition-it.tsx View File

@@ -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<Definition['props']> = {}) {
return shallow<Definition>(<Definition definition={mockDefinition()} {...props} />);
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<ExtendedSettingDefinition> = {},
initialSetting?: SettingValue,
component?: Component
) {
return renderComponent(
<Definition
definition={{ ...DEFAULT_DEFINITIONS_MOCK[0], ...definition }}
initialSettingValue={initialSetting}
component={component}
/>
);
}

+ 62
- 96
server/sonar-web/src/main/js/apps/settings/components/__tests__/NewCodePeriod-it.tsx View File

@@ -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<NewCodePeriod>(<NewCodePeriod />);
function renderNewCodePeriod() {
return renderComponent(<NewCodePeriod />);
}

+ 0
- 57
server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/NewCodePeriod-it.tsx.snap View File

@@ -1,57 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should render correctly 1`] = `
<ul
className="settings-sub-categories-list"
>
<li>
<ul
className="settings-definitions-list"
>
<li>
<div
className="settings-definition"
>
<div
className="settings-definition-left"
>
<h3
className="settings-definition-name"
title="settings.new_code_period.title"
>
settings.new_code_period.title
</h3>
<div
className="small big-spacer-top"
>
<FormattedMessage
defaultMessage="settings.new_code_period.description"
id="settings.new_code_period.description"
values={
{
"link": <withAppStateContext(DocLink)
to="/project-administration/defining-new-code/"
>
learn_more
</withAppStateContext(DocLink)>,
}
}
/>
<p
className="spacer-top"
>
settings.new_code_period.description2
</p>
</div>
</div>
<div
className="settings-definition-right"
>
<DeferredSpinner />
</div>
</div>
</li>
</ul>
</li>
</ul>
`;

+ 3
- 3
server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-it.tsx View File

@@ -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 () => {

+ 11
- 5
server/sonar-web/src/main/js/apps/settings/components/inputs/InputForBoolean.tsx View File

@@ -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 (
<div className="display-inline-block text-top">
<Toggle name={name} onChange={onChange} value={displayedValue} />
<Toggle
name={name}
onChange={onChange}
value={toggleValue}
ariaLabel={getPropertyName(setting.definition)}
/>
{value == null && <span className="spacer-left note">{translate('settings.not_set')}</span>}
</div>
);

+ 2
- 1
server/sonar-web/src/main/js/apps/settings/components/inputs/InputForFormattedText.tsx View File

@@ -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 ? (
<div className="display-flex-row">
<textarea
aria-label={getPropertyName(setting.definition)}
className="settings-large-input text-top spacer-right"
name={name}
onChange={handleInputChange}

+ 6
- 3
server/sonar-web/src/main/js/apps/settings/components/inputs/InputForJSON.tsx View File

@@ -21,7 +21,7 @@ import * as React from 'react';
import { Button } from '../../../../components/controls/buttons';
import { Alert } from '../../../../components/ui/Alert';
import { translate } from '../../../../helpers/l10n';
import { DefaultSpecializedInputProps } from '../../utils';
import { DefaultSpecializedInputProps, getPropertyName } from '../../utils';

const JSON_SPACE_SIZE = 4;

@@ -49,15 +49,18 @@ export default class InputForJSON extends React.PureComponent<DefaultSpecialized
};

render() {
const { value, name, setting } = this.props;
const { formatError } = this.state;

return (
<div className="display-flex-end">
<textarea
className="settings-large-input text-top monospaced spacer-right"
name={this.props.name}
name={name}
onChange={this.handleInputChange}
rows={5}
value={this.props.value || ''}
value={value || ''}
aria-label={getPropertyName(setting.definition)}
/>
<div>
{formatError && <Alert variant="info">{translate('settings.json.format_error')} </Alert>}

+ 2
- 0
server/sonar-web/src/main/js/apps/settings/components/inputs/InputForSecured.tsx View File

@@ -25,6 +25,7 @@ import { translate } from '../../../../helpers/l10n';
import {
DefaultInputProps,
DefaultSpecializedInputProps,
getPropertyName,
getUniqueName,
isDefaultOrInherited,
} from '../../utils';
@@ -73,6 +74,7 @@ export default class InputForSecured extends React.PureComponent<Props, State> {
<>
<input className="hidden" type="password" />
<Input
aria-label={getPropertyName(setting.definition)}
autoComplete="off"
className="js-setting-input settings-large-input"
isDefault={isDefaultOrInherited(setting)}

+ 3
- 2
server/sonar-web/src/main/js/apps/settings/components/inputs/InputForSingleSelectList.tsx View File

@@ -20,7 +20,7 @@
import * as React from 'react';
import Select from '../../../../components/controls/Select';
import { ExtendedSettingDefinition } from '../../../../types/settings';
import { DefaultSpecializedInputProps } from '../../utils';
import { DefaultSpecializedInputProps, getPropertyName } from '../../utils';

type Props = DefaultSpecializedInputProps & Pick<ExtendedSettingDefinition, 'options'>;

@@ -30,7 +30,7 @@ export default class InputForSingleSelectList extends React.PureComponent<Props>
};

render() {
const { options: opts, name, value } = this.props;
const { options: opts, name, value, setting } = this.props;

const options = opts.map((option) => ({
label: option,
@@ -42,6 +42,7 @@ export default class InputForSingleSelectList extends React.PureComponent<Props>
className="settings-large-input"
name={name}
onChange={this.handleInputChange}
aria-label={getPropertyName(setting.definition)}
options={options}
value={options.find((option) => option.value === value)}
/>

+ 5
- 3
server/sonar-web/src/main/js/apps/settings/components/inputs/InputForText.tsx View File

@@ -18,7 +18,7 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import { DefaultSpecializedInputProps } from '../../utils';
import { DefaultSpecializedInputProps, getPropertyName } from '../../utils';

export default class InputForText extends React.PureComponent<DefaultSpecializedInputProps> {
handleInputChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
@@ -26,13 +26,15 @@ export default class InputForText extends React.PureComponent<DefaultSpecialized
};

render() {
const { setting, name, value } = this.props;
return (
<textarea
className="settings-large-input text-top"
name={this.props.name}
name={name}
onChange={this.handleInputChange}
rows={5}
value={this.props.value || ''}
value={value || ''}
aria-label={getPropertyName(setting.definition)}
/>
);
}

+ 1
- 0
server/sonar-web/src/main/js/apps/settings/components/inputs/MultiValueInput.tsx View File

@@ -72,6 +72,7 @@ export default class MultiValueInput extends React.PureComponent<DefaultSpeciali

render() {
const displayedValue = [...this.ensureValue(), ...getEmptyValue(this.props.setting.definition)];

return (
<div>
<ul>

+ 7
- 0
server/sonar-web/src/main/js/apps/settings/components/inputs/PropertySetInput.tsx View File

@@ -19,9 +19,11 @@
*/
import * as React from 'react';
import { DeleteButton } from '../../../../components/controls/buttons';
import { translateWithParameters } from '../../../../helpers/l10n';
import {
DefaultSpecializedInputProps,
getEmptyValue,
getPropertyName,
getUniqueName,
isCategoryDefinition,
} from '../../utils';
@@ -75,6 +77,11 @@ export default class PropertySetInput extends React.PureComponent<DefaultSpecial
<td className="thin nowrap text-middle">
{!isLast && (
<DeleteButton
aria-label={translateWithParameters(
'settings.definitions.delete_fields',
getPropertyName(setting.definition),
index
)}
className="js-remove-value"
onClick={() => this.handleDeleteValue(index)}
/>

+ 0
- 80
server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/Input-test.tsx View File

@@ -1,80 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2023 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { shallow } from 'enzyme';
import * as React from 'react';
import { mockDefinition, mockSetting } from '../../../../../helpers/mocks/settings';
import { Setting, SettingType } from '../../../../../types/settings';
import { DefaultInputProps } from '../../../utils';
import Input from '../Input';
import InputForSecured from '../InputForSecured';
import MultiValueInput from '../MultiValueInput';
import PrimitiveInput from '../PrimitiveInput';
import PropertySetInput from '../PropertySetInput';

it('should render PrimitiveInput', () => {
const onChange = jest.fn();
const input = shallowRender({ onChange }).find(PrimitiveInput);
expect(input.length).toBe(1);
expect(input.prop('value')).toBe('foo');
expect(input.prop('onChange')).toBe(onChange);
});

it('should render Secured input', () => {
const setting: Setting = mockSetting({
key: 'foo.secured',
definition: mockDefinition({ key: 'foo.secured', type: SettingType.PROPERTY_SET }),
});
const onChange = jest.fn();
const input = shallowRender({ onChange, setting }).find(InputForSecured);
expect(input.length).toBe(1);
expect(input.prop('value')).toBe('foo');
expect(input.prop('onChange')).toBe(onChange);
});

it('should render MultiValueInput', () => {
const setting = mockSetting({
definition: mockDefinition({ multiValues: true }),
});
const onChange = jest.fn();
const value = ['foo', 'bar'];
const input = shallowRender({ onChange, setting, value }).find(MultiValueInput);
expect(input.length).toBe(1);
expect(input.prop('setting')).toBe(setting);
expect(input.prop('value')).toBe(value);
expect(input.prop('onChange')).toBe(onChange);
});

it('should render PropertySetInput', () => {
const setting: Setting = mockSetting({
definition: mockDefinition({ type: SettingType.PROPERTY_SET }),
});

const onChange = jest.fn();
const value = [{ foo: 'bar' }];
const input = shallowRender({ onChange, setting, value }).find(PropertySetInput);
expect(input.length).toBe(1);
expect(input.prop('setting')).toBe(setting);
expect(input.prop('value')).toBe(value);
expect(input.prop('onChange')).toBe(onChange);
});

function shallowRender(props: Partial<DefaultInputProps> = {}) {
return shallow(<Input onChange={jest.fn()} setting={mockSetting()} value="foo" {...props} />);
}

+ 0
- 70
server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForBoolean-test.tsx View File

@@ -1,70 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2023 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { shallow } from 'enzyme';
import * as React from 'react';
import { mockSetting } from '../../../../../helpers/mocks/settings';
import { DefaultSpecializedInputProps } from '../../../utils';
import InputForBoolean from '../InputForBoolean';

it('should render Toggle', () => {
const onChange = jest.fn();
const toggle = shallowRender({ onChange }).find('Toggle');
expect(toggle.length).toBe(1);
expect(toggle.prop('name')).toBe('foo');
expect(toggle.prop('value')).toBe(true);
expect(toggle.prop('onChange')).toBeDefined();
});

it('should render Toggle without value', () => {
const onChange = jest.fn();
const input = shallowRender({ onChange, value: undefined });
const toggle = input.find('Toggle');
expect(toggle.length).toBe(1);
expect(toggle.prop('name')).toBe('foo');
expect(toggle.prop('value')).toBe(false);
expect(toggle.prop('onChange')).toBeDefined();
expect(input.find('.note').length).toBe(1);
});

it('should call onChange', () => {
const onChange = jest.fn();

const input = shallowRender({ onChange, value: true });
const toggle = input.find('Toggle');
expect(toggle.length).toBe(1);
expect(toggle.prop('onChange')).toBeDefined();

toggle.prop<Function>('onChange')(false);

expect(onChange).toHaveBeenCalledWith(false);
});

function shallowRender(props: Partial<DefaultSpecializedInputProps> = {}) {
return shallow(
<InputForBoolean
isDefault={false}
name="foo"
onChange={jest.fn()}
setting={mockSetting()}
value={true}
{...props}
/>
);
}

+ 0
- 86
server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForFormattedText-test.tsx View File

@@ -1,86 +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 { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import * as React from 'react';
import { mockSetting } from '../../../../../helpers/mocks/settings';
import { renderComponent } from '../../../../../helpers/testReactTestingUtils';
import { DefaultSpecializedInputProps } from '../../../utils';
import InputForFormattedText from '../InputForFormattedText';

it('should render correctly with no value for login message', () => {
renderInputForFormattedText();
expect(screen.getByRole('textbox')).toBeInTheDocument();
});

it('should render correctly with a value for login message', () => {
renderInputForFormattedText({
setting: mockSetting({ values: ['*text*', 'text'], hasValue: true }),
});
expect(screen.getByRole('button', { name: 'edit' })).toBeInTheDocument();
expect(screen.getByText('text')).toBeInTheDocument();
});

it('should render correctly with a value for login message if hasValue is set', () => {
renderInputForFormattedText({
setting: mockSetting({ hasValue: true }),
});
expect(screen.getByRole('button', { name: 'edit' })).toBeInTheDocument();
});

it('should render editMode when value is empty', () => {
renderInputForFormattedText({
value: '',
});
expect(screen.getByRole('textbox')).toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'edit' })).not.toBeInTheDocument();
});

it('should render correctly if in editMode', async () => {
const user = userEvent.setup();
const onChange = jest.fn();

renderInputForFormattedText({
setting: mockSetting({ values: ['*text*', 'text'], hasValue: true }),
isEditing: true,
onChange,
});
expect(screen.getByRole('textbox')).toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'edit' })).not.toBeInTheDocument();

await user.click(screen.getByRole('textbox'));
await user.keyboard('N');
expect(onChange).toHaveBeenCalledTimes(1);
});

function renderInputForFormattedText(props: Partial<DefaultSpecializedInputProps> = {}) {
renderComponent(
<InputForFormattedText
onEditing={jest.fn()}
isEditing={false}
isDefault={true}
name="name"
onChange={jest.fn()}
setting={mockSetting({ value: undefined, hasValue: false })}
value="*text*"
{...props}
/>
);
}

+ 0
- 80
server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForJSON-test.tsx View File

@@ -1,80 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2023 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { shallow } from 'enzyme';
import * as React from 'react';
import { mockSetting } from '../../../../../helpers/mocks/settings';
import { change } from '../../../../../helpers/testUtils';
import { DefaultSpecializedInputProps } from '../../../utils';
import InputForJSON from '../InputForJSON';

it('should render correctly', () => {
expect(shallowRender()).toMatchSnapshot();
});

it('should call onChange', () => {
const onChange = jest.fn();
const wrapper = shallowRender({ onChange });

change(wrapper.find('textarea'), '{"a": 1}');
expect(onChange).toHaveBeenCalledWith('{"a": 1}');
});

it('should handle formatting for invalid JSON', () => {
const onChange = jest.fn();
const wrapper = shallowRender({ onChange, value: '{"a": 1b}' });
wrapper.instance().format();
expect(onChange).not.toHaveBeenCalled();

expect(wrapper.state().formatError).toBe(true);
expect(wrapper).toMatchSnapshot();
});

it('should handle formatting for valid JSON', () => {
const onChange = jest.fn();
const wrapper = shallowRender({ onChange, value: '{"a": 1}' });
wrapper.instance().format();
expect(onChange).toHaveBeenCalledWith(`{
"a": 1
}`);

expect(wrapper.state().formatError).toBe(false);
});

it('should handle ignore formatting if empty', () => {
const onChange = jest.fn();
const wrapper = shallowRender({ onChange, value: '' });
wrapper.instance().format();
expect(onChange).not.toHaveBeenCalled();

expect(wrapper.state().formatError).toBe(false);
});

function shallowRender(props: Partial<DefaultSpecializedInputProps> = {}) {
return shallow<InputForJSON>(
<InputForJSON
isDefault={false}
name="foo"
onChange={jest.fn()}
setting={mockSetting()}
value=""
{...props}
/>
);
}

+ 0
- 42
server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForPassword-test.tsx View File

@@ -1,42 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2023 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { shallow } from 'enzyme';
import * as React from 'react';
import { mockSetting } from '../../../../../helpers/mocks/settings';
import InputForPassword from '../InputForPassword';
import SimpleInput from '../SimpleInput';

it('should render SimpleInput', () => {
const onChange = jest.fn();
const simpleInput = shallow(
<InputForPassword
isDefault={false}
name="foo"
onChange={onChange}
setting={mockSetting()}
value="bar"
/>
).find(SimpleInput);
expect(simpleInput.length).toBe(1);
expect(simpleInput.prop('name')).toBe('foo');
expect(simpleInput.prop('value')).toBe('bar');
expect(simpleInput.prop('type')).toBe('password');
expect(simpleInput.prop('onChange')).toBeDefined();
});

+ 0
- 96
server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForSecured-test.tsx View File

@@ -1,96 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2023 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { shallow } from 'enzyme';
import * as React from 'react';
import { mockSetting } from '../../../../../helpers/mocks/settings';
import { change, click } from '../../../../../helpers/testUtils';
import InputForSecured from '../InputForSecured';
import InputForString from '../InputForString';

it('should render lock icon, but no form', () => {
const onChange = jest.fn();
const input = shallowRender({ onChange });

expect(input.find('LockIcon').length).toBe(1);
expect(input.find('input').length).toBe(0);
});

it('should open form', () => {
const onChange = jest.fn();
const input = shallowRender({ onChange });
const button = input.find('Button');
expect(button.length).toBe(1);

click(button);
expect(input.find('input').length).toBe(1);
});

it('should set value', () => {
const onChange = jest.fn(() => Promise.resolve());
const input = shallowRender({ onChange });

click(input.find('Button'));
change(input.find(InputForString), 'secret');
expect(onChange).toHaveBeenCalledWith('secret');
});

it('should show input when empty, and enable handle typing', () => {
const input = shallowRender({ setting: mockSetting({ hasValue: false }) });
const onChange = (value: string) => input.setProps({ hasValueChanged: true, value });
input.setProps({ onChange });

expect(input.find('input').length).toBe(1);
change(input.find(InputForString), 'hello');
expect(input.find('input').length).toBe(1);
expect(input.find(InputForString).prop('value')).toBe('hello');
});

it('should handle value reset', () => {
const input = shallowRender({ hasValueChanged: true, value: 'whatever' });
input.setState({ changing: true });

// reset
input.setProps({ hasValueChanged: false, value: 'original' });

expect(input.state('changing')).toBe(false);
});

it('should handle value reset to empty', () => {
const input = shallowRender({ hasValueChanged: true, value: 'whatever' });
input.setState({ changing: true });

// outside change
input.setProps({ hasValueChanged: false, setting: mockSetting({ hasValue: false }) });

expect(input.state('changing')).toBe(true);
});

function shallowRender(props: Partial<InputForSecured['props']> = {}) {
return shallow<InputForSecured>(
<InputForSecured
input={InputForString}
hasValueChanged={false}
onChange={jest.fn()}
setting={mockSetting()}
value="bar"
{...props}
/>
);
}

+ 0
- 63
server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForSingleSelectList-test.tsx View File

@@ -1,63 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2023 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { shallow } from 'enzyme';
import * as React from 'react';
import Select from '../../../../../components/controls/Select';
import { mockSetting } from '../../../../../helpers/mocks/settings';
import { DefaultSpecializedInputProps } from '../../../utils';
import InputForSingleSelectList from '../InputForSingleSelectList';

it('should render Select', () => {
const onChange = jest.fn();
const select = shallowRender({ onChange }).find(Select);
expect(select.length).toBe(1);
expect(select.prop('name')).toBe('foo');
expect(select.prop('value')).toEqual({ label: 'bar', value: 'bar' });
expect(select.prop('options')).toEqual([
{ value: 'foo', label: 'foo' },
{ value: 'bar', label: 'bar' },
{ value: 'baz', label: 'baz' },
]);
expect(select.prop('onChange')).toBeDefined();
});

it('should call onChange', () => {
const onChange = jest.fn();
const select = shallowRender({ onChange }).find(Select);
expect(select.length).toBe(1);
expect(select.prop('onChange')).toBeDefined();

select.prop<Function>('onChange')({ value: 'baz', label: 'baz' });
expect(onChange).toHaveBeenCalledWith('baz');
});

function shallowRender(props: Partial<DefaultSpecializedInputProps> = {}) {
return shallow(
<InputForSingleSelectList
isDefault={false}
name="foo"
onChange={jest.fn()}
options={['foo', 'bar', 'baz']}
setting={mockSetting()}
value="bar"
{...props}
/>
);
}

+ 0
- 57
server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForText-test.tsx View File

@@ -1,57 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2023 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { shallow } from 'enzyme';
import * as React from 'react';
import { mockSetting } from '../../../../../helpers/mocks/settings';
import { change } from '../../../../../helpers/testUtils';
import { DefaultSpecializedInputProps } from '../../../utils';
import InputForText from '../InputForText';

it('should render textarea', () => {
const onChange = jest.fn();
const textarea = shallowRender({ onChange }).find('textarea');
expect(textarea.length).toBe(1);
expect(textarea.prop('name')).toBe('foo');
expect(textarea.prop('value')).toBe('bar');
expect(textarea.prop('onChange')).toBeDefined();
});

it('should call onChange', () => {
const onChange = jest.fn();
const textarea = shallowRender({ onChange }).find('textarea');
expect(textarea.length).toBe(1);
expect(textarea.prop('onChange')).toBeDefined();

change(textarea, 'qux');
expect(onChange).toHaveBeenCalledWith('qux');
});

function shallowRender(props: Partial<DefaultSpecializedInputProps> = {}) {
return shallow(
<InputForText
isDefault={false}
name="foo"
onChange={jest.fn()}
value="bar"
{...props}
setting={mockSetting()}
/>
);
}

+ 0
- 96
server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/MultiValueInput-test.tsx View File

@@ -1,96 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2023 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { shallow, ShallowWrapper } from 'enzyme';
import * as React from 'react';
import { click } from '../../../../../helpers/testUtils';
import { ExtendedSettingDefinition, SettingType } from '../../../../../types/settings';
import { DefaultSpecializedInputProps } from '../../../utils';
import MultiValueInput from '../MultiValueInput';
import PrimitiveInput from '../PrimitiveInput';

const settingValue = {
key: 'example',
hasValue: true,
};

const settingDefinition: ExtendedSettingDefinition = {
category: 'general',
fields: [],
key: 'example',
multiValues: true,
options: [],
subCategory: 'Branches',
type: SettingType.STRING,
};

const assertValues = (inputs: ShallowWrapper<any>, values: string[]) => {
values.forEach((value, index) => {
const input = inputs.at(index);
expect(input.prop('value')).toBe(value);
});
};

it('should render one value', () => {
const multiValueInput = shallowRender();
const stringInputs = multiValueInput.find(PrimitiveInput);
expect(stringInputs.length).toBe(1 + 1);
assertValues(stringInputs, ['foo', '']);
});

it('should render several values', () => {
const multiValueInput = shallowRender({ value: ['foo', 'bar', 'baz'] });
const stringInputs = multiValueInput.find(PrimitiveInput);
expect(stringInputs.length).toBe(3 + 1);
assertValues(stringInputs, ['foo', 'bar', 'baz', '']);
});

it('should remove value', () => {
const onChange = jest.fn();
const multiValueInput = shallowRender({ onChange, value: ['foo', 'bar', 'baz'] });
click(multiValueInput.find('.js-remove-value').at(1));
expect(onChange).toHaveBeenCalledWith(['foo', 'baz']);
});

it('should change existing value', () => {
const onChange = jest.fn();
const multiValueInput = shallowRender({ onChange, value: ['foo', 'bar', 'baz'] });
multiValueInput.find(PrimitiveInput).at(1).prop('onChange')('qux');
expect(onChange).toHaveBeenCalledWith(['foo', 'qux', 'baz']);
});

it('should add new value', () => {
const onChange = jest.fn();
const multiValueInput = shallowRender({ onChange });
multiValueInput.find(PrimitiveInput).at(1).prop('onChange')('bar');
expect(onChange).toHaveBeenCalledWith(['foo', 'bar']);
});

function shallowRender(props: Partial<DefaultSpecializedInputProps> = {}) {
return shallow(
<MultiValueInput
isDefault={true}
name="bar"
onChange={jest.fn()}
setting={{ ...settingValue, definition: settingDefinition }}
value={['foo']}
{...props}
/>
);
}

+ 0
- 46
server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/PrimitiveInput-test.tsx View File

@@ -1,46 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2023 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { shallow } from 'enzyme';
import * as React from 'react';
import { mockDefinition, mockSetting } from '../../../../../helpers/mocks/settings';
import { SettingType } from '../../../../../types/settings';
import { DefaultSpecializedInputProps } from '../../../utils';
import PrimitiveInput from '../PrimitiveInput';

it.each(Object.values(SettingType).map(Array.of))(
'should render correctly for %s',
(type: SettingType) => {
const setting = mockSetting({ definition: mockDefinition({ type }) });
expect(shallowRender({ setting })).toMatchSnapshot();
}
);

function shallowRender(props: Partial<DefaultSpecializedInputProps> = {}) {
return shallow(
<PrimitiveInput
isDefault={true}
name="name"
onChange={jest.fn()}
setting={mockSetting()}
value={['foo']}
{...props}
/>
);
}

+ 0
- 86
server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/SimpleInput-test.tsx View File

@@ -1,86 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2023 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { shallow } from 'enzyme';
import * as React from 'react';
import { KeyboardKeys } from '../../../../../helpers/keycodes';
import { mockSetting } from '../../../../../helpers/mocks/settings';
import { change, mockEvent } from '../../../../../helpers/testUtils';
import SimpleInput, { SimpleInputProps } from '../SimpleInput';

it('should render input', () => {
const input = shallowRender().find('input');
expect(input.length).toBe(1);
expect(input.prop('type')).toBe('text');
expect(input.prop('className')).toContain('input-large');
expect(input.prop('name')).toBe('foo');
expect(input.prop('value')).toBe('bar');
expect(input.prop('onChange')).toBeDefined();
});

it('should call onChange', () => {
const onChange = jest.fn();
const input = shallowRender({ onChange }).find('input');
expect(input.length).toBe(1);
expect(input.prop('onChange')).toBeDefined();

change(input, 'qux');
expect(onChange).toHaveBeenCalledWith('qux');
});

it('should handle enter', () => {
const onSave = jest.fn();
shallowRender({ onSave })
.instance()
.handleKeyDown(mockEvent({ nativeEvent: { key: KeyboardKeys.Enter } }));
expect(onSave).toHaveBeenCalled();
});

it('should handle esc', () => {
const onCancel = jest.fn();
shallowRender({ onCancel })
.instance()
.handleKeyDown(mockEvent({ nativeEvent: { key: KeyboardKeys.Escape } }));
expect(onCancel).toHaveBeenCalled();
});

it('should ignore other keys', () => {
const onSave = jest.fn();
const onCancel = jest.fn();
shallowRender({ onCancel, onSave })
.instance()
.handleKeyDown(mockEvent({ nativeEvent: { key: KeyboardKeys.LeftArrow } }));
expect(onSave).not.toHaveBeenCalled();
expect(onCancel).not.toHaveBeenCalled();
});

function shallowRender(overrides: Partial<SimpleInputProps> = {}) {
return shallow<SimpleInput>(
<SimpleInput
className="input-large"
isDefault={false}
name="foo"
onChange={jest.fn()}
type="text"
setting={mockSetting()}
value="bar"
{...overrides}
/>
);
}

+ 0
- 51
server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/__snapshots__/InputForJSON-test.tsx.snap View File

@@ -1,51 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should handle formatting for invalid JSON 1`] = `
<div
className="display-flex-end"
>
<textarea
className="settings-large-input text-top monospaced spacer-right"
name="foo"
onChange={[Function]}
rows={5}
value="{"a": 1b}"
/>
<div>
<Alert
variant="info"
>
settings.json.format_error
</Alert>
<Button
className="spacer-top"
onClick={[Function]}
>
settings.json.format
</Button>
</div>
</div>
`;

exports[`should render correctly 1`] = `
<div
className="display-flex-end"
>
<textarea
className="settings-large-input text-top monospaced spacer-right"
name="foo"
onChange={[Function]}
rows={5}
value=""
/>
<div>
<Button
className="spacer-top"
onClick={[Function]}
>
settings.json.format
</Button>
</div>
</div>
`;

+ 0
- 349
server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/__snapshots__/PrimitiveInput-test.tsx.snap View File

@@ -1,349 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should render correctly for BOOLEAN 1`] = `
<InputForBoolean
isDefault={true}
name="name"
onChange={[MockFunction]}
setting={
{
"definition": {
"category": "foo category",
"fields": [],
"key": "foo",
"options": [],
"subCategory": "foo subCat",
"type": "BOOLEAN",
},
"hasValue": true,
"inherited": true,
"key": "foo",
"value": "42",
}
}
value={
[
"foo",
]
}
/>
`;

exports[`should render correctly for FLOAT 1`] = `
<InputForNumber
isDefault={true}
name="name"
onChange={[MockFunction]}
setting={
{
"definition": {
"category": "foo category",
"fields": [],
"key": "foo",
"options": [],
"subCategory": "foo subCat",
"type": "FLOAT",
},
"hasValue": true,
"inherited": true,
"key": "foo",
"value": "42",
}
}
value={
[
"foo",
]
}
/>
`;

exports[`should render correctly for FORMATTED_TEXT 1`] = `
<InputForFormattedText
isDefault={true}
name="name"
onChange={[MockFunction]}
setting={
{
"definition": {
"category": "foo category",
"fields": [],
"key": "foo",
"options": [],
"subCategory": "foo subCat",
"type": "FORMATTED_TEXT",
},
"hasValue": true,
"inherited": true,
"key": "foo",
"value": "42",
}
}
value={
[
"foo",
]
}
/>
`;

exports[`should render correctly for INTEGER 1`] = `
<InputForNumber
isDefault={true}
name="name"
onChange={[MockFunction]}
setting={
{
"definition": {
"category": "foo category",
"fields": [],
"key": "foo",
"options": [],
"subCategory": "foo subCat",
"type": "INTEGER",
},
"hasValue": true,
"inherited": true,
"key": "foo",
"value": "42",
}
}
value={
[
"foo",
]
}
/>
`;

exports[`should render correctly for JSON 1`] = `
<InputForJSON
isDefault={true}
name="name"
onChange={[MockFunction]}
setting={
{
"definition": {
"category": "foo category",
"fields": [],
"key": "foo",
"options": [],
"subCategory": "foo subCat",
"type": "JSON",
},
"hasValue": true,
"inherited": true,
"key": "foo",
"value": "42",
}
}
value={
[
"foo",
]
}
/>
`;

exports[`should render correctly for LICENSE 1`] = `
<InputForString
isDefault={true}
name="name"
onChange={[MockFunction]}
setting={
{
"definition": {
"category": "foo category",
"fields": [],
"key": "foo",
"options": [],
"subCategory": "foo subCat",
"type": "LICENSE",
},
"hasValue": true,
"inherited": true,
"key": "foo",
"value": "42",
}
}
value={
[
"foo",
]
}
/>
`;

exports[`should render correctly for LONG 1`] = `
<InputForNumber
isDefault={true}
name="name"
onChange={[MockFunction]}
setting={
{
"definition": {
"category": "foo category",
"fields": [],
"key": "foo",
"options": [],
"subCategory": "foo subCat",
"type": "LONG",
},
"hasValue": true,
"inherited": true,
"key": "foo",
"value": "42",
}
}
value={
[
"foo",
]
}
/>
`;

exports[`should render correctly for PASSWORD 1`] = `
<InputForPassword
isDefault={true}
name="name"
onChange={[MockFunction]}
setting={
{
"definition": {
"category": "foo category",
"fields": [],
"key": "foo",
"options": [],
"subCategory": "foo subCat",
"type": "PASSWORD",
},
"hasValue": true,
"inherited": true,
"key": "foo",
"value": "42",
}
}
value={
[
"foo",
]
}
/>
`;

exports[`should render correctly for PROPERTY_SET 1`] = `
<InputForString
isDefault={true}
name="name"
onChange={[MockFunction]}
setting={
{
"definition": {
"category": "foo category",
"fields": [],
"key": "foo",
"options": [],
"subCategory": "foo subCat",
"type": "PROPERTY_SET",
},
"hasValue": true,
"inherited": true,
"key": "foo",
"value": "42",
}
}
value={
[
"foo",
]
}
/>
`;

exports[`should render correctly for SINGLE_SELECT_LIST 1`] = `
<Wrapped
isDefault={true}
name="name"
onChange={[MockFunction]}
setting={
{
"definition": {
"category": "foo category",
"fields": [],
"key": "foo",
"options": [],
"subCategory": "foo subCat",
"type": "SINGLE_SELECT_LIST",
},
"hasValue": true,
"inherited": true,
"key": "foo",
"value": "42",
}
}
value={
[
"foo",
]
}
/>
`;

exports[`should render correctly for STRING 1`] = `
<InputForString
isDefault={true}
name="name"
onChange={[MockFunction]}
setting={
{
"definition": {
"category": "foo category",
"fields": [],
"key": "foo",
"options": [],
"subCategory": "foo subCat",
"type": "STRING",
},
"hasValue": true,
"inherited": true,
"key": "foo",
"value": "42",
}
}
value={
[
"foo",
]
}
/>
`;

exports[`should render correctly for TEXT 1`] = `
<InputForText
isDefault={true}
name="name"
onChange={[MockFunction]}
setting={
{
"definition": {
"category": "foo category",
"fields": [],
"key": "foo",
"options": [],
"subCategory": "foo subCat",
"type": "TEXT",
},
"hasValue": true,
"inherited": true,
"key": "foo",
"value": "42",
}
}
value={
[
"foo",
]
}
/>
`;

+ 15
- 7
server/sonar-web/src/main/js/components/controls/Toggle.tsx View File

@@ -19,7 +19,6 @@
*/
import classNames from 'classnames';
import * as React from 'react';
import { translate } from '../../helpers/l10n';
import CheckIcon from '../icons/CheckIcon';
import { Button } from './buttons';
import './Toggle.css';
@@ -32,10 +31,14 @@ interface Props {
value: boolean | string;
}

export function getToggleValue(value: string | boolean) {
return typeof value === 'string' ? value === 'true' : value;
}

export default class Toggle extends React.PureComponent<Props> {
getValue = () => {
const { value } = this.props;
return typeof value === 'string' ? value === 'true' : value;
return getToggleValue(value);
};

handleClick = () => {
@@ -51,11 +54,16 @@ export default class Toggle extends React.PureComponent<Props> {
const className = classNames('boolean-toggle', { 'boolean-toggle-on': value });

return (
<Button className={className} disabled={disabled} name={name} onClick={this.handleClick}>
<div
aria-label={ariaLabel ?? translate(value ? 'on' : 'off')}
className="boolean-toggle-handle"
>
<Button
className={className}
disabled={disabled}
aria-label={ariaLabel}
name={name}
onClick={this.handleClick}
role="switch"
aria-checked={value}
>
<div className="boolean-toggle-handle">
<CheckIcon size={12} />
</div>
</Button>

+ 6
- 3
server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/Toggle-test.tsx.snap View File

@@ -2,13 +2,14 @@

exports[`should render correctly: disabled 1`] = `
<Button
aria-checked={true}
className="boolean-toggle boolean-toggle-on"
disabled={true}
name="toggle-name"
onClick={[Function]}
role="switch"
>
<div
aria-label="on"
className="boolean-toggle-handle"
>
<CheckIcon
@@ -20,13 +21,14 @@ exports[`should render correctly: disabled 1`] = `

exports[`should render correctly: off 1`] = `
<Button
aria-checked={false}
className="boolean-toggle"
disabled={true}
name="toggle-name"
onClick={[Function]}
role="switch"
>
<div
aria-label="off"
className="boolean-toggle-handle"
>
<CheckIcon
@@ -38,13 +40,14 @@ exports[`should render correctly: off 1`] = `

exports[`should render correctly: on 1`] = `
<Button
aria-checked={true}
className="boolean-toggle boolean-toggle-on"
disabled={true}
name="toggle-name"
onClick={[Function]}
role="switch"
>
<div
aria-label="on"
className="boolean-toggle-handle"
>
<CheckIcon

+ 1
- 0
server/sonar-web/src/main/js/components/controls/buttons.tsx View File

@@ -41,6 +41,7 @@ type AllowedButtonAttributes = Pick<
| 'onMouseOver'
| 'onMouseLeave'
| 'tabIndex'
| 'role'
>;

interface ButtonProps extends AllowedButtonAttributes {

+ 15
- 8
server/sonar-web/src/main/js/helpers/__tests__/periods-test.ts View File

@@ -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 { getPeriodLabel } from '../periods';
import { mockPeriod } from '../testMocks';

@@ -85,21 +86,27 @@ describe('getPeriodLabel', () => {

it('should handle SPECIFIC_ANALYSIS', () => {
expect(
getPeriodLabel(mockPeriod({ mode: 'SPECIFIC_ANALYSIS', parameter: '7.1' }), formatter)
getPeriodLabel(
mockPeriod({ mode: NewCodePeriodSettingType.SPECIFIC_ANALYSIS, parameter: '7.1' }),
formatter
)
).toBe('overview.period.specific_analysis.2019-04-23T02:12:32+0100');
expect(
getPeriodLabel(mockPeriod({ mode: NewCodePeriodSettingType.SPECIFIC_ANALYSIS }), formatter)
).toBe('overview.period.specific_analysis.2019-04-23T02:12:32+0100');
expect(getPeriodLabel(mockPeriod({ mode: 'SPECIFIC_ANALYSIS' }), formatter)).toBe(
'overview.period.specific_analysis.2019-04-23T02:12:32+0100'
);
expect(formatter).toHaveBeenCalledTimes(2);
});

it('should handle PREVIOUS_VERSION', () => {
expect(
getPeriodLabel(mockPeriod({ mode: 'PREVIOUS_VERSION', modeParam: 'A658678DE' }), formatter)
getPeriodLabel(
mockPeriod({ mode: NewCodePeriodSettingType.PREVIOUS_VERSION, modeParam: 'A658678DE' }),
formatter
)
).toBe('overview.period.previous_version.A658678DE');
expect(getPeriodLabel(mockPeriod({ mode: 'PREVIOUS_VERSION' }), formatter)).toBe(
'overview.period.previous_version.2019-04-23T02:12:32+0100'
);
expect(
getPeriodLabel(mockPeriod({ mode: NewCodePeriodSettingType.PREVIOUS_VERSION }), formatter)
).toBe('overview.period.previous_version.2019-04-23T02:12:32+0100');
expect(formatter).toHaveBeenCalledTimes(1);
});
});

server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForString-test.tsx → server/sonar-web/src/main/js/helpers/mocks/new-code-period.ts View File

@@ -17,26 +17,12 @@
* 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 { mockSetting } from '../../../../../helpers/mocks/settings';
import InputForString from '../InputForString';
import SimpleInput from '../SimpleInput';

it('should render SimpleInput', () => {
const onChange = jest.fn();
const simpleInput = shallow(
<InputForString
isDefault={false}
name="foo"
onChange={onChange}
setting={mockSetting()}
value="bar"
/>
).find(SimpleInput);
expect(simpleInput.length).toBe(1);
expect(simpleInput.prop('name')).toBe('foo');
expect(simpleInput.prop('value')).toBe('bar');
expect(simpleInput.prop('type')).toBe('text');
expect(simpleInput.prop('onChange')).toBeDefined();
});
import { NewCodePeriod, NewCodePeriodSettingType } from '../../types/types';

export function mockNewCodePeriod(overrides: Partial<NewCodePeriod> = {}) {
return {
type: NewCodePeriodSettingType.PREVIOUS_VERSION,
...overrides,
};
}

+ 7
- 0
server/sonar-web/src/main/js/helpers/mocks/settings.ts View File

@@ -20,6 +20,7 @@
import {
ExtendedSettingDefinition,
Setting,
SettingFieldDefinition,
SettingType,
SettingValue,
SettingWithCategory,
@@ -38,6 +39,12 @@ export function mockDefinition(
};
}

export function mockSettingFieldDefinition(
overrides: Partial<SettingFieldDefinition> = {}
): SettingFieldDefinition {
return { key: 'name', name: 'Name', options: [], ...overrides };
}

export function mockSetting(overrides: Partial<Setting> = {}): Setting {
return {
key: 'foo',

+ 3
- 3
server/sonar-web/src/main/js/helpers/periods.ts View File

@@ -20,7 +20,7 @@
import { parseDate } from '../helpers/dates';
import { translate, translateWithParameters } from '../helpers/l10n';
import { ApplicationPeriod } from '../types/application';
import { Period } from '../types/types';
import { NewCodePeriodSettingType, Period } from '../types/types';

export function getPeriodLabel(
period: Period | undefined,
@@ -33,10 +33,10 @@ export function getPeriodLabel(
let parameter = period.modeParam || period.parameter || '';

switch (period.mode) {
case 'SPECIFIC_ANALYSIS':
case NewCodePeriodSettingType.SPECIFIC_ANALYSIS:
parameter = dateFormatter(period.date);
break;
case 'PREVIOUS_VERSION':
case NewCodePeriodSettingType.PREVIOUS_VERSION:
parameter = parameter || dateFormatter(period.date);
break;
/*

+ 0
- 1
server/sonar-web/src/main/js/types/settings.ts View File

@@ -74,7 +74,6 @@ export interface SettingDefinition {
}

export interface SettingFieldDefinition extends SettingDefinition {
description: string;
name: string;
}


+ 6
- 5
server/sonar-web/src/main/js/types/types.ts View File

@@ -405,11 +405,12 @@ export interface NewCodePeriodBranch {
effectiveValue?: string;
}

export type NewCodePeriodSettingType =
| 'PREVIOUS_VERSION'
| 'NUMBER_OF_DAYS'
| 'SPECIFIC_ANALYSIS'
| 'REFERENCE_BRANCH';
export enum NewCodePeriodSettingType {
PREVIOUS_VERSION = 'PREVIOUS_VERSION',
NUMBER_OF_DAYS = 'NUMBER_OF_DAYS',
SPECIFIC_ANALYSIS = 'SPECIFIC_ANALYSIS',
REFERENCE_BRANCH = 'REFERENCE_BRANCH',
}

export interface Paging {
pageIndex: number;

+ 1
- 3
sonar-core/src/main/resources/org/sonar/l10n/core.properties View File

@@ -140,8 +140,6 @@ new_name=New name
none=None
no_tags=No tags
not_now=Not now
off=Off
on=On
or=Or
open=Open
optional=Optional
@@ -244,7 +242,6 @@ no=No
valid_input=Valid input



#------------------------------------------------------------------------------
#
# GENERIC EXPRESSIONS, sorted alphabetically
@@ -1164,6 +1161,7 @@ settings.reset_confirm.title=Reset Setting
settings.reset_confirm.description=Are you sure that you want to reset this setting?
settings.definition.reset=Reset "{0}" to default values
settings.definition.delete_value=Delete value "{1}" for setting "{0}"
settings.definitions.delete_fields=Delete row {1} for setting "{0}"

settings.search.placeholder=Find in Settings
settings.search.results=Search results list

Loading…
Cancel
Save