]> source.dussan.org Git - sonarqube.git/commitdiff
Fix Quality gate app
authorGrégoire Aubert <gregoire.aubert@sonarsource.com>
Wed, 16 May 2018 14:49:45 +0000 (16:49 +0200)
committerSonarTech <sonartech@sonarsource.com>
Tue, 22 May 2018 18:20:45 +0000 (20:20 +0200)
server/sonar-web/src/main/js/apps/quality-gates/components/CopyQualityGateForm.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/CreateQualityGateForm.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/DetailsHeader.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/ListHeader.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/RenameQualityGateForm.tsx
server/sonar-web/src/main/js/components/controls/ConfirmButton.tsx
server/sonar-web/src/main/js/components/controls/ConfirmModal.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/controls/ModalButton.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/controls/__tests__/ConfirmModal-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/controls/__tests__/ModalButton-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/ConfirmModal-test.tsx.snap [new file with mode: 0644]

index 656dd635ee4bc78a3b8e90642cf43266afbfc183..0d95becd65d3dd164b1ac87ef031f514b78f2816 100644 (file)
 import * as React from 'react';
 import * as PropTypes from 'prop-types';
 import { copyQualityGate } from '../../../api/quality-gates';
-import ConfirmButton from '../../../components/controls/ConfirmButton';
-import { Button } from '../../../components/ui/buttons';
+import ConfirmModal from '../../../components/controls/ConfirmModal';
 import { translate } from '../../../helpers/l10n';
 import { getQualityGateUrl } from '../../../helpers/urls';
 import { QualityGate } from '../../../app/types';
 
 interface Props {
+  onClose: () => void;
   onCopy: () => Promise<void>;
   organization?: string;
   qualityGate: QualityGate;
@@ -50,7 +50,7 @@ export default class CopyQualityGateForm extends React.PureComponent<Props, Stat
     this.setState({ name: event.currentTarget.value });
   };
 
-  onCopy = () => {
+  handleCopy = () => {
     const { qualityGate, organization } = this.props;
     const { name } = this.state;
 
@@ -70,35 +70,29 @@ export default class CopyQualityGateForm extends React.PureComponent<Props, Stat
     const confirmDisable = !name || (qualityGate && qualityGate.name === name);
 
     return (
-      <ConfirmButton
+      <ConfirmModal
         confirmButtonText={translate('copy')}
         confirmDisable={confirmDisable}
-        modalBody={
-          <div className="modal-field">
-            <label htmlFor="quality-gate-form-name">
-              {translate('name')}
-              <em className="mandatory">*</em>
-            </label>
-            <input
-              autoFocus={true}
-              id="quality-gate-form-name"
-              maxLength={100}
-              onChange={this.handleNameChange}
-              required={true}
-              size={50}
-              type="text"
-              value={name}
-            />
-          </div>
-        }
-        modalHeader={translate('quality_gates.copy')}
-        onConfirm={this.onCopy}>
-        {({ onClick }) => (
-          <Button className="little-spacer-left" id="quality-gate-copy" onClick={onClick}>
-            {translate('copy')}
-          </Button>
-        )}
-      </ConfirmButton>
+        header={translate('quality_gates.copy')}
+        onClose={this.props.onClose}
+        onConfirm={this.handleCopy}>
+        <div className="modal-field">
+          <label htmlFor="quality-gate-form-name">
+            {translate('name')}
+            <em className="mandatory">*</em>
+          </label>
+          <input
+            autoFocus={true}
+            id="quality-gate-form-name"
+            maxLength={100}
+            onChange={this.handleNameChange}
+            required={true}
+            size={50}
+            type="text"
+            value={name}
+          />
+        </div>
+      </ConfirmModal>
     );
   }
 }
index b984d9629419ad96fddaa23a8945b8230b9cb6c6..e9b20ed5a64f72ba5c86144a2331ca14e2905bcd 100644 (file)
 import * as React from 'react';
 import * as PropTypes from 'prop-types';
 import { createQualityGate } from '../../../api/quality-gates';
