]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-14604 Improve reset password form layout
authorWouter Admiraal <wouter.admiraal@sonarsource.com>
Tue, 27 Apr 2021 13:51:18 +0000 (15:51 +0200)
committersonartech <sonartech@sonarsource.com>
Thu, 29 Apr 2021 20:03:27 +0000 (20:03 +0000)
14 files changed:
server/sonar-web/src/main/js/app/components/ResetPassword.css [deleted file]
server/sonar-web/src/main/js/app/components/ResetPassword.tsx
server/sonar-web/src/main/js/app/components/__tests__/ResetPassword-test.tsx
server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/ResetPassword-test.tsx.snap
server/sonar-web/src/main/js/apps/account/components/Security.tsx
server/sonar-web/src/main/js/apps/account/components/__tests__/__snapshots__/Security-test.tsx.snap
server/sonar-web/src/main/js/apps/change-admin-password/ChangeAdminPasswordAppRenderer.tsx
server/sonar-web/src/main/js/apps/change-admin-password/__tests__/__snapshots__/ChangeAdminPasswordAppRenderer-test.tsx.snap
server/sonar-web/src/main/js/components/common/ResetPassword.tsx [deleted file]
server/sonar-web/src/main/js/components/common/ResetPasswordForm.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/common/__tests__/ResetPassword-test.tsx [deleted file]
server/sonar-web/src/main/js/components/common/__tests__/ResetPasswordForm-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/ResetPassword-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/ResetPasswordForm-test.tsx.snap [new file with mode: 0644]

diff --git a/server/sonar-web/src/main/js/app/components/ResetPassword.css b/server/sonar-web/src/main/js/app/components/ResetPassword.css
deleted file mode 100644 (file)
index e0a344a..0000000
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2021 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.
- */
-.reset-page {
-  padding-top: 10vh;
-}
-
-.reset-page h1 {
-  line-height: 1.5;
-  font-size: 24px;
-  font-weight: 300;
-  text-align: center;
-}
-
-.reset-form {
-  width: 300px;
-  margin-left: auto;
-  margin-right: auto;
-}
index 9a55f23a4d25d34b0b636c3c40533b5c60bc4ffd..55ef8b4c88fc9ca92a2c4a785a634a2b851a40ae 100644 (file)
  */
 import * as React from 'react';
 import { translate } from 'sonar-ui-common/helpers/l10n';
-import ResetPasswordForm from '../../components/common/ResetPassword';
+import ResetPasswordForm from '../../components/common/ResetPasswordForm';
 import { whenLoggedIn } from '../../components/hoc/whenLoggedIn';
+import { getBaseUrl } from '../../helpers/system';
 import GlobalMessagesContainer from './GlobalMessagesContainer';
-import './ResetPassword.css';
 
 export interface ResetPasswordProps {
   currentUser: T.LoggedInUser;
 }
 
