Repository: mlaursen/react-md
Pull request #85 (2016-05-13 13:51:36)

Enhancement/snackbar tests
This should solve #58 


Patch:
diff --git a/src/js/Snackbars/Snackbar.js b/src/js/Snackbars/Snackbar.js
index 91591ae83b..0dbf2195c0 100644
--- a/src/js/Snackbars/Snackbar.js
+++ b/src/js/Snackbars/Snackbar.js
@@ -1,21 +1,51 @@
 import React, { Component, PropTypes } from 'react';
+import ReactDOM from 'react-dom';
 import PureRenderMixin from 'react-addons-pure-render-mixin';
 import CSSTransitionGroup from 'react-addons-css-transition-group';
+import classnames from 'classnames';
 
 import Toast from './Toast';
 
+/**
+ * A snackbar takes a queue of toasts and displays them to the user one after another.
+ * They can be auto dismissed, or require user interaction to close the toast.
+ */
 export default class Snackbar extends Component {
   constructor(props) {
     super(props);
 
     this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
+    this.state = { multiline: false };
   }
 
   static propTypes = {
+    /**
+     * An optional className to apply to the active toast.
+     */
     className: PropTypes.string,
+
+    /**
+     * The moving queue of toasts to display.
+     *
+     * ##### Shape Description
+     */
     toasts: PropTypes.arrayOf(PropTypes.shape({
+      /**
+       * The text to display in the toast.
+       */
       text: PropTypes.string.isRequired,
+
+      /**
+       * An optional key for the toast.
+       */
       key: PropTypes.any,
+
+      /**
+       * An optional action to take. If this value is a string, a `FlatButton`
+       * will be created with a `label` of this value and the `onClick` function
+       * will be the `dismiss` function. If this is an object, all values will be
+       * applied to the `FlatButton`.
+       */
       action: PropTypes.oneOfType([
         PropTypes.string,
         PropTypes.shape({
@@ -23,44 +53,94 @@ export default class Snackbar extends Component {
           label: PropTypes.string.isRequired,
         }),
       ]),
+
+      /**
+       * An optional function to call when the toast appears.
+       */
       onAppear: PropTypes.func,
     })).isRequired,
+
+    /**
+     * Boolean if the toast should automatically hide after the given timeout.
+     */
     autohide: PropTypes.bool,
+
+    /**
+     * The auto hide timeout.
+     */
     autohideTimeout: PropTypes.number,
+
+    /**
+     * A function to call that will dismiss the active toast.
+     */
     dismiss: PropTypes.func.isRequired,
-    multiline: PropTypes.bool,
-    fabTimeout: PropTypes.number,
+
+    /**
+     * The transition name to use for the snackbar.
+     */
+    transitionName: PropTypes.string.isRequired,
+
+    /**
+     * The transition enter timeout for the snackbar.
+     */
+    transitionEnterTimeout: PropTypes.number.isRequired,
+
+    /**
+     * The transition leave timeout for the snackbar.
+     */
+    transitionLeaveTimeout: PropTypes.number.isRequired,
+
+    /**
+     * An optional Floating Action Button element that should be moved
+     * when a toast appears/disappears.
+     */
+    fab: PropTypes.object,
   };
 
   static defaultProps = {
     autohide: true,
     autohideTimeout: 3000,
-    fabTimeout: 450,
+    transitionName: 'snackbar',
+    transitionEnterTimeout: 450,
+    transitionLeaveTimeout: 450,
     toasts: [],
   };
 
-  componentWillReceiveProps({ toasts, dismiss, autohide, multiline, autohideTimeout, fabTimeout }) {
-    if(this.props.toasts.length === toasts.length) { return; }
+  componentWillReceiveProps({ toasts, dismiss, autohide, autohideTimeout, fab, transitionEnterTimeout }) {
+    if(fab && !toasts.length) {
+      fab.classList.remove('snackbar-multiline-adjust');
+      fab.classList.remove('snackbar-adjust');
+    }
+
+    if(this.props.toasts.length === toasts.length || !toasts.length || toasts[0] === this.props.toasts[0]) {
+      return;
+    }
+    // If dismiss was called in chained toasts
+    this.toastTimeout && clearTimeout(this.toastTimeout);
+
     const [toast] = toasts;
-    toast && toast.onAppear && toast.onAppear();
-
-    const fixedFAB = document.querySelector('.md-floating-btn.fixed');
-    if(fixedFAB) {
-      fixedFAB.classList.remove('snackbar-multiline-adjust');
-      fixedFAB.classList.remove('snackbar-adjust');
-      if(toast) {
-        this.fabTimeout = setTimeout(() => {
-          fixedFAB.classList.add(`snackbar${multiline ? '-multiline' : ''}-adjust`);
-        }, this.props.toasts.length > 1 ? fabTimeout : 0);
-      }
+    toast.onAppear && toast.onAppear();
+
+    const state = this.initializeNextToast(toast);
+    state.chained = this.props.toasts.length > 1;
+
+    if(autohide) {
+      this.toastTimeout = setTimeout(() => {
+        dismiss();
+        this.toastTimeout = null;
+      }, autohideTimeout);
     }
 
-    if(!toast || !autohide || this.toastTimeout) { return; }
+    if(fab) {
+      fab.classList.remove('snackbar-multiline-adjust');
+      fab.classList.remove('snackbar-adjust');
+
+      this.fabTimeout = setTimeout(() => {
+        fab.classList.add(`snackbar${this.state.multiline ? '-multiline' : ''}-adjust`);
+      }, state.chained ? transitionEnterTimeout : 0);
+    }
 
-    this.toastTimeout = setTimeout(() => {
-      this.toastTimeout = null;
-      dismiss();
-    }, autohideTimeout);
+    this.setState(state);
   }
 
   componentWillUnmount() {
@@ -72,19 +152,60 @@ export default class Snackbar extends Component {
     return typeof action === 'string' ? { label: action, onClick: this.props.dismiss } : action;
   };
 
+  initializeNextToast = (toast) => {
+    const p = document.createElement('p');
+    p.innerHTML = toast.text;
+
+
+    const snackbar = document.createElement('section');
+    snackbar.className = classnames('md-snackbar', this.props.className);
+
+    snackbar.appendChild(p);
+    if(toast.action) {
+      const btn = document.createElement('button');
+      btn.className = 'md-btn md-flat-btn';
+      btn.innerHTML = typeof toast.action === 'string' ? toast.action : toast.action.label;
+
+      snackbar.appendChild(btn);
+    }
+
+    const node = ReactDOM.findDOMNode(this);
+    node.appendChild(snackbar);
+
+    const lineHeight = this.state.lineHeight || parseInt(window.getComputedStyle(p).getPropertyValue('line-height'));
+    const multiline = p.offsetHeight > lineHeight;
+    node.removeChild(snackbar);
+
+    return {
+      multiline,
+      lineHeight,
+      key: toast.key || Date.now(),
+    };
+  };
+
   render() {
-    const { className, toasts, dismiss, multiline, ...props } = this.props;
+    const { multiline, chained, key } = this.state;
+    const {
+      className,
+      toasts,
+      dismiss,
+      transitionName,
+      transitionEnterTimeout,
+      transitionLeaveTimeout,
+      ...props,
+    } = this.props;
+
     const [toast] = toasts;
     return (
       <CSSTransitionGroup
         className="md-snackbar-container"
-        transitionName="snackbar"
-        transitionEnterTimeout={1200}
-        transitionLeaveTimeout={450}
-        >
+        transitionName={transitionName}
+        transitionEnterTimeout={transitionEnterTimeout * (chained ? 2 : 1)}
+        transitionLeaveTimeout={transitionLeaveTimeout}
+      >
         {toast &&
           <Toast
-            key={toast.key || Date.now()}
+            key={key}
             className={className}
             toast={toast}
             dismiss={dismiss}
diff --git a/src/js/Snackbars/Toast.js b/src/js/Snackbars/Toast.js
index 16101d5700..03d0ad578b 100644
--- a/src/js/Snackbars/Toast.js
+++ b/src/js/Snackbars/Toast.js
@@ -2,7 +2,7 @@ import React, { Component, PropTypes } from 'react';
 import PureRenderMixin from 'react-addons-pure-render-mixin';
 import classnames from 'classnames';
 
-import { FlatButton } from '../Buttons';
+import FlatButton from '../Buttons/FlatButton';
 
 export default class Toast extends Component {
   constructor(props) {
@@ -44,7 +44,7 @@ export default class Toast extends Component {
           'multiline': multiline,
         })}
         {...props}
-        >
+      >
         <p>{text}</p>
         {action && <FlatButton {...btnProps} />}
       </section>
diff --git a/src/js/Tooltips/__mocks__/index.js b/src/js/Tooltips/__mocks__/index.js
new file mode 100644
index 0000000000..4c39db5a0f
--- /dev/null
+++ b/src/js/Tooltips/__mocks__/index.js
@@ -0,0 +1,2 @@
+/*eslint-env jest*/
+export default jest.genMockFunction().mockImplementation(ComposedComponent => ComposedComponent);
diff --git a/src/scss/components/_snackbars.scss b/src/scss/components/_snackbars.scss
index 3acf65f8d6..e2ae7774be 100644
--- a/src/scss/components/_snackbars.scss
+++ b/src/scss/components/_snackbars.scss
@@ -21,19 +21,18 @@
   .md-flat-btn {
     min-width: initial;
     overflow: visible;
-    padding: 0;
   }
 }
 
 .snackbar-enter-active:not(:only-child),
 .snackbar-enter-active:not(:only-child) * {
-  transition-delay: $md-transition-time * 3;
+  transition-delay: $md-snackbar-chained-transition-delay;
 }
 
 .snackbar-enter-active,
 .snackbar-leave-active {
   transform-origin: 0 100%;
-  transition: transform $md-transition-time * 2;
+  transition: transform $md-snackbar-transition-time;
 }
 
 .snackbar-enter {
@@ -44,7 +43,7 @@
   &.snackbar-enter-active {
     * {
       opacity: 1;
-      transition: opacity $md-transition-time * 3 ease;
+      transition: opacity $md-snackbar-opacity-transition-time ease;
     }
   }
 }
diff --git a/src/scss/helpers/_variables.scss b/src/scss/helpers/_variables.scss
index adbc9ab55f..feb1f28342 100644
--- a/src/scss/helpers/_variables.scss
+++ b/src/scss/helpers/_variables.scss
@@ -854,6 +854,24 @@ $md-snackbar-desktop-max-width: 568px;
 /// @group snackbar
 $md-snackbar-desktop-border-radius: 2px;
 
+/// The transition time for the snackbar appearing.
+///
+/// @type Time
+/// @group snackbar
+$md-snackbar-transition-time: $md-transition-time * 2 !default;
+
+/// The transition time for the snackbar's children appearing.
+///
+/// @type Time
+/// @group snackbar
+$md-snackbar-opacity-transition-time: $md-transition-time * 3 !default;
+
+/// The transition delay for chained toasts.
+///
+/// @type Time
+/// @group snackbar
+$md-snackbar-chained-transition-delay: $md-transition-time * 3 !default;
+
 
 
 


Test patch:
diff --git a/src/js/Buttons/__tests__/FlatButton.js b/src/js/Buttons/__tests__/FlatButton.js
index 33d0e63316..22a15be0a3 100644
--- a/src/js/Buttons/__tests__/FlatButton.js
+++ b/src/js/Buttons/__tests__/FlatButton.js
@@ -1,6 +1,4 @@
 /*eslint-env jest*/
-jest.unmock('../../Inks');
-jest.unmock('../../Inks/injectInk');
 jest.unmock('../Button');
 jest.unmock('../FlatButton');
 jest.unmock('../../FontIcons');
diff --git a/src/js/Buttons/__tests__/FloatingButton.js b/src/js/Buttons/__tests__/FloatingButton.js
index fff13ea47c..d4f2ed2deb 100644
--- a/src/js/Buttons/__tests__/FloatingButton.js
+++ b/src/js/Buttons/__tests__/FloatingButton.js
@@ -1,6 +1,4 @@
 /*eslint-env jest*/
-jest.unmock('../../Inks');
-jest.unmock('../../Inks/injectInk');
 jest.unmock('../../Tooltips');
 jest.unmock('../../Tooltips/injectTooltip');
 jest.unmock('../IconButton');
diff --git a/src/js/Buttons/__tests__/IconButton.js b/src/js/Buttons/__tests__/IconButton.js
index c4065dbb38..33b7b95606 100644
--- a/src/js/Buttons/__tests__/IconButton.js
+++ b/src/js/Buttons/__tests__/IconButton.js
@@ -1,6 +1,4 @@
 /*eslint-env jest*/
-jest.unmock('../../Inks');
-jest.unmock('../../Inks/injectInk');
 jest.unmock('../../Tooltips');
 jest.unmock('../../Tooltips/injectTooltip');
 jest.unmock('../IconButton');
diff --git a/src/js/Buttons/__tests__/RaisedButton.js b/src/js/Buttons/__tests__/RaisedButton.js
index 4cc94a78d4..af0a0b851c 100644
--- a/src/js/Buttons/__tests__/RaisedButton.js
+++ b/src/js/Buttons/__tests__/RaisedButton.js
@@ -1,6 +1,4 @@
 /*eslint-env jest*/
-jest.unmock('../../Inks');
-jest.unmock('../../Inks/injectInk');
 jest.unmock('../Button');
 jest.unmock('../RaisedButton');
 jest.unmock('../../FontIcons');
diff --git a/src/js/Cards/__tests__/Card.js b/src/js/Cards/__tests__/Card.js
index f7d8c28dea..25e260cfc8 100644
--- a/src/js/Cards/__tests__/Card.js
+++ b/src/js/Cards/__tests__/Card.js
@@ -10,10 +10,6 @@ jest.unmock('../../Buttons');
 jest.unmock('../../Buttons/IconButton');
 jest.unmock('../../FontIcons');
 jest.unmock('../../FontIcons/FontIcon');
-jest.unmock('../../Inks');
-jest.unmock('../../Inks/injectInk');
-jest.unmock('../../Tooltips');
-jest.unmock('../../Tooltips/injectTooltip');
 
 import React from 'react';
 import { findDOMNode } from 'react-dom';
diff --git a/src/js/Cards/__tests__/CardExpander.js b/src/js/Cards/__tests__/CardExpander.js
index 73a15f093b..9b0550f225 100644
--- a/src/js/Cards/__tests__/CardExpander.js
+++ b/src/js/Cards/__tests__/CardExpander.js
@@ -6,10 +6,6 @@ jest.unmock('../../Buttons');
 jest.unmock('../../Buttons/IconButton');
 jest.unmock('../../FontIcons');
 jest.unmock('../../FontIcons/FontIcon');
-jest.unmock('../../Inks');
-jest.unmock('../../Inks/injectInk');
-jest.unmock('../../Tooltips');
-jest.unmock('../../Tooltips/injectTooltip');
 
 import React from 'react';
 import {
diff --git a/src/js/DataTables/__tests__/EditDialogColumn.js b/src/js/DataTables/__tests__/EditDialogColumn.js
index c1c7779c6d..fe9ed2958d 100644
--- a/src/js/DataTables/__tests__/EditDialogColumn.js
+++ b/src/js/DataTables/__tests__/EditDialogColumn.js
@@ -5,8 +5,6 @@ jest.unmock('../TableColumn');
 jest.unmock('../DataTable');
 jest.unmock('../TableBody');
 jest.unmock('../TableRow');
-jest.unmock('../../Tooltips');
-jest.unmock('../../Tooltips/injectTooltip');
 jest.unmock('../../TextFields');
 jest.unmock('../../TextFields/TextField');
 jest.unmock('../../TextFields/TextDivider');
diff --git a/src/js/DataTables/__tests__/TableColumn.js b/src/js/DataTables/__tests__/TableColumn.js
index fddfe38231..3ad7de6b82 100644
--- a/src/js/DataTables/__tests__/TableColumn.js
+++ b/src/js/DataTables/__tests__/TableColumn.js
@@ -4,8 +4,6 @@ jest.unmock('../DataTable');
 jest.unmock('../TableBody');
 jest.unmock('../TableRow');
 jest.unmock('../TableColumn');
-jest.unmock('../../Tooltips');
-jest.unmock('../../Tooltips/injectTooltip');
 jest.unmock('../../FontIcons');
 jest.unmock('../../FontIcons/FontIcon');
 
diff --git a/src/js/DataTables/__tests__/TableRow.js b/src/js/DataTables/__tests__/TableRow.js
index 9adac18a07..edce2fd4a6 100644
--- a/src/js/DataTables/__tests__/TableRow.js
+++ b/src/js/DataTables/__tests__/TableRow.js
@@ -3,8 +3,6 @@ jest.unmock('../DataTable');
 jest.unmock('../TableBody');
 jest.unmock('../TableRow');
 jest.unmock('../TableColumn');
-jest.unmock('../../Tooltips');
-jest.unmock('../../Tooltips/injectTooltip');
 jest.unmock('../../FontIcons');
 jest.unmock('../../FontIcons/FontIcon');
 
diff --git a/src/js/Lists/__tests__/List.js b/src/js/Lists/__tests__/List.js
index b8112e99d6..e0594af9d1 100644
--- a/src/js/Lists/__tests__/List.js
+++ b/src/js/Lists/__tests__/List.js
@@ -5,8 +5,6 @@ jest.unmock('../ListTile');
 jest.unmock('../ListItemText');
 jest.unmock('../../Dividers');
 jest.unmock('../../Dividers/Divider');
-jest.unmock('../../Inks');
-jest.unmock('../../Inks/injectInk');
 
 import React from 'react';
 import { findDOMNode } from 'react-dom';
diff --git a/src/js/Lists/__tests__/ListItem.js b/src/js/Lists/__tests__/ListItem.js
index 218b00559b..3bdf0c6bf5 100644
--- a/src/js/Lists/__tests__/ListItem.js
+++ b/src/js/Lists/__tests__/ListItem.js
@@ -3,12 +3,8 @@ jest.unmock('../List');
 jest.unmock('../ListItem');
 jest.unmock('../ListTile');
 jest.unmock('../ListItemText');
-jest.unmock('../../Inks');
-jest.unmock('../../Inks/injectInk');
 jest.unmock('../../Buttons');
 jest.unmock('../../Buttons/IconButton');
-jest.unmock('../../Tooltips');
-jest.unmock('../../Tooltips/injectTooltip');
 jest.unmock('../../constants/keyCodes');
 jest.unmock('../../Transitions');
 jest.unmock('../../Transitions/Height');
diff --git a/src/js/Lists/__tests__/ListItemControl.js b/src/js/Lists/__tests__/ListItemControl.js
index 2d3b859893..fcc7810331 100644
--- a/src/js/Lists/__tests__/ListItemControl.js
+++ b/src/js/Lists/__tests__/ListItemControl.js
@@ -5,8 +5,6 @@ jest.unmock('../../SelectionControls/Checkbox');
 jest.unmock('../../SelectionControls/ControlContainer');
 jest.unmock('../../SelectionControls/InkedControl');
 jest.unmock('../../SelectionControls/Switch');
-jest.unmock('../../Inks');
-jest.unmock('../../Inks/injectInk');
 jest.unmock('../../constants/keyCodes');
 
 import React from 'react';
diff --git a/src/js/Menus/__tests__/Menu.js b/src/js/Menus/__tests__/Menu.js
index c2e8b7d1e5..3715a0ffbf 100644
--- a/src/js/Menus/__tests__/Menu.js
+++ b/src/js/Menus/__tests__/Menu.js
@@ -5,8 +5,6 @@ jest.unmock('../../Lists/List.js');
 jest.unmock('../../Lists/ListTile');
 jest.unmock('../../Lists/ListItemText');
 jest.unmock('../../Lists/ListItem');
-jest.unmock('../../Inks');
-jest.unmock('../../Inks/injectInk');
 
 import React from 'react';
 import { findDOMNode } from 'react-dom';
diff --git a/src/js/SelectFields/__tests__/SelectField.js b/src/js/SelectFields/__tests__/SelectField.js
index 9556260968..87934cea61 100644
--- a/src/js/SelectFields/__tests__/SelectField.js
+++ b/src/js/SelectFields/__tests__/SelectField.js
@@ -7,8 +7,6 @@ jest.unmock('../../TextFields');
 jest.unmock('../../TextFields/TextField');
 jest.unmock('../../TextFields/TextDivider');
 jest.unmock('../../TextFields/FloatingLabel');
-jest.unmock('../../Inks');
-jest.unmock('../../Inks/injectInk');
 jest.unmock('../../Lists');
 jest.unmock('../../Lists/List');
 jest.unmock('../../Lists/ListItem');
diff --git a/src/js/Snackbars/__tests__/Snackbar.js b/src/js/Snackbars/__tests__/Snackbar.js
new file mode 100644
index 0000000000..d0d7d0d8ea
--- /dev/null
+++ b/src/js/Snackbars/__tests__/Snackbar.js
@@ -0,0 +1,62 @@
+/*eslint-env jest*/
+jest.unmock('../Snackbar');
+
+import React from 'react';
+import {
+  renderIntoDocument,
+  scryRenderedComponentsWithType,
+} from 'react-addons-test-utils';
+
+import Snackbar from '../Snackbar';
+import Toast from '../Toast';
+
+describe('Snackbar', () => {
+  it('renders a toast', () => {
+    let toasts = [];
+    const dismiss = jest.genMockFunction();
+
+    let snackbar = renderIntoDocument(
+      <Snackbar toasts={toasts} dismiss={dismiss} />
+    );
+
+    let toastComps = scryRenderedComponentsWithType(snackbar, Toast);
+    expect(toastComps.length).toBe(0);
+
+    toasts = [{ text: 'Hello' }];
+    snackbar = renderIntoDocument(
+      <Snackbar toasts={toasts} dismiss={dismiss} />
+    );
+
+    toastComps = scryRenderedComponentsWithType(snackbar, Toast);
+    expect(toastComps.length).toBe(1);
+  });
+
+  it('automatically dismisses a toast after the autohide timeout', () => {
+    const dismiss = jest.genMockFunction();
+
+    class Test extends React.Component {
+      constructor(props) {
+        super(props);
+
+        this.state = { toasts: [] };
+      }
+
+      render() {
+        return <Snackbar toasts={this.state.toasts} dismiss={dismiss} />;
+      }
+    }
+    let test = renderIntoDocument(<Test />);
+
+    let toastComps = scryRenderedComponentsWithType(test, Toast);
+    expect(toastComps.length).toBe(0);
+
+    test.setState({ toasts: [{ text: 'Hello' }] });
+
+    toastComps = scryRenderedComponentsWithType(test, Toast);
+    expect(toastComps.length).toBe(1);
+    expect(dismiss.mock.calls.length).toBe(0);
+
+    jest.runAllTimers();
+    expect(dismiss.mock.calls.length).toBe(1);
+  });
+});
diff --git a/src/js/Snackbars/__tests__/Toast.js b/src/js/Snackbars/__tests__/Toast.js
new file mode 100644
index 0000000000..e99c9388fa
--- /dev/null
+++ b/src/js/Snackbars/__tests__/Toast.js
@@ -0,0 +1,60 @@
+/*eslint-env jest*/
+jest.unmock('../Toast');
+jest.unmock('../../Buttons/Button');
+jest.unmock('../../Buttons/FlatButton');
+
+import React from 'react';
+import { findDOMNode } from 'react-dom';
+import {
+  Simulate,
+  renderIntoDocument,
+  findRenderedComponentWithType,
+} from 'react-addons-test-utils';
+
+import Toast from '../Toast';
+import FlatButton from '../../Buttons/FlatButton';
+
+describe('Toast', () => {
+  it('merges className and style', () => {
+    const style = { display: 'block' };
+    const className = 'test';
+    const t = { text: 'Toast', action: 'Ok' };
+    const dismiss = jest.genMockFunction();
+    const toast = renderIntoDocument(
+      <Toast style={style} className={className} toast={t} dismiss={dismiss} />
+    );
+
+    const toastNode = findDOMNode(toast);
+    expect(toastNode.style.display).toBe(style.display);
+    expect(toastNode.classList.contains(className)).toBe(true);
+  });
+
+  it('renders the text in the toast', () => {
+    const dismiss = jest.genMockFunction();
+    const toast = renderIntoDocument(
+      <Toast toast={{ text: 'Toast' }} dismiss={dismiss} />
+    );
+
+    const toastNode = findDOMNode(toast);
+    expect(toastNode.textContent).toBe('Toast');
+  });
+
+  it('automatically dismisses a toast that has an action of a string', () => {
+    const dismiss = jest.genMockFunction();
+    const t = {
+      text: 'Toast',
+      action: 'Ok',
+    };
+
+    const toast = renderIntoDocument(
+      <Toast toast={t} dismiss={dismiss} />
+    );
+
+    const btn = findRenderedComponentWithType(toast, FlatButton);
+    expect(btn.props.onClick).toBe(dismiss);
+    expect(btn.props.label).toBe('Ok');
+
+    Simulate.click(findDOMNode(btn));
+    expect(dismiss.mock.calls.length).toBe(1);
+  });
+});