-import ConfirmButton from '../../../components/controls/ConfirmButton';
-import { Button } from '../../../components/ui/buttons';
+import ConfirmModal from '../../../components/controls/ConfirmModal';
 import { translate } from '../../../helpers/l10n';
 import { getQualityGateUrl } from '../../../helpers/urls';
 
 interface Props {
+  onClose: () => void;
   onCreate: () => Promise<void>;
   organization?: string;
 }
@@ -65,35 +65,29 @@ export default class CreateQualityGateForm extends React.PureComponent<Props, St
   render() {
     const { name } = this.state;
     return (
-      <ConfirmButton
+      <ConfirmModal
         confirmButtonText={translate('save')}
         confirmDisable={!name}
-        modalBody={
-          <div className="modal-field">
-            <label htmlFor="quality-gate-form-name">
-              {translate('name')}
-              <em className="mandatory">*</em>
-            </label>
-            <input
-              autoFocus={true}
-              id="quality-gate-form-name"
-              maxLength={100}
-              onChange={this.handleNameChange}
-              required={true}
-              size={50}
-              type="text"
-              value={name}
-            />
-          </div>
-        }
-        modalHeader={translate('quality_gates.create')}
+        header={translate('quality_gates.create')}
+        onClose={this.props.onClose}
         onConfirm={this.handleCreate}>
-        {({ onClick }) => (
-          <Button id="quality-gate-add" onClick={onClick}>
-            {translate('create')}
-          </Button>
-        )}
-      </ConfirmButton>
+        <div className="modal-field">
+          <label htmlFor="quality-gate-form-name">
+            {translate('name')}
+            <em className="mandatory">*</em>
+          </label>
+          <input
+            autoFocus={true}
+            id="quality-gate-form-name"
+            maxLength={100}
+            onChange={this.handleNameChange}
+            required={true}
+            size={50}
+            type="text"
+            value={name}
+          />
+        </div>
+      </ConfirmModal>
     );
   }
 }
index b87ceed7ad2c07b15d3bcc1a01de561431f1fb42..6f7343e0485a7c6cb092a5ca6e8490977c871f20 100644 (file)
@@ -22,15 +22,16 @@ import BuiltInQualityGateBadge from './BuiltInQualityGateBadge';
 import RenameQualityGateForm from './RenameQualityGateForm';
 import CopyQualityGateForm from './CopyQualityGateForm';
 import DeleteQualityGateForm from './DeleteQualityGateForm';
+import ModalButton from '../../../components/controls/ModalButton';
 import { setQualityGateAsDefault } from '../../../api/quality-gates';
 import { Button } from '../../../components/ui/buttons';
 import { translate } from '../../../helpers/l10n';
 import { QualityGate } from '../../../app/types';
 
 interface Props {
+  onSetDefault: () => void;
   organization?: string;
   qualityGate: QualityGate;
-  onSetDefault: () => void;
   refreshItem: () => Promise<void>;
   refreshList: () => Promise<void>;
 }
@@ -67,18 +68,38 @@ export default class DetailsHeader extends React.PureComponent<Props> {
 
             <div className="pull-right">
               {actions.rename && (
-                <RenameQualityGateForm
-                  onRename={this.handleActionRefresh}
-                  organization={organization}
-                  qualityGate={qualityGate}
-                />
+                <ModalButton
+                  modal={({ onClose }) => (
+                    <RenameQualityGateForm
+                      onClose={onClose}
+                      onRename={this.handleActionRefresh}
+                      organization={organization}
+                      qualityGate={qualityGate}
+                    />
+                  )}>
+                  {({ onClick }) => (
+                    <Button id="quality-gate-rename" onClick={onClick}>
+                      {translate('rename')}
+                    </Button>
+                  )}
+                </ModalButton>
               )}
               {actions.copy && (
-                <CopyQualityGateForm
-                  onCopy={this.handleActionRefresh}
-                  organization={organization}
-                  qualityGate={qualityGate}
-                />
+                <ModalButton
+                  modal={({ onClose }) => (
+                    <CopyQualityGateForm
+                      onClose={onClose}
+                      onCopy={this.handleActionRefresh}
+                      organization={organization}
+                      qualityGate={qualityGate}
+                    />
+                  )}>
+                  {({ onClick }) => (
+                    <Button className="little-spacer-left" id="quality-gate-copy" onClick={onClick}>
+                      {translate('copy')}
+                    </Button>
+                  )}
+                </ModalButton>
               )}
               {actions.setAsDefault && (
                 <Button
@@ -90,7 +111,7 @@ export default class DetailsHeader extends React.PureComponent<Props> {
               )}
               {actions.delete && (
                 <DeleteQualityGateForm
-                  onDelete={this.handleActionRefresh}
+                  onDelete={this.props.refreshList}
                   organization={organization}
                   qualityGate={qualityGate}
                 />
index 7ff64a81d9e276f72c8bb9caadd94aef763bd369..e85af064e43390058aad60fe31b99c64d159f532 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
+import { Button } from '../../../components/ui/buttons';
 import CreateQualityGateForm from '../components/CreateQualityGateForm';
 import DocTooltip from '../../../components/docs/DocTooltip';
+import ModalButton from '../../../components/controls/ModalButton';
 import { translate } from '../../../helpers/l10n';
 
 interface Props {
@@ -33,7 +35,20 @@ export default function ListHeader({ canCreate, refreshQualityGates, organizatio
     <header className="page-header">
       {canCreate && (
         <div className="page-actions">
-          <CreateQualityGateForm onCreate={refreshQualityGates} organization={organization} />
+          <ModalButton
+            modal={({ onClose }) => (
+              <CreateQualityGateForm
+                onClose={onClose}
+                onCreate={refreshQualityGates}
+                organization={organization}
+              />
+            )}>
+            {({ onClick }) => (
+              <Button id="quality-gate-add" onClick={onClick}>
+                {translate('create')}
+              </Button>
+            )}
+          </ModalButton>
         </div>
       )}
 
index 5845c2c7cab3a3cdd5c4c90b8d02d9868b3477e8..852ed3fb40f6dde948a682a0e2331525846876f4 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
-import ConfirmButton from '../../../components/controls/ConfirmButton';
+import ConfirmModal from '../../../components/controls/ConfirmModal';
 import { renameQualityGate } from '../../../api/quality-gates';
-import { Button } from '../../../components/ui/buttons';
 import { translate } from '../../../helpers/l10n';
 import { QualityGate } from '../../../app/types';
 
 interface Props {
+  onClose: () => void;
   onRename: () => Promise<void>;
   organization?: string;
   qualityGate: QualityGate;
@@ -44,7 +44,7 @@ export default class RenameQualityGateForm extends React.PureComponent<Props, St
     this.setState({ name: event.currentTarget.value });
   };
 
-  onRename = () => {
+  handleRename = () => {
     const { qualityGate, organization } = this.props;
     const { name } = this.state;
 
@@ -63,35 +63,29 @@ export default class RenameQualityGateForm extends React.PureComponent<Props, St
     const confirmDisable = !name || (qualityGate && qualityGate.name === name);
 
     return (
-      <ConfirmButton
+      <ConfirmModal
         confirmButtonText={translate('rename')}
         confirmDisable={confirmDisable}
-        modalBody={
-          <div className="modal-field">
-            <label htmlFor="quality-gate-form-name">
-              {translate('name')}
-              <em className="mandatory">*</em>
-            </label>
-            <input
-              autoFocus={true}
-              id="quality-gate-form-name"
-              maxLength={100}
-              onChange={this.handleNameChange}
-              required={true}
-              size={50}
-              type="text"
-              value={name}
-            />
-          </div>
-        }
-        modalHeader={translate('quality_gates.rename')}
-        onConfirm={this.onRename}>
-        {({ onClick }) => (
-          <Button id="quality-gate-rename" onClick={onClick}>
-            {translate('rename')}
-          </Button>
-        )}
-      </ConfirmButton>
+        header={translate('quality_gates.rename')}
+        onClose={this.props.onClose}
+        onConfirm={this.handleRename}>
+        <div className="modal-field">
+          <label htmlFor="quality-gate-form-name">
+            {translate('name')}
+            <em className="mandatory">*</em>
+          </label>
+          <input
+            autoFocus={true}
+            id="quality-gate-form-name"
+            maxLength={100}
+            onChange={this.handleNameChange}
+            required={true}
+            size={50}
+            type="text"
+            value={name}
+          />
+        </div>
+      </ConfirmModal>
     );
   }
 }
index 6d6f558bdcb23ca897e5e694d7e406140eb2893d..f9d609f9698b015d11dfad71e965f985c612c825 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
-import SimpleModal from './SimpleModal';
-import DeferredSpinner from '../common/DeferredSpinner';
-import { translate } from '../../helpers/l10n';
-import { SubmitButton, ResetButtonLink } from '../ui/buttons';
+import ModalButton, { ChildrenProps, ModalProps } from './ModalButton';
+import ConfirmModal from './ConfirmModal';
 
-export interface ChildrenProps {
-  onClick: () => void;
-  onFormSubmit: (event: React.FormEvent<HTMLFormElement>) => void;
-}
+export { ChildrenProps } from './ModalButton';
 
 interface Props {
   children: (props: ChildrenProps) => React.ReactNode;
@@ -44,82 +39,22 @@ interface State {
 }
 
 export default class ConfirmButton extends React.PureComponent<Props, State> {
-  mounted = false;
-  state: State = { modal: false };
-
-  componentDidMount() {
-    this.mounted = true;
-  }
-
-  componentWillUnmount() {
-    this.mounted = false;
-  }
-
-  handleButtonClick = () => {
-    this.setState({ modal: true });
-  };
-
-  handleFormSubmit = (event?: React.FormEvent<HTMLFormElement>) => {
-    if (event) {
-      event.preventDefault();
-    }
-    this.setState({ modal: true });
-  };
-
-  handleSubmit = () => {
-    const result = this.props.onConfirm(this.props.confirmData);
-    if (result) {
-      return result.then(this.handleCloseModal, () => {});
-    } else {
-      this.handleCloseModal();
-      return undefined;
-    }
-  };
-
-  handleCloseModal = () => {
-    if (this.mounted) {
-      this.setState({ modal: false });
-    }
+  renderConfirmModal = ({ onClose }: ModalProps) => {
+    return (
+      <ConfirmModal
+        confirmButtonText={this.props.confirmButtonText}
+        confirmData={this.props.confirmData}
+        confirmDisable={this.props.confirmDisable}
+        header={this.props.modalHeader}
+        isDestructive={this.props.isDestructive}
+        onClose={onClose}
+        onConfirm={this.props.onConfirm}>
+        {this.props.modalBody}
+      </ConfirmModal>
+    );
   };
 
   render() {
-    const { confirmButtonText, confirmDisable, isDestructive, modalBody, modalHeader } = this.props;
-
-    return (
-      <>
-        {this.props.children({
-          onClick: this.handleButtonClick,
-          onFormSubmit: this.handleFormSubmit
-        })}
-        {this.state.modal && (
-          <SimpleModal
-            header={modalHeader}
-            onClose={this.handleCloseModal}
-            onSubmit={this.handleSubmit}>
-            {({ onCloseClick, onFormSubmit, submitting }) => (
-              <form onSubmit={onFormSubmit}>
-                <header className="modal-head">
-                  <h2>{modalHeader}</h2>
-                </header>
-
-                <div className="modal-body">{modalBody}</div>
-
-                <footer className="modal-foot">
-                  <DeferredSpinner className="spacer-right" loading={submitting} />
-                  <SubmitButton
-                    className={isDestructive ? 'button-red' : undefined}
-                    disabled={submitting || confirmDisable}>
-                    {confirmButtonText}
-                  </SubmitButton>
-                  <ResetButtonLink disabled={submitting} onClick={onCloseClick}>
-                    {translate('cancel')}
-                  </ResetButtonLink>
-                </footer>
-              </form>
-            )}
-          </SimpleModal>
-        )}
-      </>
-    );
+    return <ModalButton modal={this.renderConfirmModal}>{this.props.children}</ModalButton>;
   }
 }
diff --git a/server/sonar-web/src/main/js/components/controls/ConfirmModal.tsx b/server/sonar-web/src/main/js/components/controls/ConfirmModal.tsx
new file mode 100644 (file)
index 0000000..631d074
--- /dev/null
@@ -0,0 +1,89 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 SimpleModal, { ChildrenProps } from './SimpleModal';
+import DeferredSpinner from '../common/DeferredSpinner';
+import { translate } from '../../helpers/l10n';
+import { SubmitButton, ResetButtonLink } from '../ui/buttons';
+
+interface Props {
+  children: React.ReactNode;
+  confirmButtonText: string;
+  confirmData?: string;
+  confirmDisable?: boolean;
+  header: string;
+  isDestructive?: boolean;
+  onClose: () => void;
+  onConfirm: (data?: string) => void | Promise<void>;
+}
+
+export default class ConfirmModal extends React.PureComponent<Props> {
+  mounted = false;
+
+  componentDidMount() {
+    this.mounted = true;
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+  }
+
+  handleSubmit = () => {
+    const result = this.props.onConfirm(this.props.confirmData);
+    if (result) {
+      return result.then(this.props.onClose, () => {});
+    } else {
+      this.props.onClose();
+      return undefined;
+    }
+  };
+
+  renderModalContent = ({ onCloseClick, onFormSubmit, submitting }: ChildrenProps) => {
+    const { children, confirmButtonText, confirmDisable, header, isDestructive } = this.props;
+    return (
+      <form onSubmit={onFormSubmit}>
+        <header className="modal-head">
+          <h2>{header}</h2>
+        </header>
+        <div className="modal-body">{children}</div>
+        <footer className="modal-foot">
+          <DeferredSpinner className="spacer-right" loading={submitting} />
+          <SubmitButton
+            className={isDestructive ? 'button-red' : undefined}
+            disabled={submitting || confirmDisable}>
+            {confirmButtonText}
+          </SubmitButton>
+          <ResetButtonLink disabled={submitting} onClick={onCloseClick}>
+            {translate('cancel')}
+          </ResetButtonLink>
+        </footer>
+      </form>
+    );
+  };
+
+  render() {
+    const { header } = this.props;
+    return (
+      <SimpleModal header={header} onClose={this.props.onClose} onSubmit={this.handleSubmit}>
+        {this.renderModalContent}
+      </SimpleModal>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/components/controls/ModalButton.tsx b/server/sonar-web/src/main/js/components/controls/ModalButton.tsx
new file mode 100644 (file)
index 0000000..c153ee1
--- /dev/null
@@ -0,0 +1,80 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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';
+
+export interface ChildrenProps {
+  onClick: () => void;
+  onFormSubmit: (event: React.FormEvent<HTMLFormElement>) => void;
+}
+
+export interface ModalProps {
+  onClose: () => void;
+}
+
+export interface Props {
+  children: (props: ChildrenProps) => React.ReactNode;
+  modal: (props: ModalProps) => React.ReactNode;
+}
+
+interface State {
+  modal: boolean;
+}
+
+export default class ModalButton extends React.PureComponent<Props, State> {
+  mounted = false;
+  state: State = { modal: false };
+
+  componentDidMount() {
+    this.mounted = true;
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+  }
+
+  handleButtonClick = () => {
+    this.setState({ modal: true });
+  };
+
+  handleFormSubmit = (event?: React.FormEvent<HTMLFormElement>) => {
+    if (event) {
+      event.preventDefault();
+    }
+    this.setState({ modal: true });
+  };
+
+  handleCloseModal = () => {
+    if (this.mounted) {
+      this.setState({ modal: false });
+    }
+  };
+
+  render() {
+    return (
+      <>
+        {this.props.children({
+          onClick: this.handleButtonClick,
+          onFormSubmit: this.handleFormSubmit
+        })}
+        {this.state.modal && this.props.modal({ onClose: this.handleCloseModal })}
+      </>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/ConfirmModal-test.tsx b/server/sonar-web/src/main/js/components/controls/__tests__/ConfirmModal-test.tsx
new file mode 100644 (file)
index 0000000..7985898
--- /dev/null
@@ -0,0 +1,60 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 { shallow } from 'enzyme';
+import ConfirmModal from '../ConfirmModal';
+import { submit, waitAndUpdate } from '../../../helpers/testUtils';
+
+it('should render correctly', () => {
+  const wrapper = shallow(
+    <ConfirmModal
+      confirmButtonText="confirm"
+      confirmData="data"
+      header="title"
+      onClose={jest.fn()}
+      onConfirm={jest.fn()}>
+      <p>My confirm message</p>
+    </ConfirmModal>
+  );
+  expect(wrapper).toMatchSnapshot();
+  expect(wrapper.find('SimpleModal').dive()).toMatchSnapshot();
+});
+
+it('should confirm and close after confirm', async () => {
+  const onClose = jest.fn();
+  const onConfirm = jest.fn(() => Promise.resolve());
+  const wrapper = shallow(
+    <ConfirmModal
+      confirmButtonText="confirm"
+      confirmData="data"
+      header="title"
+      onClose={onClose}
+      onConfirm={onConfirm}>
+      <p>My confirm message</p>
+    </ConfirmModal>
+  );
+  const modalContent = wrapper.find('SimpleModal').dive();
+  submit(modalContent.find('form'));
+  expect(onConfirm).toBeCalledWith('data');
+  expect(modalContent.find('footer')).toMatchSnapshot();
+
+  await waitAndUpdate(wrapper);
+  expect(onClose).toHaveBeenCalled();
+});
diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/ModalButton-test.tsx b/server/sonar-web/src/main/js/components/controls/__tests__/ModalButton-test.tsx
new file mode 100644 (file)
index 0000000..9b0376c
--- /dev/null
@@ -0,0 +1,38 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 { shallow } from 'enzyme';
+import ModalButton from '../ModalButton';
+import { click } from '../../../helpers/testUtils';
+
+it('should open/close modal', () => {
+  const wrapper = shallow(
+    <ModalButton modal={({ onClose }) => <button id="js-close" onClick={onClose} type="button" />}>
+      {({ onClick }) => <button id="js-open" onClick={onClick} type="button" />}
+    </ModalButton>
+  );
+
+  expect(wrapper.find('#js-open').exists()).toBeTruthy();
+  expect(wrapper.find('#js-close').exists()).toBeFalsy();
+  click(wrapper.find('#js-open'));
+  expect(wrapper.find('#js-close').exists()).toBeTruthy();
+  click(wrapper.find('#js-close'));
+  expect(wrapper.find('#js-close').exists()).toBeFalsy();
+});
diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/ConfirmModal-test.tsx.snap b/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/ConfirmModal-test.tsx.snap
new file mode 100644 (file)
index 0000000..800e634
--- /dev/null
@@ -0,0 +1,76 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should confirm and close after confirm 1`] = `
+<footer
+  className="modal-foot"
+>
+  <DeferredSpinner
+    className="spacer-right"
+    loading={true}
+    timeout={100}
+  />
+  <SubmitButton
+    disabled={true}
+  >
+    confirm
+  </SubmitButton>
+  <ResetButtonLink
+    disabled={true}
+    onClick={[Function]}
+  >
+    cancel
+  </ResetButtonLink>
+</footer>
+`;
+
+exports[`should render correctly 1`] = `
+<SimpleModal
+  header="title"
+  onClose={[MockFunction]}
+  onSubmit={[Function]}
+/>
+`;
+
+exports[`should render correctly 2`] = `
+<Modal
+  contentLabel="title"
+  onRequestClose={[MockFunction]}
+>
+  <form
+    onSubmit={[Function]}
+  >
+    <header
+      className="modal-head"
+    >
+      <h2>
+        title
+      </h2>
+    </header>
+    <div
+      className="modal-body"
+    >
+      <p>
+        My confirm message
+      </p>
+    </div>
+    <footer
+      className="modal-foot"
+    >
+      <DeferredSpinner
+        className="spacer-right"
+        loading={false}
+        timeout={100}
+      />
+      <SubmitButton>
+        confirm
+      </SubmitButton>
+      <ResetButtonLink
+        disabled={false}
+        onClick={[Function]}
+      >
+        cancel
+      </ResetButtonLink>
+    </footer>
+  </form>
+</Modal>
+`;