]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-17028 Fix project name validation when empty
authorMathieu Suen <mathieu.suen@sonarsource.com>
Wed, 20 Jul 2022 15:42:49 +0000 (17:42 +0200)
committersonartech <sonartech@sonarsource.com>
Thu, 21 Jul 2022 20:03:06 +0000 (20:03 +0000)
server/sonar-web/src/main/js/apps/create/project/ManualProjectCreate.tsx
server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProjectPageHeader-test.tsx [deleted file]
server/sonar-web/src/main/js/apps/create/project/__tests__/ManualProjectCreate-test.tsx
server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectPageHeader-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/ManualProjectCreate-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/components/controls/ValidationInput.tsx
server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/ValidationInput-test.tsx.snap
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index b3a6cff71d9fa551d201d6862a310d5907deebfd..696be7893d24be73dccda8d75167c15a3bd68113 100644 (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 (
       <>
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProjectPageHeader-test.tsx b/server/sonar-web/src/main/js/apps/create/project/__tests__/CreateProjectPageHeader-test.tsx
deleted file mode 100644 (file)
index a983b80..0000000
+++ /dev/null
@@ -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} />);
-}
index 7e4b38aed81310e73bbbef8b5018db58fce2e889..1241a7036d3a7e02c8375ba87bfe7bb4a44dda69 100644 (file)
  * 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} />
   );
 }
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectPageHeader-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/CreateProjectPageHeader-test.tsx.snap
deleted file mode 100644 (file)
index c8d1616..0000000
+++ /dev/null
@@ -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>
-`;
diff --git a/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/ManualProjectCreate-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/ManualProjectCreate-test.tsx.snap
deleted file mode 100644 (file)
index e621ef0..0000000
+++ /dev/null
@@ -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>
-`;
index 8c122effe95644204a3d1969935117834d1d9dc4..1bf1fe419df2d9945efe9efe098f6ec8292d603b 100644 (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>}
index 949eaf4f7a17dfacc73ce96a2e17e299971d3dc1..b6494ce70b2f9e7100588f987296648ff9d96bac 100644 (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>
index 8ed3dc0259ca4fb45edd2c5ad4623a8d7b955779..cbe36ffbe891d6f5d1b3164974d4fea97532d2ca 100644 (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