Browse Source

SONAR-17028 Fix project name validation when empty

tags/9.6.0.59041
Mathieu Suen 1 year ago
parent
commit
dfcd5cf9fe

+ 6
- 6
server/sonar-web/src/main/js/apps/create/project/ManualProjectCreate.tsx View File

@@ -42,10 +42,10 @@ interface Props {
interface State {
projectName: string;
projectNameError?: string;
projectNameTouched?: boolean;
projectNameTouched: boolean;
projectKey: string;
projectKeyError?: string;
projectKeyTouched?: boolean;
projectKeyTouched: boolean;
validatingProjectKey: boolean;
submitting: boolean;
}
@@ -61,6 +61,8 @@ export default class ManualProjectCreate extends React.PureComponent<Props, Stat
projectKey: '',
projectName: '',
submitting: false,
projectKeyTouched: false,
projectNameTouched: false,
validatingProjectKey: false
};
this.checkFreeKey = debounce(this.checkFreeKey, 250);
@@ -166,8 +168,6 @@ export default class ManualProjectCreate extends React.PureComponent<Props, Stat
validateName = (projectName: string) => {
if (projectName.length === 0) {
return translate('onboarding.create_project.display_name.error.empty');
} else if (projectName.length > PROJECT_NAME_MAX_LEN) {
return translate('onboarding.create_project.display_name.error.too_long');
}
return undefined;
};
@@ -186,8 +186,8 @@ export default class ManualProjectCreate extends React.PureComponent<Props, Stat
const { branchesEnabled } = this.props;

const touched = !!(projectKeyTouched || projectNameTouched);
const projectNameIsInvalid = touched && projectNameError !== undefined;
const projectNameIsValid = touched && projectNameError === undefined;
const projectNameIsInvalid = projectNameTouched && projectNameError !== undefined;
const projectNameIsValid = projectNameTouched && projectNameError === undefined;

return (
<>

+ 0
- 31
server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProjectPageHeader-test.tsx View File

@@ -1,31 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2022 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 CreateProjectPageHeader, { CreateProjectPageHeaderProps } from '../CreateProjectPageHeader';

it('should render correctly', () => {
expect(shallowRender()).toMatchSnapshot('default');
expect(shallowRender({ additionalActions: 'Bar' })).toMatchSnapshot('additional content');
});

function shallowRender(props: Partial<CreateProjectPageHeaderProps> = {}) {
return shallow<CreateProjectPageHeaderProps>(<CreateProjectPageHeader title="Foo" {...props} />);
}

+ 124
- 99
server/sonar-web/src/main/js/apps/create/project/__tests__/ManualProjectCreate-test.tsx View File

@@ -17,16 +17,11 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { shallow } from 'enzyme';
import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import * as React from 'react';
import { createProject, doesComponentExists } from '../../../../api/components';
import ProjectKeyInput from '../../../../components/common/ProjectKeyInput';
import { SubmitButton } from '../../../../components/controls/buttons';
import ValidationInput from '../../../../components/controls/ValidationInput';
import { validateProjectKey } from '../../../../helpers/projects';
import { change, mockEvent, submit, waitAndUpdate } from '../../../../helpers/testUtils';
import { ProjectKeyValidationResult } from '../../../../types/component';
import { PROJECT_NAME_MAX_LEN } from '../constants';
import { renderComponent } from '../../../../helpers/testReactTestingUtils';
import ManualProjectCreate from '../ManualProjectCreate';

jest.mock('../../../../api/components', () => ({
@@ -36,119 +31,149 @@ jest.mock('../../../../api/components', () => ({
.mockImplementation(({ component }) => Promise.resolve(component === 'exists'))
}));

jest.mock('../../../../helpers/projects', () => {
const { PROJECT_KEY_INVALID_CHARACTERS } = jest.requireActual('../../../../helpers/projects');
return {
validateProjectKey: jest.fn(() => ProjectKeyValidationResult.Valid),
PROJECT_KEY_INVALID_CHARACTERS
};
});

beforeEach(() => {
jest.clearAllMocks();
});

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

const wrapper = shallowRender();
wrapper.instance().handleProjectNameChange('My new awesome app');
expect(wrapper).toMatchSnapshot('with form filled');

expect(shallowRender({ branchesEnabled: true })).toMatchSnapshot('with branches enabled');
it('should show branch information', async () => {
renderManualProjectCreate({ branchesEnabled: true });
expect(
await screen.findByText('onboarding.create_project.pr_decoration.information')
).toBeInTheDocument();
});

it('should correctly create a project', async () => {
const onProjectCreate = jest.fn();
const wrapper = shallowRender({ onProjectCreate });

wrapper
.find(ProjectKeyInput)
.props()
.onProjectKeyChange(mockEvent({ currentTarget: { value: 'bar' } }));
change(wrapper.find('input#project-name'), 'Bar');
expect(wrapper.find(SubmitButton).props().disabled).toBe(false);
expect(validateProjectKey).toBeCalledWith('bar');
expect(doesComponentExists).toBeCalledWith({ component: 'bar' });

submit(wrapper.find('form'));
expect(createProject).toBeCalledWith({
project: 'bar',
name: 'Bar'
});

await waitAndUpdate(wrapper);
expect(onProjectCreate).toBeCalledWith('bar');
});
it('should validate form input', async () => {
const user = userEvent.setup();
renderManualProjectCreate();

it('should not display any status when the name is not defined', () => {
const wrapper = shallowRender();
const projectNameInput = wrapper.find(ValidationInput);
expect(projectNameInput.props().isInvalid).toBe(false);
expect(projectNameInput.props().isValid).toBe(false);
});

it('should have an error when the key is invalid', () => {
(validateProjectKey as jest.Mock).mockReturnValueOnce(ProjectKeyValidationResult.TooLong);

const wrapper = shallowRender();

wrapper.instance().handleProjectKeyChange('');
expect(wrapper.find(ProjectKeyInput).props().error).toBe(
`onboarding.create_project.project_key.error.${ProjectKeyValidationResult.TooLong}`
// All input valid
await user.click(
await screen.findByRole('textbox', {
name: 'onboarding.create_project.display_name field_required'
})
);
await user.keyboard('test');
expect(
screen.getByRole('textbox', { name: 'onboarding.create_project.project_key field_required' })
).toHaveValue('test');
expect(screen.getByRole('button', { name: 'set_up' })).toBeEnabled();

// Sanitize the key
await user.click(
await screen.findByRole('textbox', {
name: 'onboarding.create_project.display_name field_required'
})
);
await user.keyboard('{Control>}a{/Control}This is not a key%^$');
expect(
screen.getByRole('textbox', { name: 'onboarding.create_project.project_key field_required' })
).toHaveValue('This-is-not-a-key-');

// Clear name
await user.click(
await screen.findByRole('textbox', {
name: 'onboarding.create_project.display_name field_required'
})
);
await user.keyboard('{Control>}a{/Control}{Backspace}');
expect(
screen.getByRole('textbox', { name: 'onboarding.create_project.project_key field_required' })
).toHaveValue('');
expect(
screen.getByText('onboarding.create_project.display_name.error.empty')
).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'set_up' })).toBeDisabled();

// Only key
await user.click(
await screen.findByRole('textbox', {
name: 'onboarding.create_project.project_key field_required'
})
);
await user.keyboard('awsome-key');
expect(
screen.getByRole('textbox', { name: 'onboarding.create_project.display_name field_required' })
).toHaveValue('');
expect(screen.getByLabelText('valid_input')).toBeInTheDocument();
expect(
screen.getByText('onboarding.create_project.display_name.error.empty')
).toBeInTheDocument();

// Invalid key
await user.click(
await screen.findByRole('textbox', {
name: 'onboarding.create_project.project_key field_required'
})
);
await user.keyboard('{Control>}a{/Control}123');
expect(
await screen.findByText('onboarding.create_project.project_key.error.only_digits')
).toBeInTheDocument();
await user.keyboard('{Control>}a{/Control}@');
expect(
await screen.findByText('onboarding.create_project.project_key.error.invalid_char')
).toBeInTheDocument();
await user.keyboard('{Control>}a{/Control}exists');
expect(
await screen.findByText('onboarding.create_project.project_key.taken')
).toBeInTheDocument();
});

it('should have an error when the key already exists', async () => {
const wrapper = shallowRender();
wrapper.instance().handleProjectKeyChange('exists', true);
await waitAndUpdate(wrapper);
expect(wrapper.state().projectKeyError).toBe('onboarding.create_project.project_key.taken');
});

it('should ignore promise return if value has been changed in the meantime', async () => {
(validateProjectKey as jest.Mock)
.mockReturnValueOnce(ProjectKeyValidationResult.Valid)
.mockReturnValueOnce(ProjectKeyValidationResult.InvalidChar);
const wrapper = shallowRender();
const instance = wrapper.instance();

instance.handleProjectKeyChange('exists', true);
instance.handleProjectKeyChange('exists%', true);

await waitAndUpdate(wrapper);
it('should submit form input', async () => {
const user = userEvent.setup();
const onProjectCreate = jest.fn();
renderManualProjectCreate({ onProjectCreate });

expect(wrapper.state().projectKeyTouched).toBe(true);
expect(wrapper.state().projectKeyError).toBe(
`onboarding.create_project.project_key.error.${ProjectKeyValidationResult.InvalidChar}`
// All input valid
await user.click(
await screen.findByRole('textbox', {
name: 'onboarding.create_project.display_name field_required'
})
);
await user.keyboard('test');
await user.click(screen.getByRole('button', { name: 'set_up' }));
expect(createProject).toHaveBeenCalledWith({ name: 'test', project: 'test' });
expect(onProjectCreate).toBeCalled();
});

it('should autofill the key based on the name, and sanitize it', () => {
const wrapper = shallowRender();
it('should handle create failure', async () => {
const user = userEvent.setup();
(createProject as jest.Mock).mockRejectedValueOnce({});
const onProjectCreate = jest.fn();
renderManualProjectCreate({ onProjectCreate });

wrapper.instance().handleProjectNameChange('newName', true);
expect(wrapper.state().projectKey).toBe('newName');
// All input valid
await user.click(
await screen.findByRole('textbox', {
name: 'onboarding.create_project.display_name field_required'
})
);
await user.keyboard('test');
await user.click(screen.getByRole('button', { name: 'set_up' }));

wrapper.instance().handleProjectNameChange('my invalid +"*ç%&/()= name', true);
expect(wrapper.state().projectKey).toBe('my-invalid-name');
expect(onProjectCreate).not.toHaveBeenCalled();
});

it.each([
['empty', ''],
['too_long', new Array(PROJECT_NAME_MAX_LEN + 1).fill('a').join('')]
])('should have an error when the name is %s', (errorSuffix: string, projectName: string) => {
const wrapper = shallowRender();
it('should handle component exists failure', async () => {
const user = userEvent.setup();
(doesComponentExists as jest.Mock).mockRejectedValueOnce({});
const onProjectCreate = jest.fn();
renderManualProjectCreate({ onProjectCreate });

wrapper.instance().handleProjectNameChange(projectName, true);
expect(wrapper.find(ValidationInput).props().isInvalid).toBe(true);
expect(wrapper.state().projectNameError).toBe(
`onboarding.create_project.display_name.error.${errorSuffix}`
// All input valid
await user.click(
await screen.findByRole('textbox', {
name: 'onboarding.create_project.display_name field_required'
})
);
await user.keyboard('test');
expect(
screen.getByRole('textbox', { name: 'onboarding.create_project.display_name field_required' })
).toHaveValue('test');
});

function shallowRender(props: Partial<ManualProjectCreate['props']> = {}) {
return shallow<ManualProjectCreate>(
function renderManualProjectCreate(props: Partial<ManualProjectCreate['props']> = {}) {
renderComponent(
<ManualProjectCreate branchesEnabled={false} onProjectCreate={jest.fn()} {...props} />
);
}

+ 0
- 26
server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectPageHeader-test.tsx.snap View File

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

exports[`should render correctly: additional content 1`] = `
<header
className="huge-spacer-bottom bordered-bottom overflow-hidden"
>
<h1
className="pull-left huge big-spacer-bottom"
>
Foo
</h1>
Bar
</header>
`;

exports[`should render correctly: default 1`] = `
<header
className="huge-spacer-bottom bordered-bottom overflow-hidden"
>
<h1
className="pull-left huge big-spacer-bottom"
>
Foo
</h1>
</header>
`;

+ 0
- 188
server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/ManualProjectCreate-test.tsx.snap View File

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

exports[`should render correctly 1`] = `
<Fragment>
<CreateProjectPageHeader
title="onboarding.create_project.setup_manually"
/>
<div
className="create-project-manual"
>
<div
className="flex-1 huge-spacer-right"
>
<form
className="manual-project-create"
onSubmit={[Function]}
>
<MandatoryFieldsExplanation
className="big-spacer-bottom"
/>
<ValidationInput
className="form-field"
description="onboarding.create_project.display_name.description"
id="project-name"
isInvalid={false}
isValid={false}
label="onboarding.create_project.display_name"
required={true}
>
<input
autoFocus={true}
className="input-super-large"
id="project-name"
maxLength={255}
minLength={1}
onChange={[Function]}
type="text"
value=""
/>
</ValidationInput>
<ProjectKeyInput
label="onboarding.create_project.project_key"
onProjectKeyChange={[Function]}
projectKey=""
touched={false}
validating={false}
/>
<SubmitButton
disabled={true}
>
set_up
</SubmitButton>
<DeferredSpinner
className="spacer-left"
loading={false}
/>
</form>
</div>
</div>
</Fragment>
`;

exports[`should render correctly: with branches enabled 1`] = `
<Fragment>
<CreateProjectPageHeader
title="onboarding.create_project.setup_manually"
/>
<div
className="create-project-manual"
>
<div
className="flex-1 huge-spacer-right"
>
<form
className="manual-project-create"
onSubmit={[Function]}
>
<MandatoryFieldsExplanation
className="big-spacer-bottom"
/>
<ValidationInput
className="form-field"
description="onboarding.create_project.display_name.description"
id="project-name"
isInvalid={false}
isValid={false}
label="onboarding.create_project.display_name"
required={true}
>
<input
autoFocus={true}
className="input-super-large"
id="project-name"
maxLength={255}
minLength={1}
onChange={[Function]}
type="text"
value=""
/>
</ValidationInput>
<ProjectKeyInput
label="onboarding.create_project.project_key"
onProjectKeyChange={[Function]}
projectKey=""
touched={false}
validating={false}
/>
<SubmitButton
disabled={true}
>
set_up
</SubmitButton>
<DeferredSpinner
className="spacer-left"
loading={false}
/>
</form>
<Alert
className="big-spacer-top"
display="inline"
variant="info"
>
onboarding.create_project.pr_decoration.information
</Alert>
</div>
</div>
</Fragment>
`;

exports[`should render correctly: with form filled 1`] = `
<Fragment>
<CreateProjectPageHeader
title="onboarding.create_project.setup_manually"
/>
<div
className="create-project-manual"
>
<div
className="flex-1 huge-spacer-right"
>
<form
className="manual-project-create"
onSubmit={[Function]}
>
<MandatoryFieldsExplanation
className="big-spacer-bottom"
/>
<ValidationInput
className="form-field"
description="onboarding.create_project.display_name.description"
id="project-name"
isInvalid={false}
isValid={false}
label="onboarding.create_project.display_name"
required={true}
>
<input
autoFocus={true}
className="input-super-large"
id="project-name"
maxLength={255}
minLength={1}
onChange={[Function]}
type="text"
value="My new awesome app"
/>
</ValidationInput>
<ProjectKeyInput
label="onboarding.create_project.project_key"
onProjectKeyChange={[Function]}
projectKey="My-new-awesome-app"
touched={false}
validating={true}
/>
<SubmitButton
disabled={false}
>
set_up
</SubmitButton>
<DeferredSpinner
className="spacer-left"
loading={false}
/>
</form>
</div>
</div>
</Fragment>
`;

+ 13
- 2
server/sonar-web/src/main/js/components/controls/ValidationInput.tsx View File

@@ -18,6 +18,7 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import { translate } from '../../helpers/l10n';
import AlertErrorIcon from '../icons/AlertErrorIcon';
import AlertSuccessIcon from '../icons/AlertSuccessIcon';
import MandatoryFieldMarker from '../ui/MandatoryFieldMarker';
@@ -63,7 +64,12 @@ export default function ValidationInput(props: ValidationInputProps) {
childrenWithStatus = (
<>
{children}
{isValid && <AlertSuccessIcon className="spacer-left text-middle" />}
{isValid && (
<AlertSuccessIcon
ariaLabel={translate('valid_input')}
className="spacer-left text-middle"
/>
)}
{isInvalid && <AlertErrorIcon className="spacer-left text-middle" />}
{hasError && <span className="little-spacer-left text-danger text-middle">{error}</span>}
</>
@@ -72,7 +78,12 @@ export default function ValidationInput(props: ValidationInputProps) {
childrenWithStatus = (
<>
{children}
{isValid && <AlertSuccessIcon className="spacer-left text-middle" />}
{isValid && (
<AlertSuccessIcon
ariaLabel={translate('valid_input')}
className="spacer-left text-middle"
/>
)}
<div className="spacer-top">
{isInvalid && <AlertErrorIcon className="text-middle" />}
{hasError && <span className="little-spacer-left text-danger text-middle">{error}</span>}

+ 2
- 0
server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/ValidationInput-test.tsx.snap View File

@@ -19,6 +19,7 @@ exports[`should render correctly: default 1`] = `
>
<div />
<AlertSuccessIcon
ariaLabel="valid_input"
className="spacer-left text-middle"
/>
</div>
@@ -76,6 +77,7 @@ exports[`should render correctly: no label 1`] = `
>
<div />
<AlertSuccessIcon
ariaLabel="valid_input"
className="spacer-left text-middle"
/>
</div>

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

@@ -231,6 +231,7 @@ with=With
worst=Worst
yes=Yes
no=No
valid_input=Valid input



@@ -3382,7 +3383,6 @@ onboarding.create_project.project_key.error.only_digits=The provided key contain
onboarding.create_project.project_key.taken=This project key is already taken.
onboarding.create_project.display_name=Project display name
onboarding.create_project.display_name.error.empty=The display name is required.
onboarding.create_project.display_name.error.too_long=The display name is too long.
onboarding.create_project.display_name.description=Up to 255 characters. Some scanners might override the value you provide.
onboarding.create_project.pr_decoration.information=Manually created projects won’t benefit from the features associated with DevOps Platforms integration unless you configure them in the project settings.
onboarding.create_project.repository_imported=Already set up

Loading…
Cancel
Save