-export function ResetPassword(props: ResetPasswordProps) {
-  const { currentUser } = props;
-  const redirect = () => {
-    window.location.href = `/`; // force a refresh for the backend to handle additional redirects
-  };
-
+export function ResetPassword({ currentUser }: ResetPasswordProps) {
   return (
-    <div className="reset-page">
-      <h1 className="text-center spacer-bottom">{translate('my_account.reset_password')}</h1>
-      <h2 className="text-center huge-spacer-bottom">
-        {translate('my_account.reset_password.explain')}
-      </h2>
-      <GlobalMessagesContainer />
-      <div className="reset-form">
-        <ResetPasswordForm user={currentUser} onPasswordChange={redirect} />
+    <div className="page-wrapper-simple">
+      <div className="page-simple">
+        <GlobalMessagesContainer />
+
+        <h1 className="text-center huge">{translate('my_account.reset_password')}</h1>
+        <p className="text-center huge-spacer-top huge-spacer-bottom">
+          {translate('my_account.reset_password.explain')}
+        </p>
+
+        <div className="text-center">
+          <h2 className="big-spacer-bottom big">{translate('my_profile.password.title')}</h2>
+
+          <ResetPasswordForm
+            user={currentUser}
+            onPasswordChange={() => {
+              // Force a refresh for the backend to handle additional redirects.
+              window.location.href = getBaseUrl() + '/';
+            }}
+          />
+        </div>
       </div>
     </div>
   );
index 0f3093f3f5142716d695ea306f53a230784aed5a..56b33efcc57f3cc17c18694e81f55db1485acb50 100644 (file)
  */
 import { shallow } from 'enzyme';
 import * as React from 'react';
+import ResetPasswordForm from '../../../components/common/ResetPasswordForm';
 import { mockLoggedInUser } from '../../../helpers/testMocks';
 import { ResetPassword, ResetPasswordProps } from '../ResetPassword';
 
+jest.mock('../../../helpers/system', () => ({
+  getBaseUrl: jest.fn().mockReturnValue('/context')
+}));
+
+const originalLocation = window.location;
+
+beforeAll(() => {
+  const location = {
+    ...window.location,
+    href: null
+  };
+  Object.defineProperty(window, 'location', {
+    writable: true,
+    value: location
+  });
+});
+
+afterAll(() => {
+  Object.defineProperty(window, 'location', {
+    writable: true,
+    value: originalLocation
+  });
+});
+
 it('should render correctly', () => {
   expect(shallowRender()).toMatchSnapshot();
 });
 
+it('should navigate to the homepage after submission', () => {
+  const wrapper = shallowRender();
+  const form = wrapper.find(ResetPasswordForm);
+  const { onPasswordChange } = form.props();
+
+  if (onPasswordChange) {
+    onPasswordChange();
+  }
+
+  expect(window.location.href).toBe('/context/');
+});
+
 function shallowRender(props: Partial<ResetPasswordProps> = {}) {
   return shallow(<ResetPassword currentUser={mockLoggedInUser()} {...props} />);
 }
index ad2db130e85f33bdec14548470fbe0bd29608976..cf445a7dcb5df630afc5c2a520921098c96a3f5f 100644 (file)
@@ -2,34 +2,43 @@
 
 exports[`should render correctly 1`] = `
 <div
-  className="reset-page"
+  className="page-wrapper-simple"
 >
-  <h1
-    className="text-center spacer-bottom"
-  >
-    my_account.reset_password
-  </h1>
-  <h2
-    className="text-center huge-spacer-bottom"
-  >
-    my_account.reset_password.explain
-  </h2>
-  <Connect(GlobalMessages) />
   <div
-    className="reset-form"
+    className="page-simple"
   >
-    <ResetPassword
-      onPasswordChange={[Function]}
-      user={
-        Object {
-          "groups": Array [],
-          "isLoggedIn": true,
-          "login": "luke",
-          "name": "Skywalker",
-          "scmAccounts": Array [],
+    <Connect(GlobalMessages) />
+    <h1
+      className="text-center huge"
+    >
+      my_account.reset_password
+    </h1>
+    <p
+      className="text-center huge-spacer-top huge-spacer-bottom"
+    >
+      my_account.reset_password.explain
+    </p>
+    <div
+      className="text-center"
+    >
+      <h2
+        className="big-spacer-bottom big"
+      >
+        my_profile.password.title
+      </h2>
+      <ResetPasswordForm
+        onPasswordChange={[Function]}
+        user={
+          Object {
+            "groups": Array [],
+            "isLoggedIn": true,
+            "login": "luke",
+            "name": "Skywalker",
+            "scmAccounts": Array [],
+          }
         }
-      }
-    />
+      />
+    </div>
   </div>
 </div>
 `;
index e6c0af202389151766f38af5014c7052c5af95df..b530096017573adc926d0f6d91f54a9f222562b5 100644 (file)
@@ -21,7 +21,7 @@ import * as React from 'react';
 import { Helmet } from 'react-helmet-async';
 import { connect } from 'react-redux';
 import { translate } from 'sonar-ui-common/helpers/l10n';
-import ResetPassword from '../../../components/common/ResetPassword';
+import ResetPasswordForm from '../../../components/common/ResetPasswordForm';
 import { getCurrentUser, Store } from '../../../store/rootReducer';
 import Tokens from './Tokens';
 
@@ -34,7 +34,12 @@ export function Security({ user }: SecurityProps) {
     <div className="account-body account-container">
       <Helmet defer={false} title={translate('my_account.security')} />
       <Tokens login={user.login} />
-      {user.local && <ResetPassword user={user} />}
+      {user.local && (
+        <section className="boxed-group">
+          <h2 className="spacer-bottom">{translate('my_profile.password.title')}</h2>
+          <ResetPasswordForm className="boxed-group-inner" user={user} />
+        </section>
+      )}
     </div>
   );
 }
index 4adb1b2c1c23c60bb95cebf8cbde3dc5844ca1c8..c120e9b1c330441a6e9b7b91ffbbca2037c29989 100644 (file)
@@ -12,18 +12,28 @@ exports[`should render correctly: local user 1`] = `
   <Tokens
     login="luke"
   />
-  <ResetPassword
-    user={
-      Object {
-        "groups": Array [],
-        "isLoggedIn": true,
-        "local": true,
-        "login": "luke",
-        "name": "Skywalker",
-        "scmAccounts": Array [],
+  <section
+    className="boxed-group"
+  >
+    <h2
+      className="spacer-bottom"
+    >
+      my_profile.password.title
+    </h2>
+    <ResetPasswordForm
+      className="boxed-group-inner"
+      user={
+        Object {
+          "groups": Array [],
+          "isLoggedIn": true,
+          "local": true,
+          "login": "luke",
+          "name": "Skywalker",
+          "scmAccounts": Array [],
+        }
       }
-    }
-  />
+    />
+  </section>
 </div>
 `;
 
index 0f01456ccbab083728f60efe49362db3b4efcc0d..cee0dce669e873b25e6cfd3a95156846c0dc95d3 100644 (file)
@@ -137,7 +137,7 @@ export default function ChangeAdminPasswordAppRenderer(props: ChangeAdminPasswor
 
               <div className="form-field">
                 <SubmitButton disabled={!canSubmit || submitting}>
-                  {translate('change_verb')}
+                  {translate('update_verb')}
                   {submitting && <i className="spinner spacer-left" />}
                 </SubmitButton>
               </div>
index 5c5895a57828a5024d5a5dea5140f0d7f45fb2a6..836b65c7375731dacacb4532afe39c554bb9baea 100644 (file)
@@ -79,7 +79,7 @@ exports[`should render correctly: cannot submit 1`] = `
         <SubmitButton
           disabled={true}
         >
-          change_verb
+          update_verb
         </SubmitButton>
       </div>
     </form>
@@ -164,7 +164,7 @@ exports[`should render correctly: default 1`] = `
         <SubmitButton
           disabled={false}
         >
-          change_verb
+          update_verb
         </SubmitButton>
       </div>
     </form>
@@ -249,7 +249,7 @@ exports[`should render correctly: submitting 1`] = `
         <SubmitButton
           disabled={true}
         >
-          change_verb
+          update_verb
           <i
             className="spinner spacer-left"
           />
@@ -368,7 +368,7 @@ exports[`should render correctly: trying to use default admin password 1`] = `
         <SubmitButton
           disabled={false}
         >
-          change_verb
+          update_verb
         </SubmitButton>
       </div>
     </form>
diff --git a/server/sonar-web/src/main/js/components/common/ResetPassword.tsx b/server/sonar-web/src/main/js/components/common/ResetPassword.tsx
deleted file mode 100644 (file)
index ff59d0a..0000000
+++ /dev/null
@@ -1,155 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2021 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 React from 'react';
-import { SubmitButton } from 'sonar-ui-common/components/controls/buttons';
-import { Alert } from 'sonar-ui-common/components/ui/Alert';
-import MandatoryFieldMarker from 'sonar-ui-common/components/ui/MandatoryFieldMarker';
-import MandatoryFieldsExplanation from 'sonar-ui-common/components/ui/MandatoryFieldsExplanation';
-import { translate } from 'sonar-ui-common/helpers/l10n';
-import { changePassword } from '../../api/users';
-
-interface Props {
-  user: T.LoggedInUser;
-  onPasswordChange?: () => void;
-}
-
-interface State {
-  errors?: string[];
-  success: boolean;
-}
-
-export default class ResetPassword extends React.Component<Props, State> {
-  oldPassword: HTMLInputElement | null = null;
-  password: HTMLInputElement | null = null;
-  passwordConfirmation: HTMLInputElement | null = null;
-  state: State = {
-    success: false
-  };
-
-  handleSuccessfulChange = () => {
-    if (!this.oldPassword || !this.password || !this.passwordConfirmation) {
-      return;
-    }
-    this.oldPassword.value = '';
-    this.password.value = '';
-    this.passwordConfirmation.value = '';
-    this.setState({ success: true, errors: undefined });
-    if (this.props.onPasswordChange) {
-      this.props.onPasswordChange();
-    }
-  };
-
-  setErrors = (errors: string[]) => {
-    this.setState({ success: false, errors });
-  };
-
-  handleChangePassword = (event: React.FormEvent) => {
-    event.preventDefault();
-    if (!this.oldPassword || !this.password || !this.passwordConfirmation) {
-      return;
-    }
-    const { user } = this.props;
-    const previousPassword = this.oldPassword.value;
-    const password = this.password.value;
-    const passwordConfirmation = this.passwordConfirmation.value;
-
-    if (password !== passwordConfirmation) {
-      this.password.focus();
-      this.setErrors([translate('user.password_doesnt_match_confirmation')]);
-    } else {
-      changePassword({ login: user.login, password, previousPassword }).then(
-        this.handleSuccessfulChange,
-        () => {
-          // error already reported.
-        }
-      );
-    }
-  };
-
-  render() {
-    const { success, errors } = this.state;
-
-    return (
-      <section className="boxed-group">
-        <h2 className="spacer-bottom">{translate('my_profile.password.title')}</h2>
-
-        <form className="boxed-group-inner" onSubmit={this.handleChangePassword}>
-          {success && <Alert variant="success">{translate('my_profile.password.changed')}</Alert>}
-
-          {errors &&
-            errors.map((e, i) => (
-              /* eslint-disable-next-line react/no-array-index-key */
-              <Alert key={i} variant="error">
-                {e}
-              </Alert>
-            ))}
-
-          <MandatoryFieldsExplanation className="form-field" />
-
-          <div className="form-field">
-            <label htmlFor="old_password">
-              {translate('my_profile.password.old')}
-              <MandatoryFieldMarker />
-            </label>
-            <input
-              autoComplete="off"
-              id="old_password"
-              name="old_password"
-              ref={elem => (this.oldPassword = elem)}
-              required={true}
-              type="password"
-            />
-          </div>
-          <div className="form-field">
-            <label htmlFor="password">
-              {translate('my_profile.password.new')}
-              <MandatoryFieldMarker />
-            </label>
-            <input
-              autoComplete="off"
-              id="password"
-              name="password"
-              ref={elem => (this.password = elem)}
-              required={true}
-              type="password"
-            />
-          </div>
-          <div className="form-field">
-            <label htmlFor="password_confirmation">
-              {translate('my_profile.password.confirm')}
-              <MandatoryFieldMarker />
-            </label>
-            <input
-              autoComplete="off"
-              id="password_confirmation"
-              name="password_confirmation"
-              ref={elem => (this.passwordConfirmation = elem)}
-              required={true}
-              type="password"
-            />
-          </div>
-          <div className="form-field">
-            <SubmitButton id="change-password">{translate('update_verb')}</SubmitButton>
-          </div>
-        </form>
-      </section>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/components/common/ResetPasswordForm.tsx b/server/sonar-web/src/main/js/components/common/ResetPasswordForm.tsx
new file mode 100644 (file)
index 0000000..8f46d2f
--- /dev/null
@@ -0,0 +1,153 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 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 React from 'react';
+import { SubmitButton } from 'sonar-ui-common/components/controls/buttons';
+import { Alert } from 'sonar-ui-common/components/ui/Alert';
+import MandatoryFieldMarker from 'sonar-ui-common/components/ui/MandatoryFieldMarker';
+import MandatoryFieldsExplanation from 'sonar-ui-common/components/ui/MandatoryFieldsExplanation';
+import { translate } from 'sonar-ui-common/helpers/l10n';
+import { changePassword } from '../../api/users';
+
+interface Props {
+  className?: string;
+  user: T.LoggedInUser;
+  onPasswordChange?: () => void;
+}
+
+interface State {
+  errors?: string[];
+  success: boolean;
+}
+
+export default class ResetPasswordForm extends React.Component<Props, State> {
+  oldPassword: HTMLInputElement | null = null;
+  password: HTMLInputElement | null = null;
+  passwordConfirmation: HTMLInputElement | null = null;
+  state: State = {
+    success: false
+  };
+
+  handleSuccessfulChange = () => {
+    if (!this.oldPassword || !this.password || !this.passwordConfirmation) {
+      return;
+    }
+    this.oldPassword.value = '';
+    this.password.value = '';
+    this.passwordConfirmation.value = '';
+    this.setState({ success: true, errors: undefined });
+    if (this.props.onPasswordChange) {
+      this.props.onPasswordChange();
+    }
+  };
+
+  setErrors = (errors: string[]) => {
+    this.setState({ success: false, errors });
+  };
+
+  handleChangePassword = (event: React.FormEvent) => {
+    event.preventDefault();
+    if (!this.oldPassword || !this.password || !this.passwordConfirmation) {
+      return;
+    }
+    const { user } = this.props;
+    const previousPassword = this.oldPassword.value;
+    const password = this.password.value;
+    const passwordConfirmation = this.passwordConfirmation.value;
+
+    if (password !== passwordConfirmation) {
+      this.password.focus();
+      this.setErrors([translate('user.password_doesnt_match_confirmation')]);
+    } else {
+      changePassword({ login: user.login, password, previousPassword }).then(
+        this.handleSuccessfulChange,
+        () => {
+          // error already reported.
+        }
+      );
+    }
+  };
+
+  render() {
+    const { className } = this.props;
+    const { success, errors } = this.state;
+
+    return (
+      <form className={className} onSubmit={this.handleChangePassword}>
+        {success && <Alert variant="success">{translate('my_profile.password.changed')}</Alert>}
+
+        {errors &&
+          errors.map((e, i) => (
+            /* eslint-disable-next-line react/no-array-index-key */
+            <Alert key={i} variant="error">
+              {e}
+            </Alert>
+          ))}
+
+        <MandatoryFieldsExplanation className="form-field" />
+
+        <div className="form-field">
+          <label htmlFor="old_password">
+            {translate('my_profile.password.old')}
+            <MandatoryFieldMarker />
+          </label>
+          <input
+            autoComplete="off"
+            id="old_password"
+            name="old_password"
+            ref={elem => (this.oldPassword = elem)}
+            required={true}
+            type="password"
+          />
+        </div>
+        <div className="form-field">
+          <label htmlFor="password">
+            {translate('my_profile.password.new')}
+            <MandatoryFieldMarker />
+          </label>
+          <input
+            autoComplete="off"
+            id="password"
+            name="password"
+            ref={elem => (this.password = elem)}
+            required={true}
+            type="password"
+          />
+        </div>
+        <div className="form-field">
+          <label htmlFor="password_confirmation">
+            {translate('my_profile.password.confirm')}
+            <MandatoryFieldMarker />
+          </label>
+          <input
+            autoComplete="off"
+            id="password_confirmation"
+            name="password_confirmation"
+            ref={elem => (this.passwordConfirmation = elem)}
+            required={true}
+            type="password"
+          />
+        </div>
+        <div className="form-field">
+          <SubmitButton id="change-password">{translate('update_verb')}</SubmitButton>
+        </div>
+      </form>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/components/common/__tests__/ResetPassword-test.tsx b/server/sonar-web/src/main/js/components/common/__tests__/ResetPassword-test.tsx
deleted file mode 100644 (file)
index 904f693..0000000
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2021 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 { changePassword } from '../../../api/users';
-import { mockEvent, mockLoggedInUser } from '../../../helpers/testMocks';
-import ResetPassword from '../ResetPassword';
-
-jest.mock('../../../api/users', () => ({
-  changePassword: jest.fn().mockResolvedValue({})
-}));
-
-it('should trigger on password change prop', () => {
-  const onPasswordChange = jest.fn();
-  const wrapper = shallowRender({ onPasswordChange });
-  wrapper.instance().handleSuccessfulChange();
-  expect(onPasswordChange).not.toBeCalled();
-  wrapper.instance().oldPassword = { value: '' } as HTMLInputElement;
-  wrapper.instance().password = { value: '' } as HTMLInputElement;
-  wrapper.instance().passwordConfirmation = { value: '' } as HTMLInputElement;
-  wrapper.instance().handleSuccessfulChange();
-  expect(onPasswordChange).toBeCalled();
-});
-
-it('should not trigger password change', () => {
-  const wrapper = shallowRender();
-  wrapper.instance().oldPassword = { value: 'testold' } as HTMLInputElement;
-  wrapper.instance().password = { value: 'test', focus: () => {} } as HTMLInputElement;
-  wrapper.instance().passwordConfirmation = { value: 'test1' } as HTMLInputElement;
-  wrapper.instance().handleChangePassword(mockEvent());
-  expect(changePassword).not.toBeCalled();
-  expect(wrapper.state().errors).toBeDefined();
-});
-
-it('should trigger password change', async () => {
-  const user = mockLoggedInUser();
-  const wrapper = shallowRender({ user });
-  wrapper.instance().handleChangePassword(mockEvent());
-  await waitAndUpdate(wrapper);
-  expect(changePassword).not.toBeCalled();
-
-  wrapper.instance().oldPassword = { value: 'testold' } as HTMLInputElement;
-  wrapper.instance().password = { value: 'test' } as HTMLInputElement;
-  wrapper.instance().passwordConfirmation = { value: 'test' } as HTMLInputElement;
-  wrapper.instance().handleChangePassword(mockEvent());
-  await waitAndUpdate(wrapper);
-
-  expect(changePassword).toBeCalledWith({
-    login: user.login,
-    password: 'test',
-    previousPassword: 'testold'
-  });
-});
-
-it('renders correctly', () => {
-  expect(shallowRender()).toMatchSnapshot();
-});
-
-function shallowRender(props?: Partial<ResetPassword['props']>) {
-  return shallow<ResetPassword>(<ResetPassword user={mockLoggedInUser()} {...props} />);
-}
diff --git a/server/sonar-web/src/main/js/components/common/__tests__/ResetPasswordForm-test.tsx b/server/sonar-web/src/main/js/components/common/__tests__/ResetPasswordForm-test.tsx
new file mode 100644 (file)
index 0000000..e940f20
--- /dev/null
@@ -0,0 +1,79 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 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 { changePassword } from '../../../api/users';
+import { mockEvent, mockLoggedInUser } from '../../../helpers/testMocks';
+import ResetPasswordForm from '../ResetPasswordForm';
+
+jest.mock('../../../api/users', () => ({
+  changePassword: jest.fn().mockResolvedValue({})
+}));
+
+it('should trigger on password change prop', () => {
+  const onPasswordChange = jest.fn();
+  const wrapper = shallowRender({ onPasswordChange });
+  wrapper.instance().handleSuccessfulChange();
+  expect(onPasswordChange).not.toBeCalled();
+  wrapper.instance().oldPassword = { value: '' } as HTMLInputElement;
+  wrapper.instance().password = { value: '' } as HTMLInputElement;
+  wrapper.instance().passwordConfirmation = { value: '' } as HTMLInputElement;
+  wrapper.instance().handleSuccessfulChange();
+  expect(onPasswordChange).toBeCalled();
+});
+
+it('should not trigger password change', () => {
+  const wrapper = shallowRender();
+  wrapper.instance().oldPassword = { value: 'testold' } as HTMLInputElement;
+  wrapper.instance().password = { value: 'test', focus: () => {} } as HTMLInputElement;
+  wrapper.instance().passwordConfirmation = { value: 'test1' } as HTMLInputElement;
+  wrapper.instance().handleChangePassword(mockEvent());
+  expect(changePassword).not.toBeCalled();
+  expect(wrapper.state().errors).toBeDefined();
+});
+
+it('should trigger password change', async () => {
+  const user = mockLoggedInUser();
+  const wrapper = shallowRender({ user });
+  wrapper.instance().handleChangePassword(mockEvent());
+  await waitAndUpdate(wrapper);
+  expect(changePassword).not.toBeCalled();
+
+  wrapper.instance().oldPassword = { value: 'testold' } as HTMLInputElement;
+  wrapper.instance().password = { value: 'test' } as HTMLInputElement;
+  wrapper.instance().passwordConfirmation = { value: 'test' } as HTMLInputElement;
+  wrapper.instance().handleChangePassword(mockEvent());
+  await waitAndUpdate(wrapper);
+
+  expect(changePassword).toBeCalledWith({
+    login: user.login,
+    password: 'test',
+    previousPassword: 'testold'
+  });
+});
+
+it('renders correctly', () => {
+  expect(shallowRender()).toMatchSnapshot();
+});
+
+function shallowRender(props?: Partial<ResetPasswordForm['props']>) {
+  return shallow<ResetPasswordForm>(<ResetPasswordForm user={mockLoggedInUser()} {...props} />);
+}
diff --git a/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/ResetPassword-test.tsx.snap b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/ResetPassword-test.tsx.snap
deleted file mode 100644 (file)
index f2fbe03..0000000
+++ /dev/null
@@ -1,81 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`renders correctly 1`] = `
-<section
-  className="boxed-group"
->
-  <h2
-    className="spacer-bottom"
-  >
-    my_profile.password.title
-  </h2>
-  <form
-    className="boxed-group-inner"
-    onSubmit={[Function]}
-  >
-    <MandatoryFieldsExplanation
-      className="form-field"
-    />
-    <div
-      className="form-field"
-    >
-      <label
-        htmlFor="old_password"
-      >
-        my_profile.password.old
-        <MandatoryFieldMarker />
-      </label>
-      <input
-        autoComplete="off"
-        id="old_password"
-        name="old_password"
-        required={true}
-        type="password"
-      />
-    </div>
-    <div
-      className="form-field"
-    >
-      <label
-        htmlFor="password"
-      >
-        my_profile.password.new
-        <MandatoryFieldMarker />
-      </label>
-      <input
-        autoComplete="off"
-        id="password"
-        name="password"
-        required={true}
-        type="password"
-      />
-    </div>
-    <div
-      className="form-field"
-    >
-      <label
-        htmlFor="password_confirmation"
-      >
-        my_profile.password.confirm
-        <MandatoryFieldMarker />
-      </label>
-      <input
-        autoComplete="off"
-        id="password_confirmation"
-        name="password_confirmation"
-        required={true}
-        type="password"
-      />
-    </div>
-    <div
-      className="form-field"
-    >
-      <SubmitButton
-        id="change-password"
-      >
-        update_verb
-      </SubmitButton>
-    </div>
-  </form>
-</section>
-`;
diff --git a/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/ResetPasswordForm-test.tsx.snap b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/ResetPasswordForm-test.tsx.snap
new file mode 100644 (file)
index 0000000..3f32833
--- /dev/null
@@ -0,0 +1,71 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renders correctly 1`] = `
+<form
+  onSubmit={[Function]}
+>
+  <MandatoryFieldsExplanation
+    className="form-field"
+  />
+  <div
+    className="form-field"
+  >
+    <label
+      htmlFor="old_password"
+    >
+      my_profile.password.old
+      <MandatoryFieldMarker />
+    </label>
+    <input
+      autoComplete="off"
+      id="old_password"
+      name="old_password"
+      required={true}
+      type="password"
+    />
+  </div>
+  <div
+    className="form-field"
+  >
+    <label
+      htmlFor="password"
+    >
+      my_profile.password.new
+      <MandatoryFieldMarker />
+    </label>
+    <input
+      autoComplete="off"
+      id="password"
+      name="password"
+      required={true}
+      type="password"
+    />
+  </div>
+  <div
+    className="form-field"
+  >
+    <label
+      htmlFor="password_confirmation"
+    >
+      my_profile.password.confirm
+      <MandatoryFieldMarker />
+    </label>
+    <input
+      autoComplete="off"
+      id="password_confirmation"
+      name="password_confirmation"
+      required={true}
+      type="password"
+    />
+  </div>
+  <div
+    className="form-field"
+  >
+    <SubmitButton
+      id="change-password"
+    >
+      update_verb
+    </SubmitButton>
+  </div>
+</form>
+`;