]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-12360 Improve project creation validation
authorWouter Admiraal <wouter.admiraal@sonarsource.com>
Tue, 27 Aug 2019 10:11:41 +0000 (12:11 +0200)
committerSonarTech <sonartech@sonarsource.com>
Tue, 10 Sep 2019 18:21:02 +0000 (20:21 +0200)
server/sonar-web/src/main/js/apps/create/components/ProjectKeyInput.tsx [deleted file]
server/sonar-web/src/main/js/apps/create/components/__tests__/ProjectKeyInput-test.tsx [deleted file]
server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/ProjectKeyInput-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/apps/create/project/ManualProjectCreate.css
server/sonar-web/src/main/js/apps/create/project/ManualProjectCreate.tsx
server/sonar-web/src/main/js/apps/create/project/__tests__/ManualProjectCreate-test.tsx
server/sonar-web/src/main/js/apps/create/project/__tests__/__snapshots__/ManualProjectCreate-test.tsx.snap
sonar-core/src/main/resources/org/sonar/l10n/core.properties

diff --git a/server/sonar-web/src/main/js/apps/create/components/ProjectKeyInput.tsx b/server/sonar-web/src/main/js/apps/create/components/ProjectKeyInput.tsx
deleted file mode 100644 (file)
index 0fd857b..0000000
+++ /dev/null
@@ -1,129 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2019 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 * as classNames from 'classnames';
-import { debounce } from 'lodash';
-import * as React from 'react';
-import ValidationInput from 'sonar-ui-common/components/controls/ValidationInput';
-import { translate } from 'sonar-ui-common/helpers/l10n';
-import { doesComponentExists } from '../../../api/components';
-
-interface Props {
-  className?: string;
-  value: string;
-  onChange: (value: string | undefined) => void;
-}
-
-interface State {
-  error?: string;
-  touched: boolean;
-  validating: boolean;
-}
-
-export default class ProjectKeyInput extends React.PureComponent<Props, State> {
-  mounted = false;
-  constructor(props: Props) {
-    super(props);
-    this.state = { error: undefined, touched: false, validating: false };
-    this.checkFreeKey = debounce(this.checkFreeKey, 250);
-  }
-
-  componentDidMount() {
-    this.mounted = true;
-    if (this.props.value) {
-      this.validateKey(this.props.value);
-    }
-  }
-
-  componentWillUnmount() {
-    this.mounted = false;
-  }
-
-  checkFreeKey = (key: string) => {
-    this.setState({ validating: true });
-    return doesComponentExists({ component: key })
-      .then(alreadyExist => {
-        if (this.mounted && key === this.props.value) {
-          if (!alreadyExist) {
-            this.setState({ error: undefined, validating: false });
-          } else {
-            this.setState({
-              error: translate('onboarding.create_project.project_key.taken'),
-              touched: true,
-              validating: false
-            });
-          }
-        }
-      })
-      .catch(() => {
-        if (this.mounted && key === this.props.value) {
-          this.setState({ error: undefined, validating: false });
-        }
-      });
-  };
-
-  handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
-    const { value } = event.currentTarget;
-    this.setState({ touched: true });
-    this.validateKey(value);
-    this.props.onChange(value);
-  };
-
-  validateKey(key: string) {
-    if (key.length > 400 || !/^[\w-.:]*[a-zA-Z]+[\w-.:]*$/.test(key)) {
-      this.setState({
-        error: translate('onboarding.create_project.project_key.error'),
-        touched: true
-      });
-    } else {
-      this.checkFreeKey(key);
-    }
-  }
-
-  render() {
-    const isInvalid = this.state.touched && this.state.error !== undefined;
-    const isValid = this.state.touched && !this.state.validating && this.state.error === undefined;
-    return (
-      <ValidationInput
-        className={this.props.className}
-        description={translate('onboarding.create_project.project_key.description')}
-        error={this.state.error}
-        help={translate('onboarding.create_project.project_key.help')}
-        id="project-key"
-        isInvalid={isInvalid}
-        isValid={isValid}
-        label={translate('onboarding.create_project.project_key')}
-        required={true}>
-        <input
-          autoFocus={true}
-          className={classNames('input-super-large', {
-            'is-invalid': isInvalid,
-            'is-valid': isValid
-          })}
-          id="project-key"
-          maxLength={400}
-          minLength={1}
-          onChange={this.handleChange}
-          type="text"
-          value={this.props.value}
-        />
-      </ValidationInput>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/create/components/__tests__/ProjectKeyInput-test.tsx b/server/sonar-web/src/main/js/apps/create/components/__tests__/ProjectKeyInput-test.tsx
deleted file mode 100644 (file)
index 4f0214d..0000000
+++ /dev/null
@@ -1,87 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2019 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 { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
-import ProjectKeyInput from '../ProjectKeyInput';
-
-jest.useFakeTimers();
-
-jest.mock('../../../../api/components', () => ({
-  doesComponentExists: jest
-    .fn()
-    .mockImplementation(({ component }) => Promise.resolve(component === 'exists'))
-}));
-
-it('should render correctly', async () => {
-  const wrapper = shallow(<ProjectKeyInput onChange={jest.fn()} value="key" />);
-  expect(wrapper).toMatchSnapshot();
-  wrapper.setState({ touched: true });
-  await waitAndUpdate(wrapper);
-  expect(wrapper.find('ValidationInput').prop('isValid')).toBe(true);
-});
-
-it('should not display any status when the key is not defined', async () => {
-  const wrapper = shallow(<ProjectKeyInput onChange={jest.fn()} value="" />);
-  await waitAndUpdate(wrapper);
-  expect(wrapper.find('ValidationInput').prop('isInvalid')).toBe(false);
-  expect(wrapper.find('ValidationInput').prop('isValid')).toBe(false);
-});
-
-it('should have an error when the key is invalid', async () => {
-  const wrapper = shallow(<ProjectKeyInput onChange={jest.fn()} value="KEy-with#speci@l_char" />);
-  await waitAndUpdate(wrapper);
-  expect(wrapper.find('ValidationInput').prop('isInvalid')).toBe(true);
-});
-
-it('should have an error when the key already exists', async () => {
-  const wrapper = shallow(<ProjectKeyInput onChange={jest.fn()} value="exists" />);
-  await waitAndUpdate(wrapper);
-
-  jest.runAllTimers();
-  await new Promise(setImmediate);
-  expect(wrapper.find('ValidationInput').prop('isInvalid')).toBe(true);
-});
-
-it('should handle Change', async () => {
-  const onChange = jest.fn();
-  const wrapper = shallow(<ProjectKeyInput onChange={onChange} value="" />);
-  await waitAndUpdate(wrapper);
-
-  wrapper.find('input').simulate('change', { currentTarget: { value: 'key' } });
-
-  expect(wrapper.state('touched')).toBe(true);
-  expect(onChange).toBeCalledWith('key');
-});
-
-it('should ignore promise return if value has been changed in the meantime', async () => {
-  const onChange = (value: string) => wrapper.setProps({ value });
-  const wrapper = shallow(<ProjectKeyInput onChange={onChange} value="" />);
-  await waitAndUpdate(wrapper);
-
-  wrapper.find('input').simulate('change', { currentTarget: { value: 'exists' } });
-  wrapper.find('input').simulate('change', { currentTarget: { value: 'exists%' } });
-
-  jest.runAllTimers();
-  await new Promise(setImmediate);
-
-  expect(wrapper.state('touched')).toBe(true);
-  expect(wrapper.state('error')).toBe('onboarding.create_project.project_key.error');
-});
diff --git a/server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/ProjectKeyInput-test.tsx.snap b/server/sonar-web/src/main/js/apps/create/components/__tests__/__snapshots__/ProjectKeyInput-test.tsx.snap
deleted file mode 100644 (file)
index 58f34b9..0000000
+++ /dev/null
@@ -1,24 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render correctly 1`] = `
-<ValidationInput
-  description="onboarding.create_project.project_key.description"
-  help="onboarding.create_project.project_key.help"
-  id="project-key"
-  isInvalid={false}
-  isValid={false}
-  label="onboarding.create_project.project_key"
-  required={true}
->
-  <input
-    autoFocus={true}
-    className="input-super-large"
-    id="project-key"
-    maxLength={400}
-    minLength={1}
-    onChange={[Function]}
-    type="text"
-    value="key"
-  />
-</ValidationInput>
-`;
index 54c67f4766d3b7cf0b4be473b1fb93f7ad7b4fc8..5897ef1a9faaf51ef3062c5bed45979e8832ee6e 100644 (file)
@@ -18,7 +18,7 @@
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 .manual-project-create {
-  max-width: 650px;
+  max-width: 700px;
 }
 
 .manual-project-create .visibility-select-option {
index 40513c90d508f7766dbf22152c35376dcf1a77ea..485da00e2f888b83817cac84b751ae3a72c811e5 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as classNames from 'classnames';
+import { debounce } from 'lodash';
 import * as React from 'react';
 import { SubmitButton } from 'sonar-ui-common/components/controls/buttons';
-import HelpTooltip from 'sonar-ui-common/components/controls/HelpTooltip';
+import ValidationInput from 'sonar-ui-common/components/controls/ValidationInput';
 import DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner';
 import { translate } from 'sonar-ui-common/helpers/l10n';
-import { createProject } from '../../../api/components';
+import { createProject, doesComponentExists } from '../../../api/components';
 import VisibilitySelector from '../../../components/common/VisibilitySelector';
 import { isSonarCloud } from '../../../helpers/system';
-import ProjectKeyInput from '../components/ProjectKeyInput';
 import UpgradeOrganizationBox from '../components/UpgradeOrganizationBox';
 import './ManualProjectCreate.css';
 import OrganizationInput from './OrganizationInput';
@@ -42,10 +42,14 @@ interface Props {
 interface State {
   projectName: string;
   projectNameChanged: boolean;
+  projectNameError?: string;
   projectKey: string;
+  projectKeyError?: string;
   selectedOrganization?: T.Organization;
   selectedVisibility?: T.Visibility;
   submitting: boolean;
+  touched: boolean;
+  validating: boolean;
 }
 
 type ValidState = State & Required<Pick<State, 'projectKey' | 'projectName'>>;
@@ -60,8 +64,11 @@ export default class ManualProjectCreate extends React.PureComponent<Props, Stat
       projectName: '',
       projectNameChanged: false,
       selectedOrganization: this.getInitialSelectedOrganization(props),
-      submitting: false
+      submitting: false,
+      touched: false,
+      validating: false
     };
+    this.checkFreeKey = debounce(this.checkFreeKey, 250);
   }
 
   componentDidMount() {
@@ -72,13 +79,46 @@ export default class ManualProjectCreate extends React.PureComponent<Props, Stat
     this.mounted = false;
   }
 
+  checkFreeKey = (key: string) => {
+    return doesComponentExists({ component: key })
+      .then(alreadyExist => {
+        if (this.mounted && key === this.state.projectKey) {
+          if (!alreadyExist) {
+            this.setState({ projectKeyError: undefined, validating: false });
+          } else {
+            this.setState({
+              projectKeyError: translate('onboarding.create_project.project_key.taken'),
+              touched: true,
+              validating: false
+            });
+          }
+        }
+      })
+      .catch(() => {
+        if (this.mounted && key === this.state.projectKey) {
+          this.setState({ projectKeyError: undefined, validating: false });
+        }
+      });
+  };
+
   canChoosePrivate = (selectedOrganization: T.Organization | undefined) => {
     return Boolean(selectedOrganization && selectedOrganization.subscription === 'PAID');
   };
 
   canSubmit(state: State): state is ValidState {
+    const {
+      projectKey,
+      projectKeyError,
+      projectName,
+      projectNameError,
+      selectedOrganization
+    } = state;
     return Boolean(
-      state.projectKey && state.projectName && (!isSonarCloud() || state.selectedOrganization)
+      projectKeyError === undefined &&
+        projectNameError === undefined &&
+        projectKey.length > 0 &&
+        projectName.length > 0 &&
+        (!isSonarCloud() || selectedOrganization)
     );
   }
 
@@ -151,24 +191,67 @@ export default class ManualProjectCreate extends React.PureComponent<Props, Stat
     );
   };
 
-  handleProjectNameChange = (event: React.ChangeEvent<HTMLInputElement>) => {
-    const projectName = event.currentTarget.value;
-    this.setState({ projectName, projectNameChanged: true });
+  handleProjectKeyChange = (event: React.ChangeEvent<HTMLInputElement>) => {
+    const projectKey = event.currentTarget.value || '';
+    const projectKeyError = this.validateKey(projectKey);
+
+    this.setState(prevState => {
+      const projectName = prevState.projectNameChanged ? prevState.projectName : projectKey;
+      return {
+        projectKey,
+        projectKeyError,
+        projectName,
+        projectNameError: this.validateName(projectName),
+        touched: true,
+        validating: projectKeyError === undefined
+      };
+    });
+
+    if (projectKeyError === undefined) {
+      this.checkFreeKey(projectKey);
+    }
   };
 
-  handleProjectKeyChange = (projectKey: string) => {
-    this.setState(state => ({
-      projectKey,
-      projectName: state.projectNameChanged ? state.projectName : projectKey || ''
-    }));
+  handleProjectNameChange = (event: React.ChangeEvent<HTMLInputElement>) => {
+    const projectName = event.currentTarget.value;
+    this.setState({
+      projectName,
+      projectNameChanged: true,
+      projectNameError: this.validateName(projectName)
+    });
   };
 
   handleVisibilityChange = (selectedVisibility: T.Visibility) => {
     this.setState({ selectedVisibility });
   };
 
+  validateKey = (projectKey: string) => {
+    return projectKey.length > 400 || !/^[\w-.:]*[a-zA-Z]+[\w-.:]*$/.test(projectKey)
+      ? translate('onboarding.create_project.project_key.error')
+      : undefined;
+  };
+
+  validateName = (projectName: string) => {
+    return projectName.length === 0 || projectName.length > 255
+      ? translate('onboarding.create_project.display_name.error')
+      : undefined;
+  };
+
   render() {
-    const { selectedOrganization, submitting } = this.state;
+    const {
+      projectKey,
+      projectKeyError,
+      projectName,
+      projectNameError,
+      selectedOrganization,
+      submitting,
+      touched,
+      validating
+    } = this.state;
+    const projectKeyIsInvalid = touched && projectKeyError !== undefined;
+    const projectKeyIsValid = touched && !validating && projectKeyError === undefined;
+    const projectNameIsInvalid = touched && projectNameError !== undefined;
+    const projectNameIsValid = touched && projectNameError === undefined;
     const canChoosePrivate = this.canChoosePrivate(selectedOrganization);
 
     return (
@@ -182,37 +265,56 @@ export default class ManualProjectCreate extends React.PureComponent<Props, Stat
                 organizations={this.props.userOrganizations}
               />
             )}
-            <ProjectKeyInput
+
+            <ValidationInput
               className="form-field"
-              onChange={this.handleProjectKeyChange}
-              value={this.state.projectKey}
-            />
-            <div className="form-field">
-              <label htmlFor="project-name">
-                <span className="text-middle">
-                  <strong>{translate('onboarding.create_project.display_name')}</strong>
-                  <em className="mandatory">*</em>
-                </span>
-                <HelpTooltip
-                  className="spacer-left"
-                  overlay={translate('onboarding.create_project.display_name.help')}
-                />
-              </label>
-              <div className="little-spacer-top spacer-bottom">
-                <input
-                  className="input-super-large"
-                  id="project-name"
-                  maxLength={255}
-                  minLength={1}
-                  onChange={this.handleProjectNameChange}
-                  type="text"
-                  value={this.state.projectName}
-                />
-              </div>
-              <div className="note abs-width-400">
-                {translate('onboarding.create_project.display_name.description')}
-              </div>
-            </div>
+              description={translate('onboarding.create_project.project_key.description')}
+              error={projectKeyError}
+              help={translate('onboarding.create_project.project_key.help')}
+              id="project-key"
+              isInvalid={projectKeyIsInvalid}
+              isValid={projectKeyIsValid}
+              label={translate('onboarding.create_project.project_key')}
+              required={true}>
+              <input
+                autoFocus={true}
+                className={classNames('input-super-large', {
+                  'is-invalid': projectKeyIsInvalid,
+                  'is-valid': projectKeyIsValid
+                })}
+                id="project-key"
+                maxLength={400}
+                minLength={1}
+                onChange={this.handleProjectKeyChange}
+                type="text"
+                value={projectKey}
+              />
+            </ValidationInput>
+
+            <ValidationInput
+              className="form-field"
+              description={translate('onboarding.create_project.display_name.description')}
+              error={projectNameError}
+              help={translate('onboarding.create_project.display_name.help')}
+              id="project-name"
+              isInvalid={projectNameIsInvalid}
+              isValid={projectNameIsValid}
+              label={translate('onboarding.create_project.display_name')}
+              required={true}>
+              <input
+                className={classNames('input-super-large', {
+                  'is-invalid': projectNameIsInvalid,
+                  'is-valid': projectNameIsValid
+                })}
+                id="project-name"
+                maxLength={255}
+                minLength={1}
+                onChange={this.handleProjectNameChange}
+                type="text"
+                value={projectName}
+              />
+            </ValidationInput>
+
             {isSonarCloud() && selectedOrganization && (
               <div
                 className={classNames('visibility-select-wrapper', {
@@ -226,6 +328,7 @@ export default class ManualProjectCreate extends React.PureComponent<Props, Stat
                 />
               </div>
             )}
+
             <SubmitButton disabled={!this.canSubmit(this.state) || submitting}>
               {translate('set_up')}
             </SubmitButton>
index d0815b405c77720de0e6c37ae8745a74875fd866..93ec3cfb70f813be5dfb457e68c13886767ad53a 100644 (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.
  */
+/* eslint-disable sonarjs/no-duplicate-string */
 import { shallow } from 'enzyme';
 import * as React from 'react';
 import { change, submit, waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
@@ -24,7 +25,10 @@ import { createProject } from '../../../../api/components';
 import ManualProjectCreate from '../ManualProjectCreate';
 
 jest.mock('../../../../api/components', () => ({
-  createProject: jest.fn().mockResolvedValue({ project: { key: 'bar', name: 'Bar' } })
+  createProject: jest.fn().mockResolvedValue({ project: { key: 'bar', name: 'Bar' } }),
+  doesComponentExists: jest
+    .fn()
+    .mockImplementation(({ component }) => Promise.resolve(component === 'exists'))
 }));
 
 jest.mock('../../../../helpers/system', () => ({
@@ -32,19 +36,19 @@ jest.mock('../../../../helpers/system', () => ({
 }));
 
 beforeEach(() => {
-  (createProject as jest.Mock<any>).mockClear();
+  jest.clearAllMocks();
 });
 
 it('should render correctly', () => {
-  expect(getWrapper()).toMatchSnapshot();
+  expect(shallowRender()).toMatchSnapshot();
 });
 
 it('should correctly create a public project', async () => {
   const onProjectCreate = jest.fn();
-  const wrapper = getWrapper({ onProjectCreate });
+  const wrapper = shallowRender({ onProjectCreate });
   wrapper.find('withRouter(OrganizationInput)').prop<Function>('onChange')({ key: 'foo' });
 
-  change(wrapper.find('ProjectKeyInput'), 'bar');
+  change(wrapper.find('input#project-key'), 'bar');
   change(wrapper.find('input#project-name'), 'Bar');
   expect(wrapper.find('SubmitButton').prop('disabled')).toBe(false);
 
@@ -62,10 +66,10 @@ it('should correctly create a public project', async () => {
 
 it('should correctly create a private project', async () => {
   const onProjectCreate = jest.fn();
-  const wrapper = getWrapper({ onProjectCreate });
+  const wrapper = shallowRender({ onProjectCreate });
   wrapper.find('withRouter(OrganizationInput)').prop<Function>('onChange')({ key: 'bar' });
 
-  change(wrapper.find('ProjectKeyInput'), 'bar');
+  change(wrapper.find('input#project-key'), 'bar');
   change(wrapper.find('input#project-name'), 'Bar');
 
   submit(wrapper.find('form'));
@@ -80,8 +84,77 @@ it('should correctly create a private project', async () => {
   expect(onProjectCreate).toBeCalledWith(['bar']);
 });
 
-function getWrapper(props = {}) {
-  return shallow(
+it('should not display any status when the key is not defined', () => {
+  const wrapper = shallowRender();
+  const projectKeyInput = wrapper.find('ValidationInput').first();
+  expect(projectKeyInput.prop('isInvalid')).toBe(false);
+  expect(projectKeyInput.prop('isValid')).toBe(false);
+});
+
+it('should not display any status when the name is not defined', () => {
+  const wrapper = shallowRender();
+  const projectKeyInput = wrapper.find('ValidationInput').last();
+  expect(projectKeyInput.prop('isInvalid')).toBe(false);
+  expect(projectKeyInput.prop('isValid')).toBe(false);
+});
+
+it('should have an error when the key is invalid', () => {
+  const wrapper = shallowRender();
+  change(wrapper.find('input#project-key'), 'KEy-with#speci@l_char');
+  expect(
+    wrapper
+      .find('ValidationInput')
+      .first()
+      .prop('isInvalid')
+  ).toBe(true);
+});
+
+it('should have an error when the key already exists', async () => {
+  const wrapper = shallowRender();
+  change(wrapper.find('input#project-key'), 'exists');
+
+  await waitAndUpdate(wrapper);
+  expect(
+    wrapper
+      .find('ValidationInput')
+      .first()
+      .prop('isInvalid')
+  ).toBe(true);
+});
+
+it('should ignore promise return if value has been changed in the meantime', async () => {
+  const wrapper = shallowRender();
+
+  change(wrapper.find('input#project-key'), 'exists');
+  change(wrapper.find('input#project-key'), 'exists%');
+
+  await waitAndUpdate(wrapper);
+
+  expect(wrapper.state('touched')).toBe(true);
+  expect(wrapper.state('projectKeyError')).toBe('onboarding.create_project.project_key.error');
+});
+
+it('should autofill the name based on the key', () => {
+  const wrapper = shallowRender();
+  change(wrapper.find('input#project-key'), 'bar');
+  expect(wrapper.find('input#project-name').prop('value')).toBe('bar');
+});
+
+it('should have an error when the name is empty', () => {
+  const wrapper = shallowRender();
+  change(wrapper.find('input#project-key'), 'bar');
+  change(wrapper.find('input#project-name'), '');
+  expect(
+    wrapper
+      .find('ValidationInput')
+      .last()
+      .prop('isInvalid')
+  ).toBe(true);
+  expect(wrapper.state('projectNameError')).toBe('onboarding.create_project.display_name.error');
+});
+
+function shallowRender(props: Partial<ManualProjectCreate['props']> = {}) {
+  return shallow<ManualProjectCreate>(
     <ManualProjectCreate
       currentUser={{ groups: [], isLoggedIn: true, login: 'foo', name: 'Foo', scmAccounts: [] }}
       fetchMyOrganizations={jest.fn()}
index 3f8d2aaee08bec0cee678763beb760ce8ac4e7b1..a68eafd3480b7c43ac6a0a045a061a0b1b26280c 100644 (file)
@@ -28,53 +28,47 @@ exports[`should render correctly 1`] = `
           ]
         }
       />
-      <ProjectKeyInput
+      <ValidationInput
         className="form-field"
-        onChange={[Function]}
-        value=""
-      />
-      <div
+        description="onboarding.create_project.project_key.description"
+        help="onboarding.create_project.project_key.help"
+        id="project-key"
+        isInvalid={false}
+        isValid={false}
+        label="onboarding.create_project.project_key"
+        required={true}
+      >
+        <input
+          autoFocus={true}
+          className="input-super-large"
+          id="project-key"
+          maxLength={400}
+          minLength={1}
+          onChange={[Function]}
+          type="text"
+          value=""
+        />
+      </ValidationInput>
+      <ValidationInput
         className="form-field"
+        description="onboarding.create_project.display_name.description"
+        help="onboarding.create_project.display_name.help"
+        id="project-name"
+        isInvalid={false}
+        isValid={false}
+        label="onboarding.create_project.display_name"
+        required={true}
       >
-        <label
-          htmlFor="project-name"
-        >
-          <span
-            className="text-middle"
-          >
-            <strong>
-              onboarding.create_project.display_name
-            </strong>
-            <em
-              className="mandatory"
-            >
-              *
-            </em>
-          </span>
-          <HelpTooltip
-            className="spacer-left"
-            overlay="onboarding.create_project.display_name.help"
-          />
-        </label>
-        <div
-          className="little-spacer-top spacer-bottom"
-        >
-          <input
-            className="input-super-large"
-            id="project-name"
-            maxLength={255}
-            minLength={1}
-            onChange={[Function]}
-            type="text"
-            value=""
-          />
-        </div>
-        <div
-          className="note abs-width-400"
-        >
-          onboarding.create_project.display_name.description
-        </div>
-      </div>
+        <input
+          className="input-super-large"
+          id="project-name"
+          maxLength={255}
+          minLength={1}
+          onChange={[Function]}
+          type="text"
+          value=""
+        />
+      </ValidationInput>
       <SubmitButton
         disabled={true}
       >
index d7c8865ddccf55889c144e46a9aeff6253d3a658..9bb347d45f2578d59ebb3070e90826fbaa986ce8 100644 (file)
@@ -2857,7 +2857,7 @@ onboarding.create_project.project_key.error=The provided value doesn't match the
 onboarding.create_project.project_key.help=Your project key is a unique identifier for your project. If you are using Maven, make sure the key matches the "groupId:artifactId" format.
 onboarding.create_project.project_key.taken=This project key is already taken.
 onboarding.create_project.display_name=Display name
-onboarding.create_project.display_name.error=The provided value doesn't match the expected format.
+onboarding.create_project.display_name.error=The display name is required.
 onboarding.create_project.display_name.description=Up to 255 characters
 onboarding.create_project.display_name.help=Some scanners might override the value you provide.
 onboarding.create_project.repository_imported=Already imported: {link}