Repository: mlaursen/react-md
Pull request #80 (2016-05-11 18:53:26)

Enhancement/dialog tests
This should solve #42 and the Dialog docgen for #20 


Patch:
diff --git a/src/js/Dialogs/Dialog.js b/src/js/Dialogs/Dialog.js
index 3ebaa8b6f4..752f514075 100644
--- a/src/js/Dialogs/Dialog.js
+++ b/src/js/Dialogs/Dialog.js
@@ -1,12 +1,17 @@
 import React, { Component, PropTypes } from 'react';
 import PureRenderMixin from 'react-addons-pure-render-mixin';
 import classnames from 'classnames';
-import { FlatButton } from '../Buttons';
-import Toolbar from '../Toolbars';
-import Divider from '../Dividers';
+import Toolbar from '../Toolbars/Toolbar';
+import Divider from '../Dividers/Divider';
+
+import DialogFooter from './DialogFooter';
 
 const DIALOG_PADDING = 8;
 
+/**
+ * This is the Dialog that appears when the `DialogContainer` component has
+ * a true value for `isOpen`.
+ */
 export default class Dialog extends Component {
   constructor(props) {
     super(props);
@@ -16,27 +21,79 @@ export default class Dialog extends Component {
   }
 
   static propTypes = {
+    /**
+     * Boolean if it is a simple dialog.
+     */
     isSimple: PropTypes.bool.isRequired,
+
+    /**
+     * Boolean if it is a full page dialog.
+     */
     isFullPage: PropTypes.bool.isRequired,
+
+    /**
+     * A transform-origin string to use for a full page dialog. This will
+     * allow the dialog to appear from that origin.
+     */
     transformOrigin: PropTypes.string,
+
+    /**
+     * An optional title to display above the content or in the `Toolbar` of a `Dialog`.
+     */
     title: PropTypes.string,
+
+    /**
+     * A single action or a list of actions to display in the Dialog.
+     * This can either be a list of `FlatButton` props or `FlatButton` elements.
+     */
     actions: PropTypes.oneOfType([
-      PropTypes.arrayOf(PropTypes.node),
-      PropTypes.arrayOf(PropTypes.object),
-      PropTypes.node,
+      PropTypes.element,
       PropTypes.object,
+      PropTypes.arrayOf(PropTypes.oneOfType([
+        PropTypes.element,
+        PropTypes.object,
+      ])),
     ]),
+
+    /**
+     * Any action to display to the left of the title in a full page dialog's toolbar.
+     * See the [Toolbar's actionLeft](/components/toolbars) documentation for more information.
+     */
     actionLeft: PropTypes.node,
+
+    /**
+     * Any action to display to the right of the title in a full page dialog's toolbar.
+     * See the [Toolbar actionsRight](/components/toolbars) documentation for more information.
+     */
     actionRight: PropTypes.node,
+
+    /**
+     * An optional style to apply to the `Dialog`.
+     */
+    style: PropTypes.object,
+
+    /**
+     * An optional className to apply to the `Dialog`.
+     */
     className: PropTypes.string,
+
+    /**
+     * An optional style to apply to the dialog's content.
+     */
+    contentStyle: PropTypes.object,
+
+    /**
+     * An optional className to apply to the dialog's content.
+     */
     contentClassName: PropTypes.string,
+
+    /**
+     * Any children to display in the `Dialog`.
+     */
     children: PropTypes.node,
-    style: PropTypes.object,
-    onlyChildren: PropTypes.bool,
   };
 
   componentDidMount() {
-    if(this.props.onlyChildren) { return; }
     let state = {};
     const { dialog, content } = this.refs;
 
@@ -63,6 +120,7 @@ export default class Dialog extends Component {
       title,
       children,
       className,
+      contentStyle,
       contentClassName,
       actions,
       actionLeft,
@@ -71,13 +129,12 @@ export default class Dialog extends Component {
       transformOrigin,
       isSimple,
       isFullPage,
-      onlyChildren,
       ...props,
     } = this.props;
     const { stacked, divided } = this.state;
 
     let header, footer;
-    if(!onlyChildren && !isFullPage && title) {
+    if(!isFullPage && title) {
       header = <h1 className="md-title">{title}</h1>;
     } else if(isFullPage) {
       header = (
@@ -90,18 +147,8 @@ export default class Dialog extends Component {
       );
     }
 
-    if(actions) {
-      footer = (
-        <footer className={classnames('md-dialog-footer', { stacked })}>
-          {actions.map((action, key) => {
-            if(!React.isValidElement(action)) {
-              return <FlatButton key={key} {...action} />;
-            } else {
-              return action;
-            }
-          })}
-        </footer>
-      );
+    if(actions && actions.length) {
+      footer = <DialogFooter className={classnames({ stacked })} actions={actions} />;
     }
 
     return (
@@ -113,20 +160,18 @@ export default class Dialog extends Component {
         })}
         style={Object.assign({}, style, { transformOrigin })}
         {...props}
-        >
+      >
         {header}
         {header && divided && <Divider />}
-        {onlyChildren && children}
-        {!onlyChildren &&
-          <section
-            ref="content"
-            className={classnames('md-dialog-content', contentClassName, {
-              'simple': isSimple,
-            })}
-            >
-            {children}
-          </section>
-        }
+        <section
+          ref="content"
+          style={contentStyle}
+          className={classnames('md-dialog-content', contentClassName, {
+            'simple': isSimple,
+          })}
+        >
+          {children}
+        </section>
         {footer && divided && <Divider />}
         {footer}
       </div>
diff --git a/src/js/Dialogs/DialogContainer.js b/src/js/Dialogs/DialogContainer.js
index d6d7a6fd7e..f3442906c6 100644
--- a/src/js/Dialogs/DialogContainer.js
+++ b/src/js/Dialogs/DialogContainer.js
@@ -5,7 +5,12 @@ import classnames from 'classnames';
 
 import { setOverflow } from '../utils';
 import Dialog from './Dialog';
+import Overlay from '../Transitions/Overlay';
 
+/**
+ * This component renders a `Dialog` when the `isOpen` prop is set to true.
+ * It will manage the css transitions between the open and closed states.
+ */
 export default class DialogContainer extends Component {
   constructor(props) {
     super(props);
@@ -15,33 +20,131 @@ export default class DialogContainer extends Component {
   }
 
   static propTypes = {
+    /**
+     * Boolean if the Dialog is currently open.
+     */
     isOpen: PropTypes.bool.isRequired,
+
+    /**
+     * A function to call that will close the dialog. This function will
+     * be called when the overlay is clicked on simple dialogs.
+     */
     close: PropTypes.func.isRequired,
+
+    /**
+     * Any children to display in the Dialog.
+     */
     children: PropTypes.node,
+
+    /**
+     * An optional title to display in the dialog.
+     */
     title: PropTypes.string,
+
+    /**
+     * A single action or a list of actions to display in the Dialog.
+     * This can either be a list of `FlatButton` props or `FlatButton` elements.
+     */
     actions: PropTypes.oneOfType([
-      PropTypes.arrayOf(PropTypes.node),
-      PropTypes.arrayOf(PropTypes.object),
-      PropTypes.node,
+      PropTypes.element,
       PropTypes.object,
+      PropTypes.arrayOf(PropTypes.oneOfType([
+        PropTypes.element,
+        PropTypes.object,
+      ])),
     ]),
+
+    /**
+     * Any action to display to the left of the title in a full page dialog's toolbar.
+     * See the [Toolbar's actionLeft](/components/toolbars) documentation for more information.
+     */
     actionLeft: PropTypes.node,
+
+    /**
+     * Any action to display to the right of the title in a full page dialog's toolbar.
+     * See the [Toolbar actionsRight](/components/toolbars) documentation for more information.
+     */
     actionRight: PropTypes.node,
+
+    /**
+     * An optional className to apply to the `Dialog` container.
+     */
     className: PropTypes.string,
+
+    /**
+     * An optional className to apply to the `Dialog` itself.
+     */
+    dialogClassName: PropTypes.string,
+
+    /**
+     * An optional className to apply to the `Dialog`'s content section.
+     */
     contentClassName: PropTypes.string,
-    containerClassName: PropTypes.string,
+
+    /**
+     * Boolean if the `Dialog` should behave as a modal. This means that one
+     * of the actions must be selected to close the dialog. The overlay
+     * can not be clicked to be closed.
+     */
     modal: PropTypes.bool,
+
+    /**
+     * An optional style to apply to the `Dialog` container.
+     */
     style: PropTypes.object,
+
+    /**
+     * An optional style to apply to the `Dialog` itself.
+     */
+    dialogStyle: PropTypes.object,
+
+    /**
+     * An optional style to apply to the `Dialog`'s content section.
+     */
+    contentStyle: PropTypes.object,
+
+    /**
+     * An optional click/touch event's pageX location to use when
+     * rendering a full page dialog. This will make the `Dialog` appear relative
+     * to the click origin.
+     */
     pageX: PropTypes.number,
+
+    /**
+     * An optional click/touch event's pageY location to use when
+     * rendering a full page dialog. This will make the `Dialog` appear relative
+     * to the click origin.
+     */
     pageY: PropTypes.number,
+
+    /**
+     * The transition name to use for the `Dialog` when appearing/disappearing
+     */
     transitionName: PropTypes.string.isRequired,
+
+    /**
+     * Boolean if the enter transition should be used.
+     */
     transitionEnter: PropTypes.bool,
+
+    /**
+     * The timeout for the the enter transition.
+     */
     transitionEnterTimeout: PropTypes.number,
+
+    /**
+     * Boolean if the leave transition should be used.
+     */
     transitionLeave: PropTypes.bool,
+
+    /**
+     * The timeout for the leave transition.
+     */
     transitionLeaveTimeout: PropTypes.number,
   };
 
   static defaultProps = {
+    modal: false,
     transitionName: 'md-dialog',
     transitionEnter: true,
     transitionEnterTimeout: 300,
@@ -71,6 +174,12 @@ export default class DialogContainer extends Component {
     return `${pageX - window.scrollX}px ${pageY - window.scrollY}px`;
   };
 
+  /**
+   * The only purpose of this function is to be used when closing the dialog.
+   * The visibility gets changed to hidden and the z-index turns to -1. So
+   * this function will wait for the dialog to finish animating before removing
+   * the open className
+   */
   delayIsOpen = (time) => {
     const timeout = setTimeout(() => {
       this.setState({ openClassName: false, timeout: null });
@@ -91,12 +200,14 @@ export default class DialogContainer extends Component {
       children,
       className,
       contentClassName,
-      containerClassName,
+      dialogClassName,
       modal,
       close,
       actionLeft,
       actionRight,
       style,
+      dialogStyle,
+      contentStyle,
       transitionName,
       transitionEnter,
       transitionEnterTimeout,
@@ -104,7 +215,7 @@ export default class DialogContainer extends Component {
       transitionLeaveTimeout,
       ...props,
     } = this.props;
-    const isSimple = !actions;
+    const isSimple = !actions || !actions.length;
 
     const isFullPage = !!actionLeft || !!actionRight;
     return (
@@ -114,42 +225,32 @@ export default class DialogContainer extends Component {
         transitionEnterTimeout={transitionEnterTimeout}
         transisionLeave={transitionLeave}
         transitionLeaveTimeout={transitionLeaveTimeout}
-        className={classnames('md-dialog-container', containerClassName, {
+        className={classnames('md-dialog-container', className, {
           'open': isOpen || this.state.openClassName,
           'simple': isSimple,
           'dialog-centered': !isFullPage,
         })}
-        >
+        style={style}
+      >
         {isOpen &&
           <Dialog
             key="dialog"
             title={title}
             children={children}
-            className={className}
+            className={dialogClassName}
             contentClassName={contentClassName}
             actions={actions}
             actionLeft={actionLeft}
             actionRight={actionRight}
-            style={style}
+            style={dialogStyle}
+            contentStyle={contentStyle}
             transformOrigin={this.state.transformOrigin}
             isSimple={isSimple}
             isFullPage={isFullPage}
             {...props}
           />
         }
-        <CSSTransitionGroup
-          transitionName="md-overlay"
-          transitionEnterTimeout={150}
-          transitionLeaveTimeout={transitionLeaveTimeout}
-          >
-          {isOpen &&
-            <div
-              key="overlay"
-              className="md-overlay"
-              onClick={modal ? null : close}
-            />
-          }
-        </CSSTransitionGroup>
+        <Overlay isOpen={isOpen} onClick={modal ? null : close} />
       </CSSTransitionGroup>
     );
   }
diff --git a/src/js/Dialogs/DialogFooter.js b/src/js/Dialogs/DialogFooter.js
new file mode 100644
index 0000000000..abb75fd8af
--- /dev/null
+++ b/src/js/Dialogs/DialogFooter.js
@@ -0,0 +1,67 @@
+import React, { Component, PropTypes } from 'react';
+import PureRenderMixin from 'react-addons-pure-render-mixin';
+import classnames from 'classnames';
+
+import FlatButton from '../Buttons/FlatButton';
+
+/**
+ * A simple component for converting action objects into `FlatButton`
+ * or just rendering the actions that are valid elements.
+ */
+export default class DialogFooter extends Component {
+  constructor(props) {
+    super(props);
+
+    this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
+  }
+
+  static propTypes = {
+    /**
+     * The optional className to apply.
+     */
+    className: PropTypes.string,
+
+    /**
+     * The list of actions or a single action to display in the footer.
+     */
+    actions: PropTypes.oneOfType([
+      PropTypes.shape({
+        onClick: PropTypes.func.isRequired,
+        label: PropTypes.string.isRequired,
+      }),
+      PropTypes.element,
+      PropTypes.arrayOf(PropTypes.oneOfType([
+        PropTypes.element,
+        PropTypes.shape({
+          onClick: PropTypes.func.isRequired,
+          label: PropTypes.string.isRequired,
+        }),
+      ])),
+    ]).isRequired,
+  };
+
+  actionToElement = (action, key) => {
+    if(!React.isValidElement(action)) {
+      return <FlatButton key={key} {...action} />;
+    } else {
+      return action;
+    }
+  };
+
+  render() {
+    const { className, actions } = this.props;
+
+    let children;
+    if(Array.isArray(actions)) {
+      children = actions.map(this.actionToElement);
+    } else {
+      children = this.actionToElement(actions);
+    }
+
+    return (
+      <footer className={classnames('md-dialog-footer', className)}>
+        {children}
+      </footer>
+    );
+  }
+}
diff --git a/src/js/NavigationDrawers/NavigationDrawer.js b/src/js/NavigationDrawers/NavigationDrawer.js
index 9bdb94cccc..b115d02059 100644
--- a/src/js/NavigationDrawers/NavigationDrawer.js
+++ b/src/js/NavigationDrawers/NavigationDrawer.js
@@ -1,12 +1,12 @@
 import React, { Component, PropTypes } from 'react';
 import PureRenderMixin from 'react-addons-pure-render-mixin';
-import CSSTransitionGroup from 'react-addons-css-transition-group';
 import classnames from 'classnames';
 
 import { setOverflow } from '../utils';
 import Divider from '../Dividers';
 import { List, ListItem } from '../Lists';
 import Subheader from '../Subheaders';
+import Overlay from '../Transitions/Overlay';
 
 import NavigationDrawerHeader from './NavigationDrawerHeader';
 import NavigationDrawerToolbar from './NavigationDrawerToolbar';
@@ -203,9 +203,7 @@ export default class NavigationDrawer extends Component {
             children={toolbarChildren}
           />
           {children}
-          <CSSTransitionGroup transitionName="md-overlay" transitionEnterTimeout={150} transitionLeaveTimeout={150}>
-            {isOpen && <div key="overlay" className="md-navigation-drawer-overlay" onClick={closeDrawer} />}
-          </CSSTransitionGroup>
+          <Overlay isOpen={isOpen} onClick={closeDrawer} />
         </div>
       </div>
     );
diff --git a/src/js/Transitions/Overlay.js b/src/js/Transitions/Overlay.js
new file mode 100644
index 0000000000..5eaa566527
--- /dev/null
+++ b/src/js/Transitions/Overlay.js
@@ -0,0 +1,120 @@
+import React, { Component, PropTypes } from 'react';
+import PureRenderMixin from 'react-addons-pure-render-mixin';
+import CSSTransitionGroup from 'react-addons-css-transition-group';
+import classnames from 'classnames';
+
+/**
+ * This is a controlled component for rendering an overlay on the page.
+ *
+ * Any event listeners will be applied to overlay instead of the `CSSTransitionGroup`
+ * containing the overlay when open.
+ */
+export default class Overlay extends Component {
+  constructor(props) {
+    super(props);
+
+    this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
+  }
+
+  static propTypes = {
+    /**
+     * Boolean if the Overlay is currently open.
+     */
+    isOpen: PropTypes.bool.isRequired,
+
+    /**
+     * An optional function to call when the overlay is clicked.
+     */
+    onClick: PropTypes.func,
+
+    /**
+     * The transition name for the overlay.
+     */
+    transitionName: PropTypes.string.isRequired,
+
+    /**
+     * The enter timeout for the overlay when it has been toggled on.
+     */
+    transitionEnterTimeout: PropTypes.number.isRequired,
+
+    /**
+     * The leave timeout for the overlay when it has been toggled off.
+     */
+    transitionLeaveTimeout: PropTypes.number.isRequired,
+
+    /**
+     * An optional style to apply to the `CSSTransitionGroup` containing the overlay.
+     */
+    style: PropTypes.object,
+
+    /**
+     * An optional className to apply to the `CSSTransitionGroup` holding the overlay.
+     */
+    className: PropTypes.string,
+
+    /**
+     * The component to render the `CSSTransitionGroup` as.
+     */
+    component: PropTypes.oneOfType([
+      PropTypes.string,
+      PropTypes.func,
+    ]).isRequired,
+
+    /**
+     * An optional style to apply to the overlay itself.
+     */
+    overlayStyle: PropTypes.object,
+
+    /**
+     * An optional className to apply to the overlay itself.
+     */
+    overlayClassName: PropTypes.string,
+  };
+
+  static defaultProps = {
+    component: 'span',
+    transitionName: 'md-overlay',
+    transitionEnterTimeout: 150,
+    transitionLeaveTimeout: 150,
+  };
+
+  render() {
+    const {
+      isOpen,
+      component,
+      style,
+      className,
+      overlayStyle,
+      overlayClassName,
+      transitionName,
+      transitionEnterTimeout,
+      transitionLeaveTimeout,
+      ...props,
+    } = this.props;
+
+    let overlay;
+    if(isOpen) {
+      const overlayProps = {
+        key: 'overlay',
+        style: overlayStyle,
+        className: classnames('md-overlay', overlayClassName),
+        ...props,
+      };
+
+      overlay = <div {...overlayProps} />;
+    }
+
+    return (
+      <CSSTransitionGroup
+        component={component}
+        style={style}
+        className={className}
+        transitionName={transitionName}
+        transitionEnterTimeout={transitionEnterTimeout}
+        transitionLeaveTimeout={transitionLeaveTimeout}
+      >
+        {overlay}
+      </CSSTransitionGroup>
+    );
+  }
+}
diff --git a/src/scss/components/_navigation-drawers.scss b/src/scss/components/_navigation-drawers.scss
index 2349f7f5c0..db1c7c9680 100644
--- a/src/scss/components/_navigation-drawers.scss
+++ b/src/scss/components/_navigation-drawers.scss
@@ -66,7 +66,7 @@
   transition-duration: $md-transition-time;
   transition-property: box-shadow, left, width;
   width: 100%;
-  z-index: $md-navigation-drawer-z-index;
+  z-index: $md-navigation-drawer-toolbar-z-index;
 
   .md-title {
     color: inherit;
@@ -90,8 +90,3 @@
 .md-navigation-drawer-toggle {
   float: right;
 }
-
-.md-navigation-drawer-overlay {
-  // scss-lint:disable PlaceholderInExtend
-  @extend .md-overlay;
-}
diff --git a/src/scss/helpers/_variables.scss b/src/scss/helpers/_variables.scss
index 6de639dbbe..3be7511258 100644
--- a/src/scss/helpers/_variables.scss
+++ b/src/scss/helpers/_variables.scss
@@ -114,7 +114,7 @@ $md-transition-time: .15s !default;
 ///
 /// @type Number
 /// @group z-indexes, overlay
-$md-overlay-z-index: 3 !default;
+$md-overlay-z-index: 100 !default;
 
 /// The default z-index for the Data table's edit dialog.
 ///
@@ -160,7 +160,7 @@ $md-fab-z-index: 8 !default;
 ///
 /// @type Number
 /// @group z-indexes, navigation-drawers
-$md-navigation-drawer-z-index: 10 !default;
+$md-navigation-drawer-z-index: $md-overlay-z-index + 1 !default;
 
 /// The default z-index for the snackbar component.
 ///
@@ -172,14 +172,20 @@ $md-snackbar-z-index: 11 !default;
 ///
 /// @type Number
 /// @group z-indexes, navigation-drawers
-$md-navigation-drawer-temporary-z-index: 12 !default;
+$md-navigation-drawer-temporary-z-index: $md-overlay-z-index + 1 !default;
+
+/// The default z-index for the temporary navigation-drawer
+///
+/// @type Number
+/// @group z-indexes, navigation-drawers
+$md-navigation-drawer-toolbar-z-index: $md-overlay-z-index - 1 !default;
 
 
 /// The default z-index for the dialog component.
 ///
 /// @type Number
 /// @group z-indexes, dialogs
-$md-dialog-z-index: 26 !default;
+$md-dialog-z-index: $md-overlay-z-index + 5 !default;
 
 
 /// The most common used height throughout the material design scss.
diff --git a/src/scss/helpers/mixins/_navigation-drawers.scss b/src/scss/helpers/mixins/_navigation-drawers.scss
index 4b445fc22b..6515c163d6 100644
--- a/src/scss/helpers/mixins/_navigation-drawers.scss
+++ b/src/scss/helpers/mixins/_navigation-drawers.scss
@@ -38,10 +38,6 @@
       z-index: $md-navigation-drawer-temporary-z-index;
     }
   }
-
-  .md-navigation-drawer-content .md-navigation-drawer-overlay {
-    z-index: $md-navigation-drawer-z-index + 1;
-  }
 }
 
 @mixin md-navigation-drawer-desktop() {
@@ -79,11 +75,6 @@
         padding-left: $md-navigation-drawer-desktop-width;
         width: 100%;
       }
-
-      .md-navigation-drawer-overlay {
-        display: none;
-        visibility: hidden;
-      }
     }
   }
 }


Test patch:
diff --git a/src/js/Dialogs/__tests__/Dialog.js b/src/js/Dialogs/__tests__/Dialog.js
new file mode 100644
index 0000000000..e6d096575b
--- /dev/null
+++ b/src/js/Dialogs/__tests__/Dialog.js
@@ -0,0 +1,123 @@
+/*eslint-env jest*/
+jest.unmock('../Dialog');
+
+import React from 'react';
+import { findDOMNode } from 'react-dom';
+import {
+  renderIntoDocument,
+  scryRenderedComponentsWithType,
+  findRenderedDOMComponentWithClass,
+  scryRenderedDOMComponentsWithClass,
+} from 'react-addons-test-utils';
+
+import Dialog from '../Dialog';
+import DialogFooter from '../DialogFooter';
+import Toolbar from '../../Toolbars/Toolbar';
+
+describe('Dialog', () => {
+  it('merges className and style', () => {
+    const style = { display: 'block' };
+    const className = 'test';
+    const contentStyle = { background: 'black' };
+    const contentClassName = 'content';
+    const dialog = renderIntoDocument(
+      <Dialog
+        isSimple={true}
+        isFullPage={false}
+        style={style}
+        className={className}
+        contentStyle={contentStyle}
+        contentClassName={contentClassName}
+      />
+    );
+
+    const dialogNode = findDOMNode(dialog);
+    expect(dialogNode.style.display).toBe(style.display);
+    expect(dialogNode.classList.contains(className)).toBe(true);
+
+    const contentNode = findRenderedDOMComponentWithClass(dialog, 'md-dialog-content');
+    expect(contentNode.style.background).toBe(contentStyle.background);
+    expect(contentNode.classList.contains(contentClassName)).toBe(true);
+  });
+
+  it('displays any children in the dialog content', () => {
+    const content = 'Hello, World!';
+    const dialog = renderIntoDocument(
+      <Dialog
+        isSimple={true}
+        isFullPage={false}
+      >
+        {content}
+      </Dialog>
+    );
+
+    const contentNode = findRenderedDOMComponentWithClass(dialog, 'md-dialog-content');
+    expect(contentNode.textContent).toBe(content);
+  });
+
+  it('renders a title in the header for a simple dialog', () => {
+    const title = 'Test';
+    const dialog = renderIntoDocument(
+      <Dialog
+        isSimple={true}
+        isFullPage={false}
+        title={title}
+      />
+    );
+
+    const titleNodes = scryRenderedDOMComponentsWithClass(dialog, 'md-title');
+    expect(titleNodes.length).toBe(1);
+
+    const [titleNode] = titleNodes;
+    expect(titleNode.textContent).toBe(title);
+  });
+
+  it('renders a toolbar in the dialog header for a full page dialog', () => {
+    const dialog = renderIntoDocument(
+      <Dialog
+        isSimple={false}
+        isFullPage={true}
+        title="Test"
+        actionLeft="Left"
+        actionRight="Right"
+      />
+    );
+
+    const toolbars = scryRenderedComponentsWithType(dialog, Toolbar);
+    expect(toolbars.length).toBe(1);
+  });
+
+  it('renders a dialog footer when there are actions', () => {
+    let dialog = renderIntoDocument(
+      <Dialog
+        isSimple={true}
+        isFullPage={false}
+      />
+    );
+
+    let footerNodes = scryRenderedComponentsWithType(dialog, DialogFooter);
+    expect(footerNodes.length).toBe(0);
+
+    dialog = renderIntoDocument(
+      <Dialog
+        isSimple={true}
+        isFullPage={false}
+        actions={[]}
+      />
+    );
+
+    footerNodes = scryRenderedComponentsWithType(dialog, DialogFooter);
+    expect(footerNodes.length).toBe(0);
+
+    dialog = renderIntoDocument(
+      <Dialog
+        isSimple={true}
+        isFullPage={false}
+        actions={[{ onClick: jest.genMockFunction() }]}
+      />
+    );
+
+    footerNodes = scryRenderedComponentsWithType(dialog, DialogFooter);
+    expect(footerNodes.length).toBe(1);
+  });
+});
diff --git a/src/js/Dialogs/__tests__/DialogContainer.js b/src/js/Dialogs/__tests__/DialogContainer.js
new file mode 100644
index 0000000000..a6c1183e09
--- /dev/null
+++ b/src/js/Dialogs/__tests__/DialogContainer.js
@@ -0,0 +1,74 @@
+/*eslint-env jest*/
+jest.unmock('../DialogContainer');
+
+import React from 'react';
+import { findDOMNode } from 'react-dom';
+import {
+  renderIntoDocument,
+  scryRenderedComponentsWithType,
+} from 'react-addons-test-utils';
+
+import DialogContainer from '../DialogContainer';
+import Dialog from '../Dialog';
+
+describe('DialogContainer', () => {
+  it('merges className and style', () => {
+    const style = { display: 'block' };
+    const className = 'test';
+    const close = jest.genMockFunction();
+    const dialogContainer = renderIntoDocument(
+      <DialogContainer style={style} className={className} isOpen={false} close={close} />
+    );
+
+    const dialogContainerNode = findDOMNode(dialogContainer);
+    expect(dialogContainerNode.style.display).toBe(style.display);
+    expect(dialogContainerNode.classList.contains(className)).toBe(true);
+  });
+
+  it('renders a dialog when the isOpen prop is true', () => {
+    const close = jest.genMockFunction();
+    let dialog = renderIntoDocument(<DialogContainer isOpen={false} close={close} />);
+
+    let dialogNode = scryRenderedComponentsWithType(dialog, Dialog);
+    expect(dialogNode.length).toBe(0);
+
+    dialog = renderIntoDocument(<DialogContainer isOpen={true} close={close} />);
+    dialogNode = scryRenderedComponentsWithType(dialog, Dialog);
+    expect(dialogNode.length).toBe(1);
+  });
+
+  it('renders a simple dialog if there are no actions', () => {
+    const close = jest.genMockFunction();
+    let dialog = renderIntoDocument(
+      <DialogContainer
+        isOpen={false}
+        close={close}
+      />
+    );
+
+    let dialogNode = findDOMNode(dialog);
+    expect(dialogNode.classList.contains('simple')).toBe(true);
+
+    dialog = renderIntoDocument(
+      <DialogContainer
+        isOpen={false}
+        close={close}
+        actions={[]}
+      />
+    );
+
+    dialogNode = findDOMNode(dialog);
+    expect(dialogNode.classList.contains('simple')).toBe(true);
+
+    dialog = renderIntoDocument(
+      <DialogContainer
+        isOpen={false}
+        close={close}
+        actions={[{ onClick: close }]}
+      />
+    );
+
+    dialogNode = findDOMNode(dialog);
+    expect(dialogNode.classList.contains('simple')).toBe(false);
+  });
+});
diff --git a/src/js/Dialogs/__tests__/DialogFooter.js b/src/js/Dialogs/__tests__/DialogFooter.js
new file mode 100644
index 0000000000..3480fe93a4
--- /dev/null
+++ b/src/js/Dialogs/__tests__/DialogFooter.js
@@ -0,0 +1,87 @@
+/*eslint-env jest*/
+jest.unmock('../DialogFooter');
+
+import React from 'react';
+import {
+  renderIntoDocument,
+  scryRenderedComponentsWithType,
+  scryRenderedDOMComponentsWithTag,
+} from 'react-addons-test-utils';
+
+import DialogFooter from '../DialogFooter';
+import FlatButton from '../../Buttons/FlatButton';
+
+describe('DialogFooter', () => {
+  it('renders an actions object prop as a flat button', () => {
+    const actions = {
+      onClick: jest.genMockFunction(),
+      label: 'Test',
+    };
+
+    const footer = renderIntoDocument(
+      <DialogFooter actions={actions} />
+    );
+
+    const buttons = scryRenderedComponentsWithType(footer, FlatButton);
+    expect(buttons.length).toBe(1);
+  });
+
+  it('renders an actions element in the footer', () => {
+    const actions = <button>Hello</button>;
+    const footer = renderIntoDocument(
+      <DialogFooter actions={actions} />
+    );
+
+    const buttons = scryRenderedDOMComponentsWithTag(footer, 'button');
+    expect(buttons.length).toBe(1);
+  });
+
+  it('renders a list of action objects as flat buttons', () => {
+    const actions = [{
+      onClick: jest.genMockFunction(),
+      label: 'Test 1',
+    }, {
+      onClick: jest.genMockFunction(),
+      label: 'Test 2',
+    }];
+
+    const footer = renderIntoDocument(
+      <DialogFooter actions={actions} />
+    );
+
+    const buttons = scryRenderedComponentsWithType(footer, FlatButton);
+    expect(buttons.length).toBe(2);
+  });
+
+  it('renders a list of elements in the footer', () => {
+    const actions = [
+      <button key={0}>Test 1</button>,
+      <button key={1}>Test 2</button>,
+    ];
+
+    const footer = renderIntoDocument(
+      <DialogFooter actions={actions} />
+    );
+
+    const buttons = scryRenderedDOMComponentsWithTag(footer, 'button');
+    expect(buttons.length).toBe(2);
+  });
+
+  it('renders a mixed list of action objects and valid elements', () => {
+    const actions = [{
+      onClick: jest.genMockFunction(),
+      label: 'Test 1',
+    },
+      <button key={2}>Test 2</button>,
+    ];
+
+    const footer = renderIntoDocument(
+      <DialogFooter actions={actions} />
+    );
+
+    const flats = scryRenderedComponentsWithType(footer, FlatButton);
+    const buttons = scryRenderedDOMComponentsWithTag(footer, 'button');
+    expect(flats.length).toBe(1);
+    expect(buttons.length).toBe(1);
+  });
+});
diff --git a/src/js/Transitions/__tests__/Overlay.js b/src/js/Transitions/__tests__/Overlay.js
new file mode 100644
index 0000000000..dcd81124fd
--- /dev/null
+++ b/src/js/Transitions/__tests__/Overlay.js
@@ -0,0 +1,63 @@
+/*eslint-env jest*/
+jest.unmock('../Overlay');
+
+import React from 'react';
+import { findDOMNode } from 'react-dom';
+import {
+  Simulate,
+  renderIntoDocument,
+  findRenderedDOMComponentWithClass,
+  scryRenderedDOMComponentsWithClass,
+} from 'react-addons-test-utils';
+
+import Overlay from '../Overlay';
+
+describe('Overlay', () => {
+  it('merges className and style', () => {
+    const style = { display: 'block' };
+    const className = 'test';
+    const overlayStyle = { background: 'black' };
+    const overlayClassName = 'what-what';
+    const overlay = renderIntoDocument(
+      <Overlay
+        isOpen={true}
+        style={style}
+        className={className}
+        overlayStyle={overlayStyle}
+        overlayClassName={overlayClassName}
+      />
+    );
+
+    const overlayContainerNode = findDOMNode(overlay);
+    expect(overlayContainerNode.style.display).toBe(style.display);
+    expect(overlayContainerNode.classList.contains(className)).toBe(true);
+
+    const overlayNode = findRenderedDOMComponentWithClass(overlay, 'md-overlay');
+    expect(overlayNode.style.background).toBe(overlayStyle.background);
+    expect(overlayNode.classList.contains(overlayClassName)).toBe(true);
+  });
+
+  it('toggles the overlay when the isOpen prop is changed', () => {
+    let overlay = renderIntoDocument(<Overlay isOpen={false} />);
+
+    let overlayNodes = scryRenderedDOMComponentsWithClass(overlay, 'md-overlay');
+    expect(overlayNodes.length).toBe(0);
+
+    overlay = renderIntoDocument(<Overlay isOpen={true} />);
+    overlayNodes = scryRenderedDOMComponentsWithClass(overlay, 'md-overlay');
+
+    expect(overlayNodes.length).toBe(1);
+  });
+
+  it('passes event listeners to the overlay instead of the CSSTransitionGroup', () => {
+    const onClick = jest.genMockFunction();
+    const overlay = renderIntoDocument(
+      <Overlay isOpen={true} onClick={onClick} />
+    );
+
+    const overlayNode = findRenderedDOMComponentWithClass(overlay, 'md-overlay');
+    Simulate.click(overlayNode);
+
+    expect(onClick.mock.calls.length).toBe(1);
+  });
+});