From 6ddfed6e20a7ec0f62f08f5197672db0ef6d9b31 Mon Sep 17 00:00:00 2001 From: Naina Raisinghani Date: Tue, 20 Feb 2018 13:56:43 +1100 Subject: [PATCH 1/5] WIP --- examples/selector.amp.html | 103 ++++++++++++++++++ extensions/amp-selector/0.1/amp-selector.js | 41 +++++++ .../0.1/test/test-amp-selector.js | 33 ++++++ spec/amp-actions-and-events.md | 16 +++ 4 files changed, 193 insertions(+) create mode 100644 examples/selector.amp.html diff --git a/examples/selector.amp.html b/examples/selector.amp.html new file mode 100644 index 000000000000..bc9f59f2f047 --- /dev/null +++ b/examples/selector.amp.html @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + +
+ + +
    +
  • +
  • +
  • Poo
  • +
  • Poop
  • +
  • Poo poo
  • +
  • None of the Above
  • +
+
+ +
+ + + + + + +
+ + +
    +
  • +
  • +
  • None of the Above
  • +
+
+ + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + +
+ +
+
+ + +
+ + +
+ +
+
+
+
Non-live-list option
+
+ + diff --git a/extensions/amp-selector/0.1/amp-selector.js b/extensions/amp-selector/0.1/amp-selector.js index 0701a6c26350..ae2a43dcf0b2 100644 --- a/extensions/amp-selector/0.1/amp-selector.js +++ b/extensions/amp-selector/0.1/amp-selector.js @@ -113,6 +113,24 @@ export class AmpSelector extends AMP.BaseElement { this.element.addEventListener('click', this.clickHandler_.bind(this)); this.element.addEventListener('keydown', this.keyDownHandler_.bind(this)); } + + this.registerAction('selectUp', invocation => { + const args = invocation.args; + if (args && args['incrementPos'] !== undefined) { + this.select_(-1 * args['incrementPos']); + } else { + this.select_(-1); + } + }, ActionTrust.LOW); + + this.registerAction('selectDown', invocation => { + const args = invocation.args; + if (args && args['decrementPos'] !== undefined) { + this.select_(args['decrementPos']); + } else { + this.select_(1); + } + }, ActionTrust.LOW); } /** @override */ @@ -319,6 +337,29 @@ export class AmpSelector extends AMP.BaseElement { } } + /** + * Handles selectUp events. + * @param {!integer} incrementPos + */ + select_(incrementPos) { + + if (this.isMultiple_) { + this.clearSelection_(this.selectedOptions_[0]); + } + // Change the selection to the next element in the specified direction. + // The selection should loop around if the user attempts to go one + // past the beginning or end. + let selectedIndex_ = this.options_.indexOf(this.selectedOptions_[0]); + + selectedIndex_ = (selectedIndex_ + incrementPos) % this.options_.length; + if (selectedIndex_ < 0) { + selectedIndex_ = selectedIndex_ + this.options_.length; + } + + const selectedOption = this.options_[selectedIndex_]; + this.onOptionPicked_(selectedOption); + } + /** * Handles keyboard events. * @param {!Event} event diff --git a/extensions/amp-selector/0.1/test/test-amp-selector.js b/extensions/amp-selector/0.1/test/test-amp-selector.js index 1344cd4768ac..b3f0f519f49c 100644 --- a/extensions/amp-selector/0.1/test/test-amp-selector.js +++ b/extensions/amp-selector/0.1/test/test-amp-selector.js @@ -651,6 +651,39 @@ describes.realWin('amp-selector', { ampSelector, 'select', /* CustomEvent */ eventMatcher); }); + it('should trigger `select` action when user uses ' + + '`selectUp`/`selectDown` action with default skip value of 1', () => { + const ampSelector = getSelector({ + attributes: { + id: 'ampSelector', + }, + config: { + count: 6, + }, + }); + ampSelector.children[0].setAttribute('selected', ''); + ampSelector.build(); + + expect(ampSelector.hasAttribute('multiple')).to.be.false; + expect(ampSelector.children[0].hasAttribute('selected')).to.be.true; + + const button_down = win.document.createElement('button_down'); + button_down.setAttribute('on', 'tap:ampSelector.selectDown()'); + win.document.body.appendChild(button_down); + button_down.click(); + + // expect(ampSelector.children[0].hasAttribute('selected')).to.be.false; + // expect(ampSelector.children[1].hasAttribute('selected')).to.be.true; + + const button_up = win.document.createElement('button_up'); + button_up.setAttribute('on', 'tap:ampSelector.selectUp()'); + win.document.body.appendChild(button_up); + button_up.click(); + + // expect(ampSelector.children[1].hasAttribute('selected')).to.be.false; + // expect(ampSelector.children[0].hasAttribute('selected')).to.be.true; + }); + describe('keyboard-select-mode', () => { it('should have `none` mode by default', () => { diff --git a/spec/amp-actions-and-events.md b/spec/amp-actions-and-events.md index 1862c51cfc4f..6d5ae48457f6 100644 --- a/spec/amp-actions-and-events.md +++ b/spec/amp-actions-and-events.md @@ -362,6 +362,22 @@ event.response +### amp-selector + + + + + + + + + + + + + +
ActionDescription
selectUp(incrementPos=INTEGER)Changes the selected element to the currentPos-incrementPos. If no increment value is specified, the default is 1.
selectDown(decrementPos=INTEGER)Changes the selected element to the currentPos+decrementPos. If no decrement value is specified, the default is -1.
+ ### amp-sidebar From 524a18af185c04e36fb9634b9cb2b410ec11dd19 Mon Sep 17 00:00:00 2001 From: Naina Raisinghani Date: Tue, 20 Feb 2018 17:44:12 +1100 Subject: [PATCH 2/5] Finalize implementation and tests --- examples/selector.amp.html | 103 ------------------ extensions/amp-selector/0.1/amp-selector.js | 10 +- .../0.1/test/test-amp-selector.js | 49 +++++++-- 3 files changed, 41 insertions(+), 121 deletions(-) delete mode 100644 examples/selector.amp.html diff --git a/examples/selector.amp.html b/examples/selector.amp.html deleted file mode 100644 index bc9f59f2f047..000000000000 --- a/examples/selector.amp.html +++ /dev/null @@ -1,103 +0,0 @@ - - - - - - - - - - - - - - - -
- - -
    -
  • -
  • -
  • Poo
  • -
  • Poop
  • -
  • Poo poo
  • -
  • None of the Above
  • -
-
- - - - - - - - -
- - -
    -
  • -
  • -
  • None of the Above
  • -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
-
- - -
- - -
- -
-
-
-
Non-live-list option
-
- - diff --git a/extensions/amp-selector/0.1/amp-selector.js b/extensions/amp-selector/0.1/amp-selector.js index ae2a43dcf0b2..3878485267da 100644 --- a/extensions/amp-selector/0.1/amp-selector.js +++ b/extensions/amp-selector/0.1/amp-selector.js @@ -339,17 +339,14 @@ export class AmpSelector extends AMP.BaseElement { /** * Handles selectUp events. - * @param {!integer} incrementPos + * @param {number} incrementPos */ select_(incrementPos) { - - if (this.isMultiple_) { - this.clearSelection_(this.selectedOptions_[0]); - } // Change the selection to the next element in the specified direction. // The selection should loop around if the user attempts to go one // past the beginning or end. let selectedIndex_ = this.options_.indexOf(this.selectedOptions_[0]); + const oldSelectedIndex_ = selectedIndex_; selectedIndex_ = (selectedIndex_ + incrementPos) % this.options_.length; if (selectedIndex_ < 0) { @@ -357,7 +354,8 @@ export class AmpSelector extends AMP.BaseElement { } const selectedOption = this.options_[selectedIndex_]; - this.onOptionPicked_(selectedOption); + this.setSelection_(selectedOption); + this.clearSelection_(this.options_[oldSelectedIndex_]); } /** diff --git a/extensions/amp-selector/0.1/test/test-amp-selector.js b/extensions/amp-selector/0.1/test/test-amp-selector.js index b3f0f519f49c..e9eb806782c0 100644 --- a/extensions/amp-selector/0.1/test/test-amp-selector.js +++ b/extensions/amp-selector/0.1/test/test-amp-selector.js @@ -663,25 +663,50 @@ describes.realWin('amp-selector', { }); ampSelector.children[0].setAttribute('selected', ''); ampSelector.build(); + const impl = ampSelector.implementation_; expect(ampSelector.hasAttribute('multiple')).to.be.false; expect(ampSelector.children[0].hasAttribute('selected')).to.be.true; - const button_down = win.document.createElement('button_down'); - button_down.setAttribute('on', 'tap:ampSelector.selectDown()'); - win.document.body.appendChild(button_down); - button_down.click(); + impl.executeAction({method: 'selectDown', satisfiesTrust: () => true}); + expect(ampSelector.children[0].hasAttribute('selected')).to.be.false; + expect(ampSelector.children[1].hasAttribute('selected')).to.be.true; + + impl.executeAction({method: 'selectUp', satisfiesTrust: () => true}); + + expect(ampSelector.children[1].hasAttribute('selected')).to.be.false; + expect(ampSelector.children[0].hasAttribute('selected')).to.be.true; + + }); + + it('should trigger `select` action when user uses ' + + '`selectUp`/`selectDown` action with user specified skip value', () => { + const ampSelector = getSelector({ + attributes: { + id: 'ampSelector', + }, + config: { + count: 6, + }, + }); + ampSelector.children[0].setAttribute('selected', ''); + ampSelector.build(); + const impl = ampSelector.implementation_; - // expect(ampSelector.children[0].hasAttribute('selected')).to.be.false; - // expect(ampSelector.children[1].hasAttribute('selected')).to.be.true; + expect(ampSelector.hasAttribute('multiple')).to.be.false; + expect(ampSelector.children[0].hasAttribute('selected')).to.be.true; - const button_up = win.document.createElement('button_up'); - button_up.setAttribute('on', 'tap:ampSelector.selectUp()'); - win.document.body.appendChild(button_up); - button_up.click(); + let args = {'incrementPos': 2}; + impl.executeAction( + {method: 'selectDown', args, satisfiesTrust: () => true}); + expect(ampSelector.children[0].hasAttribute('selected')).to.be.false; + expect(ampSelector.children[1].hasAttribute('selected')).to.be.true; - // expect(ampSelector.children[1].hasAttribute('selected')).to.be.false; - // expect(ampSelector.children[0].hasAttribute('selected')).to.be.true; + args = {'decrementPos': 2}; + impl.executeAction( + {method: 'selectUp', args, satisfiesTrust: () => true}); + expect(ampSelector.children[1].hasAttribute('selected')).to.be.false; + expect(ampSelector.children[0].hasAttribute('selected')).to.be.true; }); describe('keyboard-select-mode', () => { From 2d3bd60a58456a47e256c565ab4955f246180b93 Mon Sep 17 00:00:00 2001 From: Naina Raisinghani Date: Wed, 21 Feb 2018 11:19:14 +1100 Subject: [PATCH 3/5] Make cvializ@ suggestions - change input argument name and clean up code --- extensions/amp-selector/0.1/amp-selector.js | 33 +++++++------------ .../0.1/test/test-amp-selector.js | 12 +++---- spec/amp-actions-and-events.md | 6 ++-- 3 files changed, 20 insertions(+), 31 deletions(-) diff --git a/extensions/amp-selector/0.1/amp-selector.js b/extensions/amp-selector/0.1/amp-selector.js index 3878485267da..cedd8c16f2ec 100644 --- a/extensions/amp-selector/0.1/amp-selector.js +++ b/extensions/amp-selector/0.1/amp-selector.js @@ -116,20 +116,14 @@ export class AmpSelector extends AMP.BaseElement { this.registerAction('selectUp', invocation => { const args = invocation.args; - if (args && args['incrementPos'] !== undefined) { - this.select_(-1 * args['incrementPos']); - } else { - this.select_(-1); - } + const delta = (args && args['delta'] !== undefined) ? -args['delta'] : -1; + this.select_(delta); }, ActionTrust.LOW); this.registerAction('selectDown', invocation => { const args = invocation.args; - if (args && args['decrementPos'] !== undefined) { - this.select_(args['decrementPos']); - } else { - this.select_(1); - } + const delta = (args && args['delta'] !== undefined) ? args['delta'] : 1; + this.select_(delta); }, ActionTrust.LOW); } @@ -339,23 +333,18 @@ export class AmpSelector extends AMP.BaseElement { /** * Handles selectUp events. - * @param {number} incrementPos + * @param {number} delta */ - select_(incrementPos) { + select_(delta) { // Change the selection to the next element in the specified direction. // The selection should loop around if the user attempts to go one // past the beginning or end. - let selectedIndex_ = this.options_.indexOf(this.selectedOptions_[0]); - const oldSelectedIndex_ = selectedIndex_; - - selectedIndex_ = (selectedIndex_ + incrementPos) % this.options_.length; - if (selectedIndex_ < 0) { - selectedIndex_ = selectedIndex_ + this.options_.length; - } + const previousIndex = this.options_.indexOf(this.selectedOptions_[0]); + const index = previousIndex + delta; + const normalizedIndex = index % this.options_.length; - const selectedOption = this.options_[selectedIndex_]; - this.setSelection_(selectedOption); - this.clearSelection_(this.options_[oldSelectedIndex_]); + this.setSelection_(this.options_[normalizedIndex]); + this.clearSelection_(this.options_[previousIndex]); } /** diff --git a/extensions/amp-selector/0.1/test/test-amp-selector.js b/extensions/amp-selector/0.1/test/test-amp-selector.js index e9eb806782c0..ff2b79d7a33d 100644 --- a/extensions/amp-selector/0.1/test/test-amp-selector.js +++ b/extensions/amp-selector/0.1/test/test-amp-selector.js @@ -652,7 +652,7 @@ describes.realWin('amp-selector', { }); it('should trigger `select` action when user uses ' + - '`selectUp`/`selectDown` action with default skip value of 1', () => { + '`selectUp`/`selectDown` action with default delta value of 1', () => { const ampSelector = getSelector({ attributes: { id: 'ampSelector', @@ -680,7 +680,7 @@ describes.realWin('amp-selector', { }); it('should trigger `select` action when user uses ' + - '`selectUp`/`selectDown` action with user specified skip value', () => { + '`selectUp`/`selectDown` action with user specified delta value', () => { const ampSelector = getSelector({ attributes: { id: 'ampSelector', @@ -696,16 +696,16 @@ describes.realWin('amp-selector', { expect(ampSelector.hasAttribute('multiple')).to.be.false; expect(ampSelector.children[0].hasAttribute('selected')).to.be.true; - let args = {'incrementPos': 2}; + let args = {'delta': 2}; impl.executeAction( {method: 'selectDown', args, satisfiesTrust: () => true}); expect(ampSelector.children[0].hasAttribute('selected')).to.be.false; - expect(ampSelector.children[1].hasAttribute('selected')).to.be.true; + expect(ampSelector.children[2].hasAttribute('selected')).to.be.true; - args = {'decrementPos': 2}; + args = {'delta': 2}; impl.executeAction( {method: 'selectUp', args, satisfiesTrust: () => true}); - expect(ampSelector.children[1].hasAttribute('selected')).to.be.false; + expect(ampSelector.children[2].hasAttribute('selected')).to.be.false; expect(ampSelector.children[0].hasAttribute('selected')).to.be.true; }); diff --git a/spec/amp-actions-and-events.md b/spec/amp-actions-and-events.md index 6d5ae48457f6..f7f78bbafa9b 100644 --- a/spec/amp-actions-and-events.md +++ b/spec/amp-actions-and-events.md @@ -369,12 +369,12 @@ event.response
- - + + - +
Description
selectUp(incrementPos=INTEGER)Changes the selected element to the currentPos-incrementPos. If no increment value is specified, the default is 1.selectUp(delta=INTEGER)Changes the selected element to the currentPos - `delta`. The default `delta` is set to 1.
selectDown(decrementPos=INTEGER)Changes the selected element to the currentPos+decrementPos. If no decrement value is specified, the default is -1.Changes the selected element to the currentPos + `delta`. The default `delta` is set to -1.
From 33f521e001e88046ee7af81170fdb26bb0d6954b Mon Sep 17 00:00:00 2001 From: Naina Raisinghani Date: Wed, 21 Feb 2018 14:05:43 +1100 Subject: [PATCH 4/5] Cater for large negative numbers and add tests to check for that too --- extensions/amp-selector/0.1/amp-selector.js | 8 ++++- .../0.1/test/test-amp-selector.js | 32 +++++++++++++++++++ spec/amp-actions-and-events.md | 6 ++-- 3 files changed, 42 insertions(+), 4 deletions(-) diff --git a/extensions/amp-selector/0.1/amp-selector.js b/extensions/amp-selector/0.1/amp-selector.js index cedd8c16f2ec..31c456563bff 100644 --- a/extensions/amp-selector/0.1/amp-selector.js +++ b/extensions/amp-selector/0.1/amp-selector.js @@ -341,7 +341,13 @@ export class AmpSelector extends AMP.BaseElement { // past the beginning or end. const previousIndex = this.options_.indexOf(this.selectedOptions_[0]); const index = previousIndex + delta; - const normalizedIndex = index % this.options_.length; + const normalizedIndex = index > 0 ? + index % this.options_.length : + ((index % this.options_.length) + + this.options_.length) % this.options_.length; + + user().assert(normalizedIndex >= 0, + 'This should have been positive'); this.setSelection_(this.options_[normalizedIndex]); this.clearSelection_(this.options_[previousIndex]); diff --git a/extensions/amp-selector/0.1/test/test-amp-selector.js b/extensions/amp-selector/0.1/test/test-amp-selector.js index ff2b79d7a33d..02695f40254e 100644 --- a/extensions/amp-selector/0.1/test/test-amp-selector.js +++ b/extensions/amp-selector/0.1/test/test-amp-selector.js @@ -709,6 +709,38 @@ describes.realWin('amp-selector', { expect(ampSelector.children[0].hasAttribute('selected')).to.be.true; }); + it('should trigger `select` action when user uses ' + + '`selectUp`/`selectDown` action with user specified delta value ' + + '(test large values)', () => { + const ampSelector = getSelector({ + attributes: { + id: 'ampSelector', + }, + config: { + count: 5, + }, + }); + ampSelector.children[1].setAttribute('selected', ''); + ampSelector.build(); + const impl = ampSelector.implementation_; + + expect(ampSelector.hasAttribute('multiple')).to.be.false; + expect(ampSelector.children[1].hasAttribute('selected')).to.be.true; + + let args = {'delta': 1001}; + impl.executeAction( + {method: 'selectDown', args, satisfiesTrust: () => true}); + expect(ampSelector.children[1].hasAttribute('selected')).to.be.false; + expect(ampSelector.children[2].hasAttribute('selected')).to.be.true; + + args = {'delta': 1001}; + impl.executeAction( + {method: 'selectUp', args, satisfiesTrust: () => true}); + expect(ampSelector.children[2].hasAttribute('selected')).to.be.false; + expect(ampSelector.children[1].hasAttribute('selected')).to.be.true; + }); + + describe('keyboard-select-mode', () => { it('should have `none` mode by default', () => { diff --git a/spec/amp-actions-and-events.md b/spec/amp-actions-and-events.md index f7f78bbafa9b..52b3e5c4c702 100644 --- a/spec/amp-actions-and-events.md +++ b/spec/amp-actions-and-events.md @@ -370,11 +370,11 @@ event.response selectUp(delta=INTEGER) - Changes the selected element to the currentPos - `delta`. The default `delta` is set to 1. + Moves the selection up by the value of `delta`. The default `delta` is set to 1. - selectDown(decrementPos=INTEGER) - Changes the selected element to the currentPos + `delta`. The default `delta` is set to -1. + selectDown(delta=INTEGER) + Moves the selection down by the value of `delta`. The default `delta` is set to -1. From a848f219e07cb400e7b3c482ae8f9c87724dac61 Mon Sep 17 00:00:00 2001 From: Naina Raisinghani Date: Thu, 22 Feb 2018 10:10:29 +1100 Subject: [PATCH 5/5] Move mod functionality to helper and add tests too --- extensions/amp-selector/0.1/amp-selector.js | 10 +---- src/utils/math.js | 26 +++++++++++ test/functional/utils/test-math.js | 50 ++++++++++++++++++++- 3 files changed, 77 insertions(+), 9 deletions(-) diff --git a/extensions/amp-selector/0.1/amp-selector.js b/extensions/amp-selector/0.1/amp-selector.js index 31c456563bff..ade6f4a05898 100644 --- a/extensions/amp-selector/0.1/amp-selector.js +++ b/extensions/amp-selector/0.1/amp-selector.js @@ -21,7 +21,7 @@ import {Services} from '../../../src/services'; import {closestBySelector, isRTL, tryFocus} from '../../../src/dom'; import {createCustomEvent} from '../../../src/event-helper'; import {dev, user} from '../../../src/log'; - +import {mod} from '../../../src/utils/math'; const TAG = 'amp-selector'; /** @@ -341,13 +341,7 @@ export class AmpSelector extends AMP.BaseElement { // past the beginning or end. const previousIndex = this.options_.indexOf(this.selectedOptions_[0]); const index = previousIndex + delta; - const normalizedIndex = index > 0 ? - index % this.options_.length : - ((index % this.options_.length) + - this.options_.length) % this.options_.length; - - user().assert(normalizedIndex >= 0, - 'This should have been positive'); + const normalizedIndex = mod(index, this.options_.length); this.setSelection_(this.options_[normalizedIndex]); this.clearSelection_(this.options_[previousIndex]); diff --git a/src/utils/math.js b/src/utils/math.js index 286eb0f554b4..25154bf74a8a 100644 --- a/src/utils/math.js +++ b/src/utils/math.js @@ -53,6 +53,32 @@ export function mapRange(val, min1, max1, min2, max2) { return (val - min1) * (max2 - min2) / (max1 - min1) + min2; } +/** + * Computes the modulus of values `a` and `b`. + * + * This is needed because the % operator in JavaScript doesn't implement + * modulus behaviour as can be seen by the spec here: + * http://www.ecma-international.org/ecma-262/5.1/#sec-11.5.3. + * It instead is used to obtain the remainder of a division. + * This function uses the remainder (%) operator to determine the modulus. + * Derived from here: + * https://stackoverflow.com/questions/25726760/javascript-modular-arithmetic/47354356#47354356 + * + * @param {number} a + * @param {number} b + * @returns {number} returns the modulus of the two numbers. + * @example + * + * _.min(10, 5); + * // => 0 + * + * _.mod(-1, 5); + * // => 4 + */ +export function mod(a, b) { + return a > 0 && b > 0 ? a % b : ((a % b) + b) % b; +} + /** * Restricts a number to be in the given min/max range. * diff --git a/test/functional/utils/test-math.js b/test/functional/utils/test-math.js index 40c68a0090df..c1283fcd7851 100644 --- a/test/functional/utils/test-math.js +++ b/test/functional/utils/test-math.js @@ -14,7 +14,7 @@ * limitations under the License. */ -import {clamp, mapRange} from '../../../src/utils/math'; +import {clamp, mapRange, mod} from '../../../src/utils/math'; describes.sandboxed('utils/math', {}, () => { @@ -40,6 +40,54 @@ describes.sandboxed('utils/math', {}, () => { }); }); + describe('mod', () => { + it('a -> positive number, b -> positive number', () => { + expect(mod(0, 5)).to.equal(0); + expect(mod(1, 5)).to.equal(1); + expect(mod(2, 5)).to.equal(2); + expect(mod(3, 5)).to.equal(3); + expect(mod(4, 5)).to.equal(4); + expect(mod(5, 5)).to.equal(0); + expect(mod(6, 5)).to.equal(1); + expect(mod(7, 5)).to.equal(2); + expect(mod(1001, 5)).to.equal(1); + }); + + it('a -> negative number, b -> positive number', () => { + expect(mod(-1, 5)).to.equal(4); + expect(mod(-2, 5)).to.equal(3); + expect(mod(-3, 5)).to.equal(2); + expect(mod(-4, 5)).to.equal(1); + expect(mod(-5, 5)).to.equal(0); + expect(mod(-6, 5)).to.equal(4); + expect(mod(-7, 5)).to.equal(3); + expect(mod(-1001, 5)).to.equal(4); + }); + + it('a -> positive number, b -> negative number', () => { + expect(mod(0, -5)).to.equal(0); + expect(mod(1, -5)).to.equal(-4); + expect(mod(2, -5)).to.equal(-3); + expect(mod(3, -5)).to.equal(-2); + expect(mod(4, -5)).to.equal(-1); + expect(mod(5, -5)).to.equal(0); + expect(mod(6, -5)).to.equal(-4); + expect(mod(7, -5)).to.equal(-3); + expect(mod(1001, -5)).to.equal(-4); + }); + + it('a -> negative number, b -> negative number', () => { + expect(mod(-1, -5)).to.equal(-1); + expect(mod(-2, -5)).to.equal(-2); + expect(mod(-3, -5)).to.equal(-3); + expect(mod(-4, -5)).to.equal(-4); + expect(mod(-5, -5)).to.equal(0); + expect(mod(-6, -5)).to.equal(-1); + expect(mod(-7, -5)).to.equal(-2); + expect(mod(-1001, -5)).to.equal(-1); + }); + }); + describe('clamp', () => { it('should not clamp if within the range', () => { expect(clamp(0.5, 0, 1)).to.equal(0.5);