From 0bc6de13626aee553b33ee12c442255f9c4da70a Mon Sep 17 00:00:00 2001
From: Paul Kaplan <pkaplan@media.mit.edu>
Date: Wed, 23 Jan 2019 14:30:41 -0500
Subject: [PATCH] Throttle updates to assets on sprite selector item via HOC

Adds unit tests for this HOC.
---
 .../sprite-selector/sprite-list.jsx           |  5 +-
 src/lib/throttled-property-hoc.jsx            | 43 +++++++++++++++
 .../unit/util/throttled-property-hoc.test.jsx | 54 +++++++++++++++++++
 3 files changed, 101 insertions(+), 1 deletion(-)
 create mode 100644 src/lib/throttled-property-hoc.jsx
 create mode 100644 test/unit/util/throttled-property-hoc.test.jsx

diff --git a/src/components/sprite-selector/sprite-list.jsx b/src/components/sprite-selector/sprite-list.jsx
index 3f8910fe5..2c9ad0ccc 100644
--- a/src/components/sprite-selector/sprite-list.jsx
+++ b/src/components/sprite-selector/sprite-list.jsx
@@ -8,9 +8,12 @@ import Box from '../box/box.jsx';
 import SpriteSelectorItem from '../../containers/sprite-selector-item.jsx';
 import SortableHOC from '../../lib/sortable-hoc.jsx';
 import SortableAsset from '../asset-panel/sortable-asset.jsx';
+import ThrottledPropertyHOC from '../../lib/throttled-property-hoc.jsx';
 
 import styles from './sprite-selector.css';
 
+const ThrottledSpriteSelectorItem = ThrottledPropertyHOC('asset', 500, SpriteSelectorItem);
+
 const SpriteList = function (props) {
     const {
         containerRef,
@@ -74,7 +77,7 @@ const SpriteList = function (props) {
                             onAddSortable={onAddSortable}
                             onRemoveSortable={onRemoveSortable}
                         >
-                            <SpriteSelectorItem
+                            <ThrottledSpriteSelectorItem
                                 asset={sprite.costume && sprite.costume.asset}
                                 className={classNames(styles.sprite, {
                                     [styles.raised]: isRaised,
diff --git a/src/lib/throttled-property-hoc.jsx b/src/lib/throttled-property-hoc.jsx
new file mode 100644
index 000000000..cd7cd8ed8
--- /dev/null
+++ b/src/lib/throttled-property-hoc.jsx
@@ -0,0 +1,43 @@
+import React from 'react';
+
+/* Higher Order Component to throttle updates to specific props.
+ * Why? Because certain prop updates are expensive, and need to be throttled.
+ * This allows renders when other properties change, and will use the last
+ * rendered value of a prop for comparison.
+ * @param {string} propName the name of the prop to throttle updates from.
+ * @param {string} throttleTime the minimum time between updates to that specific property.
+ * @param {React.Component} WrappedComponent component who will not update for certain props.
+ * @returns {React.Component} component with throttling behavior
+ */
+const ThrottledPropertyHOC = function (propName, throttleTime, WrappedComponent) {
+    class ThrottledPropertyWrapper extends React.Component {
+        shouldComponentUpdate (nextProps) {
+            for (const property in nextProps) {
+                if (property !== propName && this.props[property] !== nextProps[property]) {
+                    return true; // Always update if another property has changed
+                }
+            }
+
+            // If only that prop has changed, allow update to go to render based
+            // on _lastRenderedTime and _lastRenderTime are updated in render
+            if (nextProps[propName] !== this._lastRenderedValue &&
+                Date.now() - this._lastRenderTime > throttleTime
+            ) {
+                return true; // Allow this update to go to render
+            }
+
+            return false;
+        }
+        render () {
+            this._lastRenderTime = Date.now();
+            this._lastRenderedValue = this.props[propName];
+            return (
+                <WrappedComponent {...this.props} />
+            );
+        }
+    }
+
+    return ThrottledPropertyWrapper;
+};
+
+export default ThrottledPropertyHOC;
diff --git a/test/unit/util/throttled-property-hoc.test.jsx b/test/unit/util/throttled-property-hoc.test.jsx
new file mode 100644
index 000000000..37fca7022
--- /dev/null
+++ b/test/unit/util/throttled-property-hoc.test.jsx
@@ -0,0 +1,54 @@
+import React from 'react';
+import {mount} from 'enzyme';
+
+import ThrottledPropertyHOC from '../../../src/lib/throttled-property-hoc.jsx';
+
+describe('VMListenerHOC', () => {
+    let mounted;
+    const throttleTime = 500;
+    beforeEach(() => {
+        const Component = ({propToThrottle, doNotThrottle}) => (
+            <input
+                name={doNotThrottle}
+                value={propToThrottle}
+            />
+        );
+        const WrappedComponent = ThrottledPropertyHOC('propToThrottle', throttleTime, Component);
+
+        global.Date.now = () => 0;
+
+        mounted = mount(
+            <WrappedComponent
+                doNotThrottle="oldvalue"
+                propToThrottle={0}
+            />
+        );
+    });
+
+    test('it passes the props on initial render ', () => {
+        expect(mounted.find('[value=0]').exists()).toEqual(true);
+        expect(mounted.find('[name="oldvalue"]').exists()).toEqual(true);
+    });
+
+    test('it does not rerender if throttled prop is updated too soon', () => {
+        global.Date.now = () => throttleTime / 2;
+        mounted.setProps({propToThrottle: 1});
+        mounted.update();
+        expect(mounted.find('[value=0]').exists()).toEqual(true);
+    });
+
+    test('it does rerender if throttled prop is updated after throttle timeout', () => {
+        global.Date.now = () => throttleTime * 2;
+        mounted.setProps({propToThrottle: 1});
+        mounted.update();
+        expect(mounted.find('[value=1]').exists()).toEqual(true);
+    });
+
+    test('it does rerender if a non-throttled prop is changed', () => {
+        global.Date.now = () => throttleTime / 2;
+        mounted.setProps({doNotThrottle: 'newvalue', propToThrottle: 2});
+        mounted.update();
+        expect(mounted.find('[name="newvalue"]').exists()).toEqual(true);
+        expect(mounted.find('[value=2]').exists()).toEqual(true);
+    });
+});
-- 
GitLab