Repository: mlaursen/react-md
Pull request #82 (2016-05-12 13:58:35)

Enhancement/progress tests
This should solve #51 


Patch:
diff --git a/src/js/Progress/CircularProgress.js b/src/js/Progress/CircularProgress.js
index f66f7ced7f..e1783a3d1c 100644
--- a/src/js/Progress/CircularProgress.js
+++ b/src/js/Progress/CircularProgress.js
@@ -1,10 +1,23 @@
 import React, { Component, PropTypes } from 'react';
 import PureRenderMixin from 'react-addons-pure-render-mixin';
 import classnames from 'classnames';
+import { isBetween } from '../utils';
 
 const ROATE_DISTANCE = 360 * 1.75;
 const BASE_SIZE = 24; // font-icon font size
 
+/**
+ * The `CircularProgress` component is used to give visual feedback while your app
+ * is loading.
+ *
+ * There are two different types of circular progress bars: `Determinate` and `Indeterminate`.
+ *
+ * A `Determinate` circular progress bar should be used when you want to keep track of the current
+ * progress. An example would be downloading a file.
+ *
+ * An `Indeterminate` circular progress bar should be used when you can not keep track of the progress
+ * yourself. An example would be waiting for some API call to complete.
+ */
 export default class CircularProgress extends Component {
   constructor(props) {
     super(props);
@@ -13,36 +26,92 @@ export default class CircularProgress extends Component {
   }
 
   static propTypes = {
+    /**
+     * A style object to apply to the svg. If this is a determinate `CircularProgress`,
+     * the `transform` (and vendor prefixes) styles will be merged with the current
+     * progress rotation.
+     */
+    style: PropTypes.object.isRequired,
+
+    /**
+     * An optional className to apply to the svg.
+     */
     className: PropTypes.string,
-    value: PropTypes.number,
-    scale: PropTypes.number,
+
+    /**
+     * The current value of the progress. If this value is defined, it will
+     * be converted to a determinate circular progress. The progress will not
+     * advance unless this value changes.
+     *
+     * This value should also be a number between 0 and 100.
+     */
+    value: (props, propName, component) => {
+      if(typeof props[propName] === 'undefined') { return; }
+      let err = PropTypes.number(props, propName, component);
+      if(!err) {
+        const value = props[propName];
+        if(!isBetween(value, 0, 100)) {
+          err = new Error(`A determinate '${component}' was given a value '${value}'. The 'value' prop should be between 0 and 100`);
+        }
+      }
+
+      return err;
+    },
+
+    /**
+     * The scale for the circular progress.
+     */
+    scale: PropTypes.number.isRequired,
+
+    /**
+     * You probably don't want to update this. I'm not good at svg. This should
+     * match the scss variable `$md-circular-progress-stroke-dashoffset`.
+     */
     determinateDashoffset: PropTypes.number.isRequired,
+
+    /**
+     * Boolean if the the progress should be centered in it's container.
+     */
     centered: PropTypes.bool,
   };
 
   static defaultProps = {
+    style: {},
     scale: 1,
     determinateDashoffset: 187,
     centered: true,
   };
 
   render() {
-    const { scale, className, value, determinateDashoffset, centered } = this.props;
+    const {
+      scale,
+      style,
+      className,
+      value,
+      determinateDashoffset,
+      centered,
+      ...props,
+    } = this.props;
+
     const isDeterminate = typeof value === 'number';
-    let circleStyle, svgStyle;
+    let circleStyle, svgStyle = style;
     if(isDeterminate) {
       const rotate = `rotate(${ROATE_DISTANCE / 100 * value}deg)`;
       circleStyle = {
         strokeDashoffset: determinateDashoffset - (determinateDashoffset / 100 * value),
       };
-      svgStyle = {
-        WebkitTransform: rotate,
-        MozTransform: rotate,
-        transform: rotate,
-      };
+
+      svgStyle = Object.assign({}, style, {
+        WebkitTransform: classnames(style.WebkitTransform, rotate),
+        MozTransform: classnames(style.MozTransform, rotate),
+        transform: classnames(style.transform, rotate),
+      });
     }
+
     return (
       <svg
+        {...props}
+        style={svgStyle}
         className={classnames('md-circular-progress', className, {
           'determinate': isDeterminate,
           'indeterminate': !isDeterminate,
@@ -52,8 +121,7 @@ export default class CircularProgress extends Component {
         height={scale * BASE_SIZE}
         viewBox="0 0 66 66"
         xmlns="http://www.w3.org/2000/svg"
-        style={svgStyle}
-        >
+      >
         <circle
           className="md-circular-progress-path"
           strokeWidth="6"
diff --git a/src/js/Progress/LinearProgress.js b/src/js/Progress/LinearProgress.js
index 3baba05b20..ab7f176ac3 100644
--- a/src/js/Progress/LinearProgress.js
+++ b/src/js/Progress/LinearProgress.js
@@ -2,8 +2,26 @@ import React, { Component, PropTypes } from 'react';
 import PureRenderMixin from 'react-addons-pure-render-mixin';
 import classnames from 'classnames';
 
-import { numberBetween } from '../utils';
+import { isBetween } from '../utils';
 
+/**
+ * There are 3 different types of linear progress bars: `Determinate`,
+ * `Indeterminate`, and `Query Indeterminate`.
+ *
+ * A `Determinate` linear progress bar should be used when you can keep track of the
+ * progress and have a percentage complete you can work with. An example would be
+ * uploading/downloading a file.
+ *
+ * An `Indeterminate` linear progress bar should be used when you can not keep track
+ * of the progess yourself. An example might be waiting for an API call to complete.
+ *
+ * A `Query Indeterminate` linear progress bar is used when you are combining
+ * `Indeterminate` and `Determinate`. A Linear Progress component can be displayed
+ * as a query indeterminate progress bar by adding the prop `query={true}` to the
+ * component. Until a progress value is given, it will display the query linear
+ * progress animation. Afterwards, it will start the determinate animation of where
+ * you manually keep updating the value of the progress.
+ */
 export default class LinearProgress extends Component {
   constructor(props) {
     super(props);
@@ -12,8 +30,34 @@ export default class LinearProgress extends Component {
   }
 
   static propTypes = {
+    /**
+     * An optional className to apply to the linear progress container.
+     */
     className: PropTypes.string,
-    value: PropTypes.number,
+
+    /**
+     * The current value of the progress. If this value is defined, it will
+     * be converted to a determinate circular progress. The progress will not
+     * advance unless this value changes.
+     *
+     * This value should also be a number between 0 and 100.
+     */
+    value: (props, propName, component) => {
+      if(typeof props[propName] === 'undefined') { return; }
+      let err = PropTypes.number(props, propName, component);
+      if(!err) {
+        const value = props[propName];
+        if(!isBetween(value, 0, 100)) {
+          err = new Error(`A determinate '${component}' was given a value '${value}'. The 'value' prop should be between 0 and 100`);
+        }
+      }
+
+      return err;
+    },
+
+    /**
+     * Boolean if this should be a query indeterminate progress bar.
+     */
     query: PropTypes.bool,
   };
 
@@ -27,18 +71,18 @@ export default class LinearProgress extends Component {
 
     let style;
     if(isDeterminate) {
-      style = { width: `${numberBetween(value, 0, 100)}%` };
+      style = { width: `${value}%` };
     }
 
     return (
       <div className={classnames('md-linear-progress-container', className)} {...props}>
         <div
+          style={style}
           className={classnames('md-linear-progress', {
             query,
             'determinate': isDeterminate,
             'indeterminate': !isDeterminate,
           })}
-          style={style}
         />
       </div>
     );
diff --git a/src/js/utils/__mocks__/index.js b/src/js/utils/__mocks__/index.js
index 31e254bb0d..f7f999ff14 100644
--- a/src/js/utils/__mocks__/index.js
+++ b/src/js/utils/__mocks__/index.js
@@ -7,3 +7,4 @@ export const getOffset = jest.genMockFunction().mockImplementation(() => {
 });
 
 export const animate = jest.genMockFunction();
+export const isBetween = jest.genMockFunction().mockImplementation(() => true);
diff --git a/src/scss/helpers/_variables.scss b/src/scss/helpers/_variables.scss
index 3be7511258..adbc9ab55f 100644
--- a/src/scss/helpers/_variables.scss
+++ b/src/scss/helpers/_variables.scss
@@ -987,7 +987,7 @@ $md-linear-progress-opacity: .3 !default;
 ///
 /// @type String
 /// @group progress
-$md-linear-progress-height: 4px !default;
+$md-linear-progress-height: 3px !default;
 
 /// The time it takes for an indeterminate linear progress animation cycle to take.
 /// This is the bar moving backwards and the short bar.


Test patch:
diff --git a/src/js/Progress/__tests__/CircularProgress.js b/src/js/Progress/__tests__/CircularProgress.js
new file mode 100644
index 0000000000..324e5548d5
--- /dev/null
+++ b/src/js/Progress/__tests__/CircularProgress.js
@@ -0,0 +1,63 @@
+/*eslint-env jest*/
+jest.unmock('../CircularProgress');
+
+import React from 'react';
+import { findDOMNode } from 'react-dom';
+import {
+  renderIntoDocument,
+} from 'react-addons-test-utils';
+
+import CircularProgress from '../CircularProgress';
+
+describe('CircularProgress', () => {
+  it('merges className and style', () => {
+    const style = { display: 'block' };
+    const className = 'test';
+    const circularProgress = renderIntoDocument(
+      <CircularProgress style={style} className={className} />
+    );
+
+    const circularProgressNode = findDOMNode(circularProgress);
+    expect(circularProgressNode.style.display).toBe(style.display);
+    expect(circularProgressNode.classList.contains(className)).toBe(true);
+  });
+
+  it('converts to a determinate progress if there is a value', () => {
+    const progress = renderIntoDocument(
+      <CircularProgress value={0} />
+    );
+
+    const progressNode = findDOMNode(progress);
+    expect(progressNode.classList.contains('determinate')).toBe(true);
+  });
+
+  it('combines the existing transform style in a determinate circular progress', () => {
+    const style = {
+      WebkitTransform: 'transform3d(0, 22px, 0)',
+      MozTransform: 'transform3d(0, 22px, 0)',
+      transform: 'transform3d(0, 22px, 0)',
+    };
+
+    const progress = renderIntoDocument(
+      <CircularProgress style={style} value={2} />
+    );
+
+    const progressNode = findDOMNode(progress);
+    expect(progressNode.style.WebkitTransform).toContain(style.WebkitTransform);
+    expect(progressNode.style.MozTransform).toContain(style.MozTransform);
+    expect(progressNode.style.transform).toContain(style.transform);
+  });
+
+  it('allows the circular progress to be scaled', () => {
+    const baseSize = 24;
+    const scale = 1.5;
+    const progress = renderIntoDocument(
+      <CircularProgress scale={scale} />
+    );
+
+    const expected = (baseSize * scale) + '';
+    const progressNode = findDOMNode(progress);
+    expect(progressNode.getAttribute('width')).toBe(expected);
+    expect(progressNode.getAttribute('height')).toBe(expected);
+  });
+});
diff --git a/src/js/Progress/__tests__/LinearProgress.js b/src/js/Progress/__tests__/LinearProgress.js
new file mode 100644
index 0000000000..77edc7817d
--- /dev/null
+++ b/src/js/Progress/__tests__/LinearProgress.js
@@ -0,0 +1,53 @@
+/*eslint-env jest*/
+jest.unmock('../LinearProgress');
+
+import React from 'react';
+import { findDOMNode } from 'react-dom';
+import {
+  renderIntoDocument,
+  findRenderedDOMComponentWithClass,
+} from 'react-addons-test-utils';
+
+import LinearProgress from '../LinearProgress';
+
+describe('LinearProgress', () => {
+  it('merges className and style', () => {
+    const style = { display: 'block' };
+    const className = 'test';
+    const progress = renderIntoDocument(
+      <LinearProgress style={style} className={className} />
+    );
+
+    const progressNode = findDOMNode(progress);
+    expect(progressNode.style.display).toBe(style.display);
+    expect(progressNode.classList.contains(className)).toBe(true);
+  });
+
+  it('converts to a determinate progress if there is a value', () => {
+    const progress = renderIntoDocument(
+      <LinearProgress value={0} />
+    );
+
+    const progressNode = findRenderedDOMComponentWithClass(progress, 'md-linear-progress');
+    expect(progressNode.classList.contains('determinate')).toBe(true);
+    expect(progressNode.style.width).toBe('0%');
+  });
+
+  it('updates the progress width based on the current value', () => {
+    let value = 12;
+    let progress = renderIntoDocument(
+      <LinearProgress value={value} />
+    );
+
+    let progressNode = findRenderedDOMComponentWithClass(progress, 'md-linear-progress');
+    expect(progressNode.style.width).toBe(value + '%');
+
+    value = 0;
+    progress = renderIntoDocument(
+      <LinearProgress value={value} />
+    );
+
+    progressNode = findRenderedDOMComponentWithClass(progress, 'md-linear-progress');
+    expect(progressNode.style.width).toBe(value + '%');
+  });
+});