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