// Copyright 2008 The Closure Library Authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS-IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. /** * @fileoverview Unit tests for goog.functions. */ goog.provide('goog.functionsTest'); goog.setTestOnly('goog.functionsTest'); goog.require('goog.array'); goog.require('goog.functions'); goog.require('goog.testing.MockClock'); goog.require('goog.testing.PropertyReplacer'); goog.require('goog.testing.jsunit'); goog.require('goog.testing.recordFunction'); var fTrue = makeCallOrderLogger('fTrue', true); var gFalse = makeCallOrderLogger('gFalse', false); var hTrue = makeCallOrderLogger('hTrue', true); var stubs = new goog.testing.PropertyReplacer(); function setUp() { callOrder = []; } function tearDown() { stubs.reset(); } var foo = 'global'; var obj = {foo: 'obj'}; function getFoo(arg1, arg2) { return {foo: this.foo, arg1: arg1, arg2: arg2}; } function testTrue() { assertTrue(goog.functions.TRUE()); } function testFalse() { assertFalse(goog.functions.FALSE()); } function testLock() { function add(var_args) { var result = 0; for (var i = 0; i < arguments.length; i++) { result += arguments[i]; } return result; } assertEquals(6, add(1, 2, 3)); assertEquals(0, goog.functions.lock(add)(1, 2, 3)); assertEquals(3, goog.functions.lock(add, 2)(1, 2, 3)); assertEquals(6, goog.partial(add, 1, 2)(3)); assertEquals(3, goog.functions.lock(goog.partial(add, 1, 2))(3)); } function testNth() { assertEquals(1, goog.functions.nth(0)(1)); assertEquals(2, goog.functions.nth(1)(1, 2)); assertEquals('a', goog.functions.nth(0)('a', 'b')); assertEquals(undefined, goog.functions.nth(0)()); assertEquals(undefined, goog.functions.nth(1)(true)); assertEquals(undefined, goog.functions.nth(-1)()); } function testPartialRight() { var f = function(x, y) { return x / y; }; var g = goog.functions.partialRight(f, 2); assertEquals(2, g(4)); var h = goog.functions.partialRight(f, 4, 2); assertEquals(2, h()); var i = goog.functions.partialRight(f); assertEquals(2, i(4, 2)); } function testPartialRightUsesGlobal() { var f = function(x, y) { assertEquals(goog.global, this); return x / y; }; var g = goog.functions.partialRight(f, 2); var h = goog.functions.partialRight(g, 4); assertEquals(2, h()); } function testPartialRightWithCall() { var obj = {}; var f = function(x, y) { assertEquals(obj, this); return x / y; }; var g = goog.functions.partialRight(f, 2); var h = goog.functions.partialRight(g, 4); assertEquals(2, h.call(obj)); } function testPartialRightAndBind() { // This ensures that this "survives" through a partialRight. var p = goog.functions.partialRight(getFoo, 'dog'); var b = goog.bind(p, obj, 'hot'); var res = b(); assertEquals(obj.foo, res.foo); assertEquals('hot', res.arg1); assertEquals('dog', res.arg2); } function testBindAndPartialRight() { // This ensures that this "survives" through a partialRight. var b = goog.bind(getFoo, obj, 'hot'); var p = goog.functions.partialRight(b, 'dog'); var res = p(); assertEquals(obj.foo, res.foo); assertEquals('hot', res.arg1); assertEquals('dog', res.arg2); } function testPartialRightMultipleCalls() { var f = goog.testing.recordFunction(); var a = goog.functions.partialRight(f, 'foo'); var b = goog.functions.partialRight(a, 'bar'); a(); a(); b(); b(); assertEquals(4, f.getCallCount()); var calls = f.getCalls(); assertArrayEquals(['foo'], calls[0].getArguments()); assertArrayEquals(['foo'], calls[1].getArguments()); assertArrayEquals(['bar', 'foo'], calls[2].getArguments()); assertArrayEquals(['bar', 'foo'], calls[3].getArguments()); } function testIdentity() { assertEquals(3, goog.functions.identity(3)); assertEquals(3, goog.functions.identity(3, 4, 5, 6)); assertEquals('Hi there', goog.functions.identity('Hi there')); assertEquals(null, goog.functions.identity(null)); assertEquals(undefined, goog.functions.identity()); var arr = [1, 'b', null]; assertEquals(arr, goog.functions.identity(arr)); var obj = {a: 'ay', b: 'bee', c: 'see'}; assertEquals(obj, goog.functions.identity(obj)); } function testConstant() { assertEquals(3, goog.functions.constant(3)()); assertEquals(undefined, goog.functions.constant()()); } function testError() { var f = goog.functions.error('x'); var e = assertThrows( 'A function created by goog.functions.error must throw an error', f); assertEquals('x', e.message); } function testFail() { var obj = {}; var f = goog.functions.fail(obj); var e = assertThrows( 'A function created by goog.functions.raise must throw its input', f); assertEquals(obj, e); } function testCompose() { var add2 = function(x) { return x + 2; }; var doubleValue = function(x) { return x * 2; }; assertEquals(6, goog.functions.compose(doubleValue, add2)(1)); assertEquals(4, goog.functions.compose(add2, doubleValue)(1)); assertEquals(6, goog.functions.compose(add2, add2, doubleValue)(1)); assertEquals( 12, goog.functions.compose(doubleValue, add2, add2, doubleValue)(1)); assertUndefined(goog.functions.compose()(1)); assertEquals(3, goog.functions.compose(add2)(1)); var add2Numbers = function(x, y) { return x + y; }; assertEquals(17, goog.functions.compose(add2Numbers)(10, 7)); assertEquals(34, goog.functions.compose(doubleValue, add2Numbers)(10, 7)); } function testAdd() { assertUndefined(goog.functions.sequence()()); assertCallOrderAndReset([]); assert(goog.functions.sequence(fTrue)()); assertCallOrderAndReset(['fTrue']); assertFalse(goog.functions.sequence(fTrue, gFalse)()); assertCallOrderAndReset(['fTrue', 'gFalse']); assert(goog.functions.sequence(fTrue, gFalse, hTrue)()); assertCallOrderAndReset(['fTrue', 'gFalse', 'hTrue']); assert(goog.functions.sequence(goog.functions.identity)(true)); assertFalse(goog.functions.sequence(goog.functions.identity)(false)); } function testAnd() { // the return value is unspecified for an empty and goog.functions.and()(); assertCallOrderAndReset([]); assert(goog.functions.and(fTrue)()); assertCallOrderAndReset(['fTrue']); assertFalse(goog.functions.and(fTrue, gFalse)()); assertCallOrderAndReset(['fTrue', 'gFalse']); assertFalse(goog.functions.and(fTrue, gFalse, hTrue)()); assertCallOrderAndReset(['fTrue', 'gFalse']); assert(goog.functions.and(goog.functions.identity)(true)); assertFalse(goog.functions.and(goog.functions.identity)(false)); } function testOr() { // the return value is unspecified for an empty or goog.functions.or()(); assertCallOrderAndReset([]); assert(goog.functions.or(fTrue)()); assertCallOrderAndReset(['fTrue']); assert(goog.functions.or(fTrue, gFalse)()); assertCallOrderAndReset(['fTrue']); assert(goog.functions.or(fTrue, gFalse, hTrue)()); assertCallOrderAndReset(['fTrue']); assert(goog.functions.or(goog.functions.identity)(true)); assertFalse(goog.functions.or(goog.functions.identity)(false)); } function testNot() { assertTrue(goog.functions.not(gFalse)()); assertCallOrderAndReset(['gFalse']); assertTrue(goog.functions.not(goog.functions.identity)(false)); assertFalse(goog.functions.not(goog.functions.identity)(true)); var f = function(a, b) { assertEquals(1, a); assertEquals(2, b); return false; }; assertTrue(goog.functions.not(f)(1, 2)); } function testCreate(expectedArray) { var tempConstructor = function(a, b) { this.foo = a; this.bar = b; }; var factory = goog.partial(goog.functions.create, tempConstructor, 'baz'); var instance = factory('qux'); assert(instance instanceof tempConstructor); assertEquals(instance.foo, 'baz'); assertEquals(instance.bar, 'qux'); } function testWithReturnValue() { var obj = {}; var f = function(a, b) { assertEquals(obj, this); assertEquals(1, a); assertEquals(2, b); }; assertTrue(goog.functions.withReturnValue(f, true).call(obj, 1, 2)); assertFalse(goog.functions.withReturnValue(f, false).call(obj, 1, 2)); } function testEqualTo() { assertTrue(goog.functions.equalTo(42)(42)); assertFalse(goog.functions.equalTo(42)(13)); assertFalse(goog.functions.equalTo(42)('a string')); assertFalse(goog.functions.equalTo(42)('42')); assertTrue(goog.functions.equalTo(42, true)('42')); assertTrue(goog.functions.equalTo(0)(0)); assertFalse(goog.functions.equalTo(0)('')); assertFalse(goog.functions.equalTo(0)(1)); assertTrue(goog.functions.equalTo(0, true)(0)); assertTrue(goog.functions.equalTo(0, true)('')); assertFalse(goog.functions.equalTo(0, true)(1)); } function makeCallOrderLogger(name, returnValue) { return function() { callOrder.push(name); return returnValue; }; } function assertCallOrderAndReset(expectedArray) { assertArrayEquals(expectedArray, callOrder); callOrder = []; } function testCacheReturnValue() { var returnFive = function() { return 5; }; var recordedReturnFive = goog.testing.recordFunction(returnFive); var cachedRecordedReturnFive = goog.functions.cacheReturnValue(recordedReturnFive); assertEquals(0, recordedReturnFive.getCallCount()); assertEquals(5, cachedRecordedReturnFive()); assertEquals(1, recordedReturnFive.getCallCount()); assertEquals(5, cachedRecordedReturnFive()); assertEquals(1, recordedReturnFive.getCallCount()); } function testCacheReturnValueFlagEnabled() { var count = 0; var returnIncrementingInteger = function() { count++; return count; }; var recordedFunction = goog.testing.recordFunction(returnIncrementingInteger); var cachedRecordedFunction = goog.functions.cacheReturnValue(recordedFunction); assertEquals(0, recordedFunction.getCallCount()); assertEquals(1, cachedRecordedFunction()); assertEquals(1, recordedFunction.getCallCount()); assertEquals(1, cachedRecordedFunction()); assertEquals(1, recordedFunction.getCallCount()); assertEquals(1, cachedRecordedFunction()); } function testCacheReturnValueFlagDisabled() { stubs.set(goog.functions, 'CACHE_RETURN_VALUE', false); var count = 0; var returnIncrementingInteger = function() { count++; return count; }; var recordedFunction = goog.testing.recordFunction(returnIncrementingInteger); var cachedRecordedFunction = goog.functions.cacheReturnValue(recordedFunction); assertEquals(0, recordedFunction.getCallCount()); assertEquals(1, cachedRecordedFunction()); assertEquals(1, recordedFunction.getCallCount()); assertEquals(2, cachedRecordedFunction()); assertEquals(2, recordedFunction.getCallCount()); assertEquals(3, cachedRecordedFunction()); } function testOnce() { var recordedFunction = goog.testing.recordFunction(); var f = goog.functions.once(recordedFunction); assertEquals(0, recordedFunction.getCallCount()); f(); assertEquals(1, recordedFunction.getCallCount()); f(); assertEquals(1, recordedFunction.getCallCount()); } function testDebounce() { // Encoded sequences of commands to perform mapped to expected # of calls. // f: fire // w: wait (for the timer to elapse) assertAsyncDecoratorCommandSequenceCalls(goog.functions.debounce, { 'f': 0, 'ff': 0, 'fff': 0, 'fw': 1, 'ffw': 1, 'fffw': 1, 'fwffwf': 2, 'ffwwwffwwfwf': 3 }); } function testDebounceScopeBinding() { var interval = 500; var mockClock = new goog.testing.MockClock(true); var x = {'y': 0}; goog.functions.debounce(function() { ++this['y']; }, interval, x)(); assertEquals(0, x['y']); mockClock.tick(interval); assertEquals(1, x['y']); mockClock.uninstall(); } function testDebounceArgumentBinding() { var interval = 500; var mockClock = new goog.testing.MockClock(true); var calls = 0; var debouncedFn = goog.functions.debounce(function(a, b, c) { ++calls; assertEquals(3, a); assertEquals('string', b); assertEquals(false, c); }, interval); debouncedFn(3, 'string', false); mockClock.tick(interval); assertEquals(1, calls); // goog.functions.debounce should always pass the last arguments passed to the // decorator into the decorated function, even if called multiple times. debouncedFn(); mockClock.tick(interval / 2); debouncedFn(8, null, true); debouncedFn(3, 'string', false); mockClock.tick(interval); assertEquals(2, calls); mockClock.uninstall(); } function testDebounceArgumentAndScopeBinding() { var interval = 500; var mockClock = new goog.testing.MockClock(true); var x = {'calls': 0}; var debouncedFn = goog.functions.debounce(function(a, b, c) { ++this['calls']; assertEquals(3, a); assertEquals('string', b); assertEquals(false, c); }, interval, x); debouncedFn(3, 'string', false); mockClock.tick(interval); assertEquals(1, x['calls']); // goog.functions.debounce should always pass the last arguments passed to the // decorator into the decorated function, even if called multiple times. debouncedFn(); mockClock.tick(interval / 2); debouncedFn(8, null, true); debouncedFn(3, 'string', false); mockClock.tick(interval); assertEquals(2, x['calls']); mockClock.uninstall(); } function testThrottle() { // Encoded sequences of commands to perform mapped to expected # of calls. // f: fire // w: wait (for the timer to elapse) assertAsyncDecoratorCommandSequenceCalls(goog.functions.throttle, { 'f': 1, 'ff': 1, 'fff': 1, 'fw': 1, 'ffw': 2, 'fwf': 2, 'fffw': 2, 'fwfff': 2, 'fwfffw': 3, 'fwffwf': 3, 'ffwf': 2, 'ffwff': 2, 'ffwfw': 3, 'ffwffwf': 3, 'ffwffwff': 3, 'ffwffwffw': 4, 'ffwwwffwwfw': 5, 'ffwwwffwwfwf': 6 }); } function testThrottleScopeBinding() { var interval = 500; var mockClock = new goog.testing.MockClock(true); var x = {'y': 0}; goog.functions.throttle(function() { ++this['y']; }, interval, x)(); assertEquals(1, x['y']); mockClock.uninstall(); } function testThrottleArgumentBinding() { var interval = 500; var mockClock = new goog.testing.MockClock(true); var calls = 0; var throttledFn = goog.functions.throttle(function(a, b, c) { ++calls; assertEquals(3, a); assertEquals('string', b); assertEquals(false, c); }, interval); throttledFn(3, 'string', false); assertEquals(1, calls); // goog.functions.throttle should always pass the last arguments passed to the // decorator into the decorated function, even if called multiple times. throttledFn(); mockClock.tick(interval / 2); throttledFn(8, null, true); throttledFn(3, 'string', false); mockClock.tick(interval); assertEquals(2, calls); mockClock.uninstall(); } function testThrottleArgumentAndScopeBinding() { var interval = 500; var mockClock = new goog.testing.MockClock(true); var x = {'calls': 0}; var throttledFn = goog.functions.throttle(function(a, b, c) { ++this['calls']; assertEquals(3, a); assertEquals('string', b); assertEquals(false, c); }, interval, x); throttledFn(3, 'string', false); assertEquals(1, x['calls']); // goog.functions.throttle should always pass the last arguments passed to the // decorator into the decorated function, even if called multiple times. throttledFn(); mockClock.tick(interval / 2); throttledFn(8, null, true); throttledFn(3, 'string', false); mockClock.tick(interval); assertEquals(2, x['calls']); mockClock.uninstall(); } function testRateLimit() { // Encoded sequences of commands to perform mapped to expected # of calls. // f: fire // w: wait (for the timer to elapse) assertAsyncDecoratorCommandSequenceCalls(goog.functions.rateLimit, { 'f': 1, 'ff': 1, 'fff': 1, 'fw': 1, 'ffw': 1, 'fwf': 2, 'fffw': 1, 'fwfff': 2, 'fwfffw': 2, 'fwffwf': 3, 'ffwf': 2, 'ffwff': 2, 'ffwfw': 2, 'ffwffwf': 3, 'ffwffwff': 3, 'ffwffwffw': 3, 'ffwwwffwwfw': 3, 'ffwwwffwwfwf': 4 }); } function testRateLimitScopeBinding() { var interval = 500; var mockClock = new goog.testing.MockClock(true); var x = {'y': 0}; goog.functions.rateLimit(function() { ++this['y']; }, interval, x)(); assertEquals(1, x['y']); mockClock.uninstall(); } function testRateLimitArgumentBinding() { var interval = 500; var mockClock = new goog.testing.MockClock(true); var calls = 0; var rateLimitedFn = goog.functions.rateLimit(function(a, b, c) { ++calls; assertEquals(3, a); assertEquals('string', b); assertEquals(false, c); }, interval); rateLimitedFn(3, 'string', false); assertEquals(1, calls); // goog.functions.rateLimit should always pass the first arguments passed to // the // decorator into the decorated function, even if called multiple times. rateLimitedFn(); mockClock.tick(interval / 2); rateLimitedFn(8, null, true); mockClock.tick(interval); rateLimitedFn(3, 'string', false); assertEquals(2, calls); mockClock.uninstall(); } function testRateLimitArgumentAndScopeBinding() { var interval = 500; var mockClock = new goog.testing.MockClock(true); var x = {'calls': 0}; var rateLimitedFn = goog.functions.rateLimit(function(a, b, c) { ++this['calls']; assertEquals(3, a); assertEquals('string', b); assertEquals(false, c); }, interval, x); rateLimitedFn(3, 'string', false); assertEquals(1, x['calls']); // goog.functions.rateLimit should always pass the last arguments passed to // the // decorator into the decorated function, even if called multiple times. rateLimitedFn(); mockClock.tick(interval / 2); rateLimitedFn(8, null, true); mockClock.tick(interval); rateLimitedFn(3, 'string', false); assertEquals(2, x['calls']); mockClock.uninstall(); } /** * Wraps a {@code goog.testing.recordFunction} with the specified decorator and * executes a list of command sequences, asserting that in each case the * decorated function is called the expected number of times. * @param {function():*} decorator The async decorator to test. * @param {!Object.} expectedCommandSequenceCalls An object * mapping string command sequences (where 'f' is 'fire' and 'w' is 'wait') * to the number times we expect a decorated function to be called during * the execution of those commands. */ function assertAsyncDecoratorCommandSequenceCalls( decorator, expectedCommandSequenceCalls) { var interval = 500; var mockClock = new goog.testing.MockClock(true); for (var commandSequence in expectedCommandSequenceCalls) { var recordedFunction = goog.testing.recordFunction(); var f = decorator(recordedFunction, interval); for (var i = 0; i < commandSequence.length; ++i) { switch (commandSequence[i]) { case 'f': f(); break; case 'w': mockClock.tick(interval); break; } } var expectedCalls = expectedCommandSequenceCalls[commandSequence]; assertEquals( 'Expected ' + expectedCalls + ' calls for command sequence "' + commandSequence + '" (' + goog.array .map( commandSequence, function(command) { switch (command) { case 'f': return 'fire'; case 'w': return 'wait'; } }) .join(' -> ') + ')', expectedCalls, recordedFunction.getCallCount()); } mockClock.uninstall(); }