Adding mapbox-gl branch

This commit is contained in:
Andreas Hocevar
2015-03-16 18:50:27 +01:00
parent 7985f030fa
commit 57ee7f52fd
3109 changed files with 943365 additions and 0 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2007 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>
Closure Unit Tests - goog.testing.asserts
</title>
<script src="../base.js">
</script>
<script>
goog.require('goog.testing.assertsTest');
</script>
</head>
<body>
</body>
</html>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,175 @@
// Copyright 2010 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 A wrapper for MockControl that provides mocks and assertions
* for testing asynchronous code. All assertions will only be verified when
* $verifyAll is called on the wrapped MockControl.
*
* This class is meant primarily for testing code that exposes asynchronous APIs
* without being truly asynchronous (using asynchronous primitives like browser
* events or timeouts). This is often the case when true asynchronous
* depedencies have been mocked out. This means that it doesn't rely on
* AsyncTestCase or DeferredTestCase, although it can be used with those as
* well.
*
* Example usage:
*
* <pre>
* var mockControl = new goog.testing.MockControl();
* var asyncMockControl = new goog.testing.async.MockControl(mockControl);
*
* myAsyncObject.onSuccess(asyncMockControl.asyncAssertEquals(
* 'callback should run and pass the correct value',
* 'http://someurl.com');
* asyncMockControl.assertDeferredEquals(
* 'deferred object should be resolved with the correct value',
* 'http://someurl.com',
* myAsyncObject.getDeferredUrl());
* asyncMockControl.run();
* mockControl.$verifyAll();
* </pre>
*
*/
goog.provide('goog.testing.async.MockControl');
goog.require('goog.asserts');
goog.require('goog.async.Deferred');
goog.require('goog.debug');
goog.require('goog.testing.asserts');
goog.require('goog.testing.mockmatchers.IgnoreArgument');
/**
* Provides asynchronous mocks and assertions controlled by a parent
* MockControl.
*
* @param {goog.testing.MockControl} mockControl The parent MockControl.
* @constructor
* @final
*/
goog.testing.async.MockControl = function(mockControl) {
/**
* The parent MockControl.
* @type {goog.testing.MockControl}
* @private
*/
this.mockControl_ = mockControl;
};
/**
* Returns a function that will assert that it will be called, and run the given
* callback when it is.
*
* @param {string} name The name of the callback mock.
* @param {function(...*) : *} callback The wrapped callback. This will be
* called when the returned function is called.
* @param {Object=} opt_selfObj The object which this should point to when the
* callback is run.
* @return {!Function} The mock callback.
* @suppress {missingProperties} Mocks do not fit in the type system well.
*/
goog.testing.async.MockControl.prototype.createCallbackMock = function(
name, callback, opt_selfObj) {
goog.asserts.assert(
goog.isString(name),
'name parameter ' + goog.debug.deepExpose(name) + ' should be a string');
var ignored = new goog.testing.mockmatchers.IgnoreArgument();
// Use everyone's favorite "double-cast" trick to subvert the type system.
var obj = /** @type {Object} */ (this.mockControl_.createFunctionMock(name));
var fn = /** @type {Function} */ (obj);
fn(ignored).$does(function(args) {
if (opt_selfObj) {
callback = goog.bind(callback, opt_selfObj);
}
return callback.apply(this, args);
});
fn.$replay();
return function() { return fn(arguments); };
};
/**
* Returns a function that will assert that its arguments are equal to the
* arguments given to asyncAssertEquals. In addition, the function also asserts
* that it will be called.
*
* @param {string} message A message to print if the arguments are wrong.
* @param {...*} var_args The arguments to assert.
* @return {function(...*) : void} The mock callback.
*/
goog.testing.async.MockControl.prototype.asyncAssertEquals = function(
message, var_args) {
var expectedArgs = Array.prototype.slice.call(arguments, 1);
return this.createCallbackMock('asyncAssertEquals', function() {
assertObjectEquals(
message, expectedArgs, Array.prototype.slice.call(arguments));
});
};
/**
* Asserts that a deferred object will have an error and call its errback
* function.
* @param {goog.async.Deferred} deferred The deferred object.
* @param {function() : void} fn A function wrapping the code in which the error
* will occur.
*/
goog.testing.async.MockControl.prototype.assertDeferredError = function(
deferred, fn) {
deferred.addErrback(this.createCallbackMock(
'assertDeferredError', function() {}));
goog.testing.asserts.callWithoutLogging(fn);
};
/**
* Asserts that a deferred object will call its callback with the given value.
*
* @param {string} message A message to print if the arguments are wrong.
* @param {goog.async.Deferred|*} expected The expected value. If this is a
* deferred object, then the expected value is the deferred value.
* @param {goog.async.Deferred|*} actual The actual value. If this is a deferred
* object, then the actual value is the deferred value. Either this or
* 'expected' must be deferred.
*/
goog.testing.async.MockControl.prototype.assertDeferredEquals = function(
message, expected, actual) {
if (expected instanceof goog.async.Deferred &&
actual instanceof goog.async.Deferred) {
// Assert that the first deferred is resolved.
expected.addCallback(this.createCallbackMock(
'assertDeferredEquals', function(exp) {
// Assert that the second deferred is resolved, and that the value is
// as expected.
actual.addCallback(this.asyncAssertEquals(message, exp));
}, this));
} else if (expected instanceof goog.async.Deferred) {
expected.addCallback(this.createCallbackMock(
'assertDeferredEquals', function(exp) {
assertObjectEquals(message, exp, actual);
}));
} else if (actual instanceof goog.async.Deferred) {
actual.addCallback(this.asyncAssertEquals(message, expected));
} else {
throw Error('Either expected or actual must be deferred');
}
};
@@ -0,0 +1,23 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2010 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<!--
-->
<head>
<title>
Closure Unit Tests - goog.testing.async.MockControl;
</title>
<script type="text/javascript" src="../../base.js">
</script>
<script type="text/javascript">
goog.require('goog.testing.async.MockControlTest');
</script>
</head>
<body>
</body>
</html>
@@ -0,0 +1,217 @@
// Copyright 2010 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.
goog.provide('goog.testing.async.MockControlTest');
goog.setTestOnly('goog.testing.async.MockControlTest');
goog.require('goog.async.Deferred');
goog.require('goog.testing.MockControl');
goog.require('goog.testing.asserts');
goog.require('goog.testing.async.MockControl');
goog.require('goog.testing.jsunit');
var mockControl;
var asyncMockControl;
var mockControl2;
var asyncMockControl2;
function setUp() {
mockControl = new goog.testing.MockControl();
asyncMockControl = new goog.testing.async.MockControl(mockControl);
// We need two of these for the tests where we need to verify our meta-test
// assertions without verifying our tested assertions.
mockControl2 = new goog.testing.MockControl();
asyncMockControl2 = new goog.testing.async.MockControl(mockControl2);
}
function assertVerifyFails() {
assertThrowsJsUnitException(function() { mockControl.$verifyAll(); });
}
function testCreateCallbackMockFailure() {
asyncMockControl.createCallbackMock('failingCallbackMock', function() {});
assertVerifyFails();
}
function testCreateCallbackMockSuccess() {
var callback = asyncMockControl.createCallbackMock(
'succeedingCallbackMock', function() {});
callback();
mockControl.$verifyAll();
}
function testCreateCallbackMockSuccessWithArg() {
var callback = asyncMockControl.createCallbackMock(
'succeedingCallbackMockWithArg',
asyncMockControl.createCallbackMock(
'metaCallbackMock',
function(val) { assertEquals(10, val); }));
callback(10);
mockControl.$verifyAll();
}
function testCreateCallbackMockSuccessWithArgs() {
var callback = asyncMockControl.createCallbackMock(
'succeedingCallbackMockWithArgs',
asyncMockControl.createCallbackMock(
'metaCallbackMock', function(val1, val2, val3) {
assertEquals(10, val1);
assertEquals('foo', val2);
assertObjectEquals({foo: 'bar'}, val3);
}));
callback(10, 'foo', {foo: 'bar'});
mockControl.$verifyAll();
}
function testAsyncAssertEqualsFailureNeverCalled() {
asyncMockControl.asyncAssertEquals('never called', 12);
assertVerifyFails();
}
function testAsyncAssertEqualsFailureNumberOfArgs() {
assertThrowsJsUnitException(function() {
asyncMockControl.asyncAssertEquals('wrong number of args', 12)();
});
}
function testAsyncAssertEqualsFailureOneArg() {
assertThrowsJsUnitException(function() {
asyncMockControl.asyncAssertEquals('wrong arg value', 12)(13);
});
}
function testAsyncAssertEqualsFailureThreeArgs() {
assertThrowsJsUnitException(function() {
asyncMockControl.asyncAssertEquals('wrong arg values', 1, 2, 15)(2, 2, 15);
});
}
function testAsyncAssertEqualsSuccessNoArgs() {
asyncMockControl.asyncAssertEquals('should be called')();
mockControl.$verifyAll();
}
function testAsyncAssertEqualsSuccessThreeArgs() {
asyncMockControl.asyncAssertEquals('should have args', 1, 2, 3)(1, 2, 3);
mockControl.$verifyAll();
}
function testAssertDeferredErrorFailureNoError() {
var deferred = new goog.async.Deferred();
asyncMockControl.assertDeferredError(deferred, function() {});
assertVerifyFails();
}
function testAssertDeferredErrorSuccess() {
var deferred = new goog.async.Deferred();
asyncMockControl.assertDeferredError(deferred, function() {
deferred.errback(new Error('FAIL'));
});
mockControl.$verifyAll();
}
function testAssertDeferredEqualsFailureActualDeferredNeverResolves() {
var actual = new goog.async.Deferred();
asyncMockControl.assertDeferredEquals('doesn\'t resolve', 12, actual);
assertVerifyFails();
}
function testAssertDeferredEqualsFailureActualDeferredNeverResolvesBoth() {
var actualDeferred = new goog.async.Deferred();
var expectedDeferred = new goog.async.Deferred();
expectedDeferred.callback(12);
asyncMockControl.assertDeferredEquals(
'doesn\'t resolve', expectedDeferred, actualDeferred);
assertVerifyFails();
}
function testAssertDeferredEqualsFailureExpectedDeferredNeverResolves() {
var expected = new goog.async.Deferred();
asyncMockControl.assertDeferredEquals('doesn\'t resolve', expected, 12);
assertVerifyFails();
}
function testAssertDeferredEqualsFailureExpectedDeferredNeverResolvesBoth() {
var actualDeferred = new goog.async.Deferred();
var expectedDeferred = new goog.async.Deferred();
actualDeferred.callback(12);
asyncMockControl.assertDeferredEquals(
'doesn\'t resolve', expectedDeferred, actualDeferred);
assertVerifyFails();
}
function testAssertDeferredEqualsFailureWrongValueActualDeferred() {
var actual = new goog.async.Deferred();
asyncMockControl.assertDeferredEquals('doesn\'t resolve', 12, actual);
asyncMockControl2.assertDeferredError(actual, function() {
actual.callback(13);
});
mockControl2.$verifyAll();
}
function testAssertDeferredEqualsFailureWrongValueExpectedDeferred() {
var expected = new goog.async.Deferred();
asyncMockControl.assertDeferredEquals('doesn\'t resolve', expected, 12);
asyncMockControl2.assertDeferredError(expected, function() {
expected.callback(13);
});
mockControl2.$verifyAll();
}
function testAssertDeferredEqualsFailureWongValueBothDeferred() {
var actualDeferred = new goog.async.Deferred();
var expectedDeferred = new goog.async.Deferred();
asyncMockControl.assertDeferredEquals(
'different values', expectedDeferred, actualDeferred);
expectedDeferred.callback(12);
asyncMockControl2.assertDeferredError(actualDeferred, function() {
actualDeferred.callback(13);
});
assertVerifyFails();
mockControl2.$verifyAll();
}
function testAssertDeferredEqualsFailureNeitherDeferredEverResolves() {
var actualDeferred = new goog.async.Deferred();
var expectedDeferred = new goog.async.Deferred();
asyncMockControl.assertDeferredEquals(
'doesn\'t resolve', expectedDeferred, actualDeferred);
assertVerifyFails();
}
function testAssertDeferredEqualsSuccessActualDeferred() {
var actual = new goog.async.Deferred();
asyncMockControl.assertDeferredEquals('should succeed', 12, actual);
actual.callback(12);
mockControl.$verifyAll();
}
function testAssertDeferredEqualsSuccessExpectedDeferred() {
var expected = new goog.async.Deferred();
asyncMockControl.assertDeferredEquals('should succeed', expected, 12);
expected.callback(12);
mockControl.$verifyAll();
}
function testAssertDeferredEqualsSuccessBothDeferred() {
var actualDeferred = new goog.async.Deferred();
var expectedDeferred = new goog.async.Deferred();
asyncMockControl.assertDeferredEquals(
'should succeed', expectedDeferred, actualDeferred);
expectedDeferred.callback(12);
actualDeferred.callback(12);
mockControl.$verifyAll();
}
@@ -0,0 +1,900 @@
// Copyright 2010 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.
// All Rights Reserved.
/**
* @fileoverview A class representing a set of test functions that use
* asynchronous functions that cannot be meaningfully mocked.
*
* To create a Google-compatable JsUnit test using this test case, put the
* following snippet in your test:
*
* var asyncTestCase = goog.testing.AsyncTestCase.createAndInstall();
*
* To make the test runner wait for your asynchronous behaviour, use:
*
* asyncTestCase.waitForAsync('Waiting for xhr to respond');
*
* The next test will not start until the following call is made, or a
* timeout occurs:
*
* asyncTestCase.continueTesting();
*
* There does NOT need to be a 1:1 mapping of waitForAsync calls and
* continueTesting calls. The next test will be run after a single call to
* continueTesting is made, as long as there is no subsequent call to
* waitForAsync in the same thread.
*
* Example:
* // Returning here would cause the next test to be run.
* asyncTestCase.waitForAsync('description 1');
* // Returning here would *not* cause the next test to be run.
* // Only effect of additional waitForAsync() calls is an updated
* // description in the case of a timeout.
* asyncTestCase.waitForAsync('updated description');
* asyncTestCase.continueTesting();
* // Returning here would cause the next test to be run.
* asyncTestCase.waitForAsync('just kidding, still running.');
* // Returning here would *not* cause the next test to be run.
*
* The test runner can also be made to wait for more than one asynchronous
* event with:
*
* asyncTestCase.waitForSignals(n);
*
* The next test will not start until asyncTestCase.signal() is called n times,
* or the test step timeout is exceeded.
*
* This class supports asynchronous behaviour in all test functions except for
* tearDownPage. If such support is needed, it can be added.
*
* Example Usage:
*
* var asyncTestCase = goog.testing.AsyncTestCase.createAndInstall();
* // Optionally, set a longer-than-normal step timeout.
* asyncTestCase.stepTimeout = 30 * 1000;
*
* function testSetTimeout() {
* var step = 0;
* function stepCallback() {
* step++;
* switch (step) {
* case 1:
* var startTime = goog.now();
* asyncTestCase.waitForAsync('step 1');
* window.setTimeout(stepCallback, 100);
* break;
* case 2:
* assertTrue('Timeout fired too soon',
* goog.now() - startTime >= 100);
* asyncTestCase.waitForAsync('step 2');
* window.setTimeout(stepCallback, 100);
* break;
* case 3:
* assertTrue('Timeout fired too soon',
* goog.now() - startTime >= 200);
* asyncTestCase.continueTesting();
* break;
* default:
* fail('Unexpected call to stepCallback');
* }
* }
* stepCallback();
* }
*
* Known Issues:
* IE7 Exceptions:
* As the failingtest.html will show, it appears as though ie7 does not
* propagate an exception past a function called using the func.call()
* syntax. This causes case 3 of the failing tests (exceptions) to show up
* as timeouts in IE.
* window.onerror:
* This seems to catch errors only in ff2/ff3. It does not work in Safari or
* IE7. The consequence of this is that exceptions that would have been
* caught by window.onerror show up as timeouts.
*
* @author agrieve@google.com (Andrew Grieve)
*/
goog.provide('goog.testing.AsyncTestCase');
goog.provide('goog.testing.AsyncTestCase.ControlBreakingException');
goog.require('goog.testing.TestCase');
goog.require('goog.testing.asserts');
/**
* A test case that is capable of running tests the contain asynchronous logic.
* @param {string=} opt_name A descriptive name for the test case.
* @extends {goog.testing.TestCase}
* @constructor
*/
goog.testing.AsyncTestCase = function(opt_name) {
goog.testing.TestCase.call(this, opt_name);
};
goog.inherits(goog.testing.AsyncTestCase, goog.testing.TestCase);
/**
* Represents result of top stack function call.
* @typedef {{controlBreakingExceptionThrown: boolean, message: string}}
* @private
*/
goog.testing.AsyncTestCase.TopStackFuncResult_;
/**
* An exception class used solely for control flow.
* @param {string=} opt_message Error message.
* @constructor
* @extends {Error}
* @final
*/
goog.testing.AsyncTestCase.ControlBreakingException = function(opt_message) {
goog.testing.AsyncTestCase.ControlBreakingException.base(
this, 'constructor', opt_message);
/**
* The exception message.
* @type {string}
*/
this.message = opt_message || '';
};
goog.inherits(goog.testing.AsyncTestCase.ControlBreakingException, Error);
/**
* Return value for .toString().
* @type {string}
*/
goog.testing.AsyncTestCase.ControlBreakingException.TO_STRING =
'[AsyncTestCase.ControlBreakingException]';
/**
* Marks this object as a ControlBreakingException
* @type {boolean}
*/
goog.testing.AsyncTestCase.ControlBreakingException.prototype.
isControlBreakingException = true;
/** @override */
goog.testing.AsyncTestCase.ControlBreakingException.prototype.toString =
function() {
// This shows up in the console when the exception is not caught.
return goog.testing.AsyncTestCase.ControlBreakingException.TO_STRING;
};
/**
* How long to wait for a single step of a test to complete in milliseconds.
* A step starts when a call to waitForAsync() is made.
* @type {number}
*/
goog.testing.AsyncTestCase.prototype.stepTimeout = 1000;
/**
* How long to wait after a failed test before moving onto the next one.
* The purpose of this is to allow any pending async callbacks from the failing
* test to finish up and not cause the next test to fail.
* @type {number}
*/
goog.testing.AsyncTestCase.prototype.timeToSleepAfterFailure = 500;
/**
* Turn on extra logging to help debug failing async. tests.
* @type {boolean}
* @private
*/
goog.testing.AsyncTestCase.prototype.enableDebugLogs_ = false;
/**
* A reference to the original asserts.js assert_() function.
* @private
*/
goog.testing.AsyncTestCase.prototype.origAssert_;
/**
* A reference to the original asserts.js fail() function.
* @private
*/
goog.testing.AsyncTestCase.prototype.origFail_;
/**
* A reference to the original window.onerror function.
* @type {Function|undefined}
* @private
*/
goog.testing.AsyncTestCase.prototype.origOnError_;
/**
* The stage of the test we are currently on.
* @type {Function|undefined}}
* @private
*/
goog.testing.AsyncTestCase.prototype.curStepFunc_;
/**
* The name of the stage of the test we are currently on.
* @type {string}
* @private
*/
goog.testing.AsyncTestCase.prototype.curStepName_ = '';
/**
* The stage of the test we should run next.
* @type {Function|undefined}
* @private
*/
goog.testing.AsyncTestCase.prototype.nextStepFunc;
/**
* The name of the stage of the test we should run next.
* @type {string}
* @private
*/
goog.testing.AsyncTestCase.prototype.nextStepName_ = '';
/**
* The handle to the current setTimeout timer.
* @type {number}
* @private
*/
goog.testing.AsyncTestCase.prototype.timeoutHandle_ = 0;
/**
* Marks if the cleanUp() function has been called for the currently running
* test.
* @type {boolean}
* @private
*/
goog.testing.AsyncTestCase.prototype.cleanedUp_ = false;
/**
* The currently active test.
* @type {goog.testing.TestCase.Test|undefined}
* @protected
*/
goog.testing.AsyncTestCase.prototype.activeTest;
/**
* A flag to prevent recursive exception handling.
* @type {boolean}
* @private
*/
goog.testing.AsyncTestCase.prototype.inException_ = false;
/**
* Flag used to determine if we can move to the next step in the testing loop.
* @type {boolean}
* @private
*/
goog.testing.AsyncTestCase.prototype.isReady_ = true;
/**
* Number of signals to wait for before continuing testing when waitForSignals
* is used.
* @type {number}
* @private
*/
goog.testing.AsyncTestCase.prototype.expectedSignalCount_ = 0;
/**
* Number of signals received.
* @type {number}
* @private
*/
goog.testing.AsyncTestCase.prototype.receivedSignalCount_ = 0;
/**
* Flag that tells us if there is a function in the call stack that will make
* a call to pump_().
* @type {boolean}
* @private
*/
goog.testing.AsyncTestCase.prototype.returnWillPump_ = false;
/**
* The number of times we have thrown a ControlBreakingException so that we
* know not to complain in our window.onerror handler. In Webkit, window.onerror
* is not supported, and so this counter will keep going up but we won't care
* about it.
* @type {number}
* @private
*/
goog.testing.AsyncTestCase.prototype.numControlExceptionsExpected_ = 0;
/**
* The current step name.
* @return {!string} Step name.
* @protected
*/
goog.testing.AsyncTestCase.prototype.getCurrentStepName = function() {
return this.curStepName_;
};
/**
* Preferred way of creating an AsyncTestCase. Creates one and initializes it
* with the G_testRunner.
* @param {string=} opt_name A descriptive name for the test case.
* @return {!goog.testing.AsyncTestCase} The created AsyncTestCase.
*/
goog.testing.AsyncTestCase.createAndInstall = function(opt_name) {
var asyncTestCase = new goog.testing.AsyncTestCase(opt_name);
goog.testing.TestCase.initializeTestRunner(asyncTestCase);
return asyncTestCase;
};
/**
* Informs the testcase not to continue to the next step in the test cycle
* until continueTesting is called.
* @param {string=} opt_name A description of what we are waiting for.
*/
goog.testing.AsyncTestCase.prototype.waitForAsync = function(opt_name) {
this.isReady_ = false;
this.curStepName_ = opt_name || this.curStepName_;
// Reset the timer that tracks if the async test takes too long.
this.stopTimeoutTimer_();
this.startTimeoutTimer_();
};
/**
* Continue with the next step in the test cycle.
*/
goog.testing.AsyncTestCase.prototype.continueTesting = function() {
if (this.receivedSignalCount_ < this.expectedSignalCount_) {
var remaining = this.expectedSignalCount_ - this.receivedSignalCount_;
throw Error('Still waiting for ' + remaining + ' signals.');
}
this.endCurrentStep_();
};
/**
* Ends the current test step and queues the next test step to run.
* @private
*/
goog.testing.AsyncTestCase.prototype.endCurrentStep_ = function() {
if (!this.isReady_) {
// We are a potential entry point, so we pump.
this.isReady_ = true;
this.stopTimeoutTimer_();
// Run this in a setTimeout so that the caller has a chance to call
// waitForAsync() again before we continue.
this.timeout(goog.bind(this.pump_, this, null), 0);
}
};
/**
* Informs the testcase not to continue to the next step in the test cycle
* until signal is called the specified number of times. Within a test, this
* function behaves additively if called multiple times; the number of signals
* to wait for will be the sum of all expected number of signals this function
* was called with.
* @param {number} times The number of signals to receive before
* continuing testing.
* @param {string=} opt_name A description of what we are waiting for.
*/
goog.testing.AsyncTestCase.prototype.waitForSignals =
function(times, opt_name) {
this.expectedSignalCount_ += times;
if (this.receivedSignalCount_ < this.expectedSignalCount_) {
this.waitForAsync(opt_name);
}
};
/**
* Signals once to continue with the test. If this is the last signal that the
* test was waiting on, call continueTesting.
*/
goog.testing.AsyncTestCase.prototype.signal = function() {
if (++this.receivedSignalCount_ === this.expectedSignalCount_ &&
this.expectedSignalCount_ > 0) {
this.endCurrentStep_();
}
};
/**
* Handles an exception thrown by a test.
* @param {*=} opt_e The exception object associated with the failure
* or a string.
* @throws Always throws a ControlBreakingException.
*/
goog.testing.AsyncTestCase.prototype.doAsyncError = function(opt_e) {
// If we've caught an exception that we threw, then just pass it along. This
// can happen if doAsyncError() was called from a call to assert and then
// again by pump_().
if (opt_e && opt_e.isControlBreakingException) {
throw opt_e;
}
// Prevent another timeout error from triggering for this test step.
this.stopTimeoutTimer_();
// doError() uses test.name. Here, we create a dummy test and give it a more
// helpful name based on the step we're currently on.
var fakeTestObj = new goog.testing.TestCase.Test(this.curStepName_,
goog.nullFunction);
if (this.activeTest) {
fakeTestObj.name = this.activeTest.name + ' [' + fakeTestObj.name + ']';
}
if (this.activeTest) {
// Note: if the test has an error, and then tearDown has an error, they will
// both be reported.
this.doError(fakeTestObj, opt_e);
} else {
this.exceptionBeforeTest = opt_e;
}
// This is a potential entry point, so we pump. We also add in a bit of a
// delay to try and prevent any async behavior from the failed test from
// causing the next test to fail.
this.timeout(goog.bind(this.pump_, this, this.doAsyncErrorTearDown_),
this.timeToSleepAfterFailure);
// We just caught an exception, so we do not want the code above us on the
// stack to continue executing. If pump_ is in our call-stack, then it will
// batch together multiple errors, so we only increment the count if pump_ is
// not in the stack and let pump_ increment the count when it batches them.
if (!this.returnWillPump_) {
this.numControlExceptionsExpected_ += 1;
this.dbgLog_('doAsynError: numControlExceptionsExpected_ = ' +
this.numControlExceptionsExpected_ + ' and throwing exception.');
}
// Copy the error message to ControlBreakingException.
var message = '';
if (typeof opt_e == 'string') {
message = opt_e;
} else if (opt_e && opt_e.message) {
message = opt_e.message;
}
throw new goog.testing.AsyncTestCase.ControlBreakingException(message);
};
/**
* Sets up the test page and then waits until the test case has been marked
* as ready before executing the tests.
* @override
*/
goog.testing.AsyncTestCase.prototype.runTests = function() {
this.hookAssert_();
this.hookOnError_();
this.setNextStep_(this.doSetUpPage_, 'setUpPage');
// We are an entry point, so we pump.
this.pump_();
};
/**
* Starts the tests.
* @override
*/
goog.testing.AsyncTestCase.prototype.cycleTests = function() {
// We are an entry point, so we pump.
this.saveMessage('Start');
this.setNextStep_(this.doIteration_, 'doIteration');
this.pump_();
};
/**
* Finalizes the test case, called when the tests have finished executing.
* @override
*/
goog.testing.AsyncTestCase.prototype.finalize = function() {
this.unhookAll_();
this.setNextStep_(null, 'finalized');
goog.testing.AsyncTestCase.superClass_.finalize.call(this);
};
/**
* Enables verbose logging of what is happening inside of the AsyncTestCase.
*/
goog.testing.AsyncTestCase.prototype.enableDebugLogging = function() {
this.enableDebugLogs_ = true;
};
/**
* Logs the given debug message to the console (when enabled).
* @param {string} message The message to log.
* @private
*/
goog.testing.AsyncTestCase.prototype.dbgLog_ = function(message) {
if (this.enableDebugLogs_) {
this.log('AsyncTestCase - ' + message);
}
};
/**
* Wraps doAsyncError() for when we are sure that the test runner has no user
* code above it in the stack.
* @param {string|Error=} opt_e The exception object associated with the
* failure or a string.
* @private
*/
goog.testing.AsyncTestCase.prototype.doTopOfStackAsyncError_ =
function(opt_e) {
/** @preserveTry */
try {
this.doAsyncError(opt_e);
} catch (e) {
// We know that we are on the top of the stack, so there is no need to
// throw this exception in this case.
if (e.isControlBreakingException) {
this.numControlExceptionsExpected_ -= 1;
this.dbgLog_('doTopOfStackAsyncError_: numControlExceptionsExpected_ = ' +
this.numControlExceptionsExpected_ + ' and catching exception.');
} else {
throw e;
}
}
};
/**
* Calls the tearDown function, catching any errors, and then moves on to
* the next step in the testing cycle.
* @private
*/
goog.testing.AsyncTestCase.prototype.doAsyncErrorTearDown_ = function() {
if (this.inException_) {
// We get here if tearDown is throwing the error.
// Upon calling continueTesting, the inline function 'doAsyncError' (set
// below) is run.
this.endCurrentStep_();
} else {
this.inException_ = true;
this.isReady_ = true;
// The continue point is different depending on if the error happened in
// setUpPage() or in setUp()/test*()/tearDown().
var stepFuncAfterError = this.nextStepFunc_;
var stepNameAfterError = 'TestCase.execute (after error)';
if (this.activeTest) {
stepFuncAfterError = this.doIteration_;
stepNameAfterError = 'doIteration (after error)';
}
// We must set the next step before calling tearDown.
this.setNextStep_(function() {
this.inException_ = false;
// This is null when an error happens in setUpPage.
this.setNextStep_(stepFuncAfterError, stepNameAfterError);
}, 'doAsyncError');
// Call the test's tearDown().
if (!this.cleanedUp_) {
this.cleanedUp_ = true;
this.tearDown();
}
}
};
/**
* Replaces the asserts.js assert_() and fail() functions with a wrappers to
* catch the exceptions.
* @private
*/
goog.testing.AsyncTestCase.prototype.hookAssert_ = function() {
if (!this.origAssert_) {
this.origAssert_ = _assert;
this.origFail_ = fail;
var self = this;
_assert = function() {
/** @preserveTry */
try {
self.origAssert_.apply(this, arguments);
} catch (e) {
self.dbgLog_('Wrapping failed assert()');
self.doAsyncError(e);
}
};
fail = function() {
/** @preserveTry */
try {
self.origFail_.apply(this, arguments);
} catch (e) {
self.dbgLog_('Wrapping fail()');
self.doAsyncError(e);
}
};
}
};
/**
* Sets a window.onerror handler for catching exceptions that happen in async
* callbacks. Note that as of Safari 3.1, Safari does not support this.
* @private
*/
goog.testing.AsyncTestCase.prototype.hookOnError_ = function() {
if (!this.origOnError_) {
this.origOnError_ = window.onerror;
var self = this;
window.onerror = function(error, url, line) {
// Ignore exceptions that we threw on purpose.
var cbe =
goog.testing.AsyncTestCase.ControlBreakingException.TO_STRING;
if (String(error).indexOf(cbe) != -1 &&
self.numControlExceptionsExpected_) {
self.numControlExceptionsExpected_ -= 1;
self.dbgLog_('window.onerror: numControlExceptionsExpected_ = ' +
self.numControlExceptionsExpected_ + ' and ignoring exception. ' +
error);
// Tell the browser not to compain about the error.
return true;
} else {
self.dbgLog_('window.onerror caught exception.');
var message = error + '\nURL: ' + url + '\nLine: ' + line;
self.doTopOfStackAsyncError_(message);
// Tell the browser to complain about the error.
return false;
}
};
}
};
/**
* Unhooks window.onerror and _assert.
* @private
*/
goog.testing.AsyncTestCase.prototype.unhookAll_ = function() {
if (this.origOnError_) {
window.onerror = this.origOnError_;
this.origOnError_ = null;
_assert = this.origAssert_;
this.origAssert_ = null;
fail = this.origFail_;
this.origFail_ = null;
}
};
/**
* Enables the timeout timer. This timer fires unless continueTesting is
* called.
* @private
*/
goog.testing.AsyncTestCase.prototype.startTimeoutTimer_ = function() {
if (!this.timeoutHandle_ && this.stepTimeout > 0) {
this.timeoutHandle_ = this.timeout(goog.bind(function() {
this.dbgLog_('Timeout timer fired with id ' + this.timeoutHandle_);
this.timeoutHandle_ = 0;
this.doTopOfStackAsyncError_('Timed out while waiting for ' +
'continueTesting() to be called.');
}, this, null), this.stepTimeout);
this.dbgLog_('Started timeout timer with id ' + this.timeoutHandle_);
}
};
/**
* Disables the timeout timer.
* @private
*/
goog.testing.AsyncTestCase.prototype.stopTimeoutTimer_ = function() {
if (this.timeoutHandle_) {
this.dbgLog_('Clearing timeout timer with id ' + this.timeoutHandle_);
this.clearTimeout(this.timeoutHandle_);
this.timeoutHandle_ = 0;
}
};
/**
* Sets the next function to call in our sequence of async callbacks.
* @param {Function} func The function that executes the next step.
* @param {string} name A description of the next step.
* @private
*/
goog.testing.AsyncTestCase.prototype.setNextStep_ = function(func, name) {
this.nextStepFunc_ = func && goog.bind(func, this);
this.nextStepName_ = name;
};
/**
* Calls the given function, redirecting any exceptions to doAsyncError.
* @param {Function} func The function to call.
* @return {!goog.testing.AsyncTestCase.TopStackFuncResult_} Returns a
* TopStackFuncResult_.
* @private
*/
goog.testing.AsyncTestCase.prototype.callTopOfStackFunc_ = function(func) {
/** @preserveTry */
try {
func.call(this);
return {controlBreakingExceptionThrown: false, message: ''};
} catch (e) {
this.dbgLog_('Caught exception in callTopOfStackFunc_');
/** @preserveTry */
try {
this.doAsyncError(e);
return {controlBreakingExceptionThrown: false, message: ''};
} catch (e2) {
if (!e2.isControlBreakingException) {
throw e2;
}
return {controlBreakingExceptionThrown: true, message: e2.message};
}
}
};
/**
* Calls the next callback when the isReady_ flag is true.
* @param {Function=} opt_doFirst A function to call before pumping.
* @private
* @throws Throws a ControlBreakingException if there were any failing steps.
*/
goog.testing.AsyncTestCase.prototype.pump_ = function(opt_doFirst) {
// If this function is already above us in the call-stack, then we should
// return rather than pumping in order to minimize call-stack depth.
if (!this.returnWillPump_) {
this.setBatchTime(this.now());
this.returnWillPump_ = true;
var topFuncResult = {};
if (opt_doFirst) {
topFuncResult = this.callTopOfStackFunc_(opt_doFirst);
}
// Note: we don't check for this.running here because it is not set to true
// while executing setUpPage and tearDownPage.
// Also, if isReady_ is false, then one of two things will happen:
// 1. Our timeout callback will be called.
// 2. The tests will call continueTesting(), which will call pump_() again.
while (this.isReady_ && this.nextStepFunc_ &&
!topFuncResult.controlBreakingExceptionThrown) {
this.curStepFunc_ = this.nextStepFunc_;
this.curStepName_ = this.nextStepName_;
this.nextStepFunc_ = null;
this.nextStepName_ = '';
this.dbgLog_('Performing step: ' + this.curStepName_);
topFuncResult =
this.callTopOfStackFunc_(/** @type {Function} */(this.curStepFunc_));
// If the max run time is exceeded call this function again async so as
// not to block the browser.
var delta = this.now() - this.getBatchTime();
if (delta > goog.testing.TestCase.maxRunTime &&
!topFuncResult.controlBreakingExceptionThrown) {
this.saveMessage('Breaking async');
var self = this;
this.timeout(function() { self.pump_(); }, 100);
break;
}
}
this.returnWillPump_ = false;
} else if (opt_doFirst) {
opt_doFirst.call(this);
}
};
/**
* Sets up the test page and then waits untill the test case has been marked
* as ready before executing the tests.
* @private
*/
goog.testing.AsyncTestCase.prototype.doSetUpPage_ = function() {
this.setNextStep_(this.execute, 'TestCase.execute');
this.setUpPage();
};
/**
* Step 1: Move to the next test.
* @private
*/
goog.testing.AsyncTestCase.prototype.doIteration_ = function() {
this.expectedSignalCount_ = 0;
this.receivedSignalCount_ = 0;
this.activeTest = this.next();
if (this.activeTest && this.running) {
this.result_.runCount++;
// If this test should be marked as having failed, doIteration will go
// straight to the next test.
if (this.maybeFailTestEarly(this.activeTest)) {
this.setNextStep_(this.doIteration_, 'doIteration');
} else {
this.setNextStep_(this.doSetUp_, 'setUp');
}
} else {
// All tests done.
this.finalize();
}
};
/**
* Step 2: Call setUp().
* @private
*/
goog.testing.AsyncTestCase.prototype.doSetUp_ = function() {
this.log('Running test: ' + this.activeTest.name);
this.cleanedUp_ = false;
this.setNextStep_(this.doExecute_, this.activeTest.name);
this.setUp();
};
/**
* Step 3: Call test.execute().
* @private
*/
goog.testing.AsyncTestCase.prototype.doExecute_ = function() {
this.setNextStep_(this.doTearDown_, 'tearDown');
this.activeTest.execute();
};
/**
* Step 4: Call tearDown().
* @private
*/
goog.testing.AsyncTestCase.prototype.doTearDown_ = function() {
this.cleanedUp_ = true;
this.setNextStep_(this.doNext_, 'doNext');
this.tearDown();
};
/**
* Step 5: Call doSuccess()
* @private
*/
goog.testing.AsyncTestCase.prototype.doNext_ = function() {
this.setNextStep_(this.doIteration_, 'doIteration');
this.doSuccess(/** @type {goog.testing.TestCase.Test} */(this.activeTest));
};
@@ -0,0 +1,31 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2009 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<!--
Author: agrieve@google.com (Andrew Grieve)
This tests that the AsyncTestCase can handle asynchronous behaviour in:
setUpPage(),
setUp(),
test*(),
tearDown()
-->
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>
Closure Unit Tests - goog.testing.AsyncTestCase Asyncronous Tests
</title>
<script src="../base.js">
</script>
<script type="text/javascript">
goog.require('goog.testing.AsyncTestCaseAsyncTest');
</script>
</head>
<body>
</body>
</html>
@@ -0,0 +1,136 @@
// Copyright 2009 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.
goog.provide('goog.testing.AsyncTestCaseAsyncTest');
goog.setTestOnly('goog.testing.AsyncTestCaseAsyncTest');
goog.require('goog.testing.AsyncTestCase');
goog.require('goog.testing.jsunit');
// Has the setUp() function been called.
var setUpCalled = false;
// Has the current test function completed. This helps us to ensure that
// the next test is not started before the previous completed.
var curTestIsDone = true;
// Use an asynchronous test runner for our tests.
var asyncTestCase =
goog.testing.AsyncTestCase.createAndInstall(document.title);
/**
* Uses window.setTimeout() to perform asynchronous behaviour and uses
* asyncTestCase.waitForAsync() and asyncTestCase.continueTesting() to mark
* the beginning and end of it.
* @param {number} numAsyncCalls The number of asynchronous calls to make.
* @param {string} name The name of the current step.
*/
function doAsyncStuff(numAsyncCalls, name) {
if (numAsyncCalls > 0) {
curTestIsDone = false;
asyncTestCase.waitForAsync(
'doAsyncStuff-' + name + '(' + numAsyncCalls + ')');
window.setTimeout(function() {
doAsyncStuff(numAsyncCalls - 1, name);
}, 0);
} else {
curTestIsDone = true;
asyncTestCase.continueTesting();
}
}
function setUpPage() {
debug('setUpPage was called.');
doAsyncStuff(3, 'setUpPage');
}
function setUp() {
assertTrue(curTestIsDone);
doAsyncStuff(3, 'setUp');
}
function tearDown() {
assertTrue(curTestIsDone);
}
function test1() {
assertTrue(curTestIsDone);
doAsyncStuff(1, 'test1');
}
function test2_asyncContinueThenWait() {
var activeTest = asyncTestCase.activeTest_;
function async1() {
asyncTestCase.continueTesting();
asyncTestCase.waitForAsync('2');
window.setTimeout(async2, 0);
}
function async2() {
asyncTestCase.continueTesting();
assertEquals('Did not wait for inner waitForAsync',
activeTest,
asyncTestCase.activeTest_);
}
asyncTestCase.waitForAsync('1');
window.setTimeout(async1, 0);
}
function test3() {
assertTrue(curTestIsDone);
doAsyncStuff(2, 'test3');
}
function tearDownPage() {
debug('tearDownPage was called.');
assertTrue(curTestIsDone);
}
var callback = function() {
curTestIsDone = true;
asyncTestCase.signal();
};
var doAsyncSignals = function() {
curTestIsDone = false;
window.setTimeout(callback, 0);
};
function testSignalsReturn() {
doAsyncSignals();
doAsyncSignals();
doAsyncSignals();
asyncTestCase.waitForSignals(3);
}
function testSignalsMixedSyncAndAsync() {
asyncTestCase.signal();
doAsyncSignals();
doAsyncSignals();
asyncTestCase.waitForSignals(3);
}
function testSignalsMixedSyncAndAsyncMultipleWaits() {
asyncTestCase.signal();
doAsyncSignals();
asyncTestCase.waitForSignals(1);
doAsyncSignals();
asyncTestCase.waitForSignals(2);
}
function testSignalsCallContinueTestingBeforeFinishing() {
doAsyncSignals();
asyncTestCase.waitForSignals(2);
window.setTimeout(function() {
var thrown = assertThrows(function() {
asyncTestCase.continueTesting();
});
assertEquals('Still waiting for 1 signals.', thrown.message);
}, 0);
doAsyncSignals(); // To not timeout.
}
@@ -0,0 +1,33 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2009 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<!--
Author: agrieve@google.com (Andrew Grieve)
This tests that the AsyncTestCase can handle synchronous behaviour in:
setUpPage(),
setUp(),
test*(),
tearDown()
It is the same test as asynctestcase_async_test.html, except that it uses a mock
version of window.setTimeout() to eliminate all asynchronous calls.
-->
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>
Closure Unit Tests - goog.testing.AsyncTestCase Synchronous Tests
</title>
<script src="../base.js">
</script>
<script type="text/javascript">
goog.require('goog.testing.AsyncTestCaseSyncTest');
</script>
</head>
<body>
</body>
</html>
@@ -0,0 +1,111 @@
// Copyright 2009 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.
goog.provide('goog.testing.AsyncTestCaseSyncTest');
goog.setTestOnly('goog.testing.AsyncTestCaseSyncTest');
goog.require('goog.testing.AsyncTestCase');
goog.require('goog.testing.jsunit');
// Has the setUp() function been called.
var setUpCalled = false;
// Has the current test function completed. This helps us to ensure that the
// next test is not started before the previous completed.
var curTestIsDone = true;
// For restoring it later.
var oldTimeout = window.setTimeout;
// Use an asynchronous test runner for our tests.
var asyncTestCase =
goog.testing.AsyncTestCase.createAndInstall(document.title);
/**
* Uses window.setTimeout() to perform asynchronous behaviour and uses
* asyncTestCase.waitForAsync() and asyncTestCase.continueTesting() to mark
* the beginning and end of it.
* @param {number} numAsyncCalls The number of asynchronous calls to make.
* @param {string} name The name of the current step.
*/
function doAsyncStuff(numAsyncCalls, name) {
if (numAsyncCalls > 0) {
curTestIsDone = false;
asyncTestCase.waitForAsync(
'doAsyncStuff-' + name + '(' + numAsyncCalls + ')');
window.setTimeout(function() {
doAsyncStuff(numAsyncCalls - 1, name);
}, 0);
} else {
curTestIsDone = true;
asyncTestCase.continueTesting();
}
}
function setUpPage() {
debug('setUpPage was called.');
// Don't do anything asynchronously.
window.setTimeout = function(callback, time) {
callback();
};
doAsyncStuff(3, 'setUpPage');
}
function setUp() {
assertTrue(curTestIsDone);
doAsyncStuff(3, 'setUp');
}
function tearDown() {
assertTrue(curTestIsDone);
}
function test1() {
assertTrue(curTestIsDone);
doAsyncStuff(1, 'test1');
}
function test2() {
assertTrue(curTestIsDone);
doAsyncStuff(2, 'test2');
}
function test3() {
assertTrue(curTestIsDone);
doAsyncStuff(5, 'test3');
}
var callback = function() {
curTestIsDone = true;
asyncTestCase.signal();
};
var doAsyncSignals = function() {
curTestIsDone = false;
window.setTimeout(callback, 0);
};
function testSignalsReturn() {
doAsyncSignals();
doAsyncSignals();
doAsyncSignals();
asyncTestCase.waitForSignals(3);
}
function testSignalsCallContinueTestingBeforeFinishing() {
doAsyncSignals();
asyncTestCase.waitForSignals(2);
window.setTimeout(function() {
var thrown = assertThrows(function() {
asyncTestCase.continueTesting();
});
assertEquals('Still waiting for 1 signals.', thrown.message);
}, 0);
doAsyncSignals(); // To not timeout.
}
function tearDownPage() {
debug('tearDownPage was called.');
assertTrue(curTestIsDone);
window.setTimeout = oldTimeout;
}
@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2011 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>
Closure Unit Tests - goog.testing.asynctestcase
</title>
<script src="../base.js">
</script>
<script>
goog.require('goog.testing.AsyncTestCaseTest');
</script>
</head>
<body>
</body>
</html>
@@ -0,0 +1,52 @@
// Copyright 2011 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.
goog.provide('goog.testing.AsyncTestCaseTest');
goog.setTestOnly('goog.testing.AsyncTestCaseTest');
goog.require('goog.debug.Error');
goog.require('goog.testing.AsyncTestCase');
goog.require('goog.testing.asserts');
goog.require('goog.testing.jsunit');
function testControlBreakingExceptionThrown() {
var asyncTestCase = new goog.testing.AsyncTestCase();
// doAsyncError with no message.
try {
asyncTestCase.doAsyncError();
} catch (e) {
assertTrue(e.isControlBreakingException);
assertEquals('', e.message);
}
// doAsyncError with string.
var errorMessage1 = 'Error message 1';
try {
asyncTestCase.doAsyncError(errorMessage1);
} catch (e) {
assertTrue(e.isControlBreakingException);
assertEquals(errorMessage1, e.message);
}
// doAsyncError with error.
var errorMessage2 = 'Error message 2';
try {
var error = new goog.debug.Error(errorMessage2);
asyncTestCase.doAsyncError(error);
} catch (e) {
assertTrue(e.isControlBreakingException);
assertEquals(errorMessage2, e.message);
}
}
@@ -0,0 +1,96 @@
// Copyright 2010 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.
goog.provide('goog.testing.benchmark');
goog.setTestOnly('goog.testing.benchmark');
goog.require('goog.dom');
goog.require('goog.dom.TagName');
goog.require('goog.testing.PerformanceTable');
goog.require('goog.testing.PerformanceTimer');
goog.require('goog.testing.TestCase');
/**
* Run the benchmarks.
* @private
*/
goog.testing.benchmark.run_ = function() {
// Parse the 'times' query parameter if it's set.
var times = 200;
var search = window.location.search;
var timesMatch = search.match(/(?:\?|&)times=([^?&]+)/i);
if (timesMatch) {
times = Number(timesMatch[1]);
}
var prefix = 'benchmark';
// First, get the functions.
var testSources = goog.testing.TestCase.getGlobals();
var benchmarks = {};
var names = [];
for (var i = 0; i < testSources.length; i++) {
var testSource = testSources[i];
for (var name in testSource) {
if ((new RegExp('^' + prefix)).test(name)) {
var ref;
try {
ref = testSource[name];
} catch (ex) {
// NOTE(brenneman): When running tests from a file:// URL on Firefox
// 3.5 for Windows, any reference to window.sessionStorage raises
// an "Operation is not supported" exception. Ignore any exceptions
// raised by simply accessing global properties.
ref = undefined;
}
if (goog.isFunction(ref)) {
benchmarks[name] = ref;
names.push(name);
}
}
}
}
document.body.appendChild(
goog.dom.createTextNode(
'Running ' + names.length + ' benchmarks ' + times + ' times each.'));
document.body.appendChild(goog.dom.createElement(goog.dom.TagName.BR));
names.sort();
// Build a table and timer.
var performanceTimer = new goog.testing.PerformanceTimer(times);
performanceTimer.setDiscardOutliers(true);
var performanceTable = new goog.testing.PerformanceTable(document.body,
performanceTimer, 2);
// Next, run the benchmarks.
for (var i = 0; i < names.length; i++) {
performanceTable.run(benchmarks[names[i]], names[i]);
}
};
/**
* Onload handler that runs the benchmarks.
* @param {Event} e The event object.
*/
window.onload = function(e) {
goog.testing.benchmark.run_();
};
@@ -0,0 +1,691 @@
// Copyright 2009 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 Defines test classes for tests that can wait for conditions.
*
* Normal unit tests must complete their test logic within a single function
* execution. This is ideal for most tests, but makes it difficult to test
* routines that require real time to complete. The tests and TestCase in this
* file allow for tests that can wait until a condition is true before
* continuing execution.
*
* Each test has the typical three phases of execution: setUp, the test itself,
* and tearDown. During each phase, the test function may add wait conditions,
* which result in new test steps being added for that phase. All steps in a
* given phase must complete before moving on to the next phase. An error in
* any phase will stop that test and report the error to the test runner.
*
* This class should not be used where adequate mocks exist. Time-based routines
* should use the MockClock, which runs much faster and provides equivalent
* results. Continuation tests should be used for testing code that depends on
* browser behaviors that are difficult to mock. For example, testing code that
* relies on Iframe load events, event or layout code that requires a setTimeout
* to become valid, and other browser-dependent native object interactions for
* which mocks are insufficient.
*
* Sample usage:
*
* <pre>
* var testCase = new goog.testing.ContinuationTestCase();
* testCase.autoDiscoverTests();
*
* if (typeof G_testRunner != 'undefined') {
* G_testRunner.initialize(testCase);
* }
*
* function testWaiting() {
* var someVar = true;
* waitForTimeout(function() {
* assertTrue(someVar)
* }, 500);
* }
*
* function testWaitForEvent() {
* var et = goog.events.EventTarget();
* waitForEvent(et, 'test', function() {
* // Test step runs after the event fires.
* })
* et.dispatchEvent(et, 'test');
* }
*
* function testWaitForCondition() {
* var counter = 0;
*
* waitForCondition(function() {
* // This function is evaluated periodically until it returns true, or it
* // times out.
* return ++counter >= 3;
* }, function() {
* // This test step is run once the condition becomes true.
* assertEquals(3, counter);
* });
* }
* </pre>
*
* @author brenneman@google.com (Shawn Brenneman)
*/
goog.provide('goog.testing.ContinuationTestCase');
goog.provide('goog.testing.ContinuationTestCase.Step');
goog.provide('goog.testing.ContinuationTestCase.Test');
goog.require('goog.array');
goog.require('goog.events.EventHandler');
goog.require('goog.testing.TestCase');
goog.require('goog.testing.asserts');
/**
* Constructs a test case that supports tests with continuations. Test functions
* may issue "wait" commands that suspend the test temporarily and continue once
* the wait condition is met.
*
* @param {string=} opt_name Optional name for the test case.
* @constructor
* @extends {goog.testing.TestCase}
* @final
*/
goog.testing.ContinuationTestCase = function(opt_name) {
goog.testing.TestCase.call(this, opt_name);
/**
* An event handler for waiting on Closure or browser events during tests.
* @type {goog.events.EventHandler<!goog.testing.ContinuationTestCase>}
* @private
*/
this.handler_ = new goog.events.EventHandler(this);
};
goog.inherits(goog.testing.ContinuationTestCase, goog.testing.TestCase);
/**
* The default maximum time to wait for a single test step in milliseconds.
* @type {number}
*/
goog.testing.ContinuationTestCase.MAX_TIMEOUT = 1000;
/**
* Lock used to prevent multiple test steps from running recursively.
* @type {boolean}
* @private
*/
goog.testing.ContinuationTestCase.locked_ = false;
/**
* The current test being run.
* @type {goog.testing.ContinuationTestCase.Test}
* @private
*/
goog.testing.ContinuationTestCase.prototype.currentTest_ = null;
/**
* Enables or disables the wait functions in the global scope.
* @param {boolean} enable Whether the wait functions should be exported.
* @private
*/
goog.testing.ContinuationTestCase.prototype.enableWaitFunctions_ =
function(enable) {
if (enable) {
goog.exportSymbol('waitForCondition',
goog.bind(this.waitForCondition, this));
goog.exportSymbol('waitForEvent', goog.bind(this.waitForEvent, this));
goog.exportSymbol('waitForTimeout', goog.bind(this.waitForTimeout, this));
} else {
// Internet Explorer doesn't allow deletion of properties on the window.
goog.global['waitForCondition'] = undefined;
goog.global['waitForEvent'] = undefined;
goog.global['waitForTimeout'] = undefined;
}
};
/** @override */
goog.testing.ContinuationTestCase.prototype.runTests = function() {
this.enableWaitFunctions_(true);
goog.testing.ContinuationTestCase.superClass_.runTests.call(this);
};
/** @override */
goog.testing.ContinuationTestCase.prototype.finalize = function() {
this.enableWaitFunctions_(false);
goog.testing.ContinuationTestCase.superClass_.finalize.call(this);
};
/** @override */
goog.testing.ContinuationTestCase.prototype.cycleTests = function() {
// Get the next test in the queue.
if (!this.currentTest_) {
this.currentTest_ = this.createNextTest_();
}
// Run the next step of the current test, or exit if all tests are complete.
if (this.currentTest_) {
this.runNextStep_();
} else {
this.finalize();
}
};
/**
* Creates the next test in the queue.
* @return {goog.testing.ContinuationTestCase.Test} The next test to execute, or
* null if no pending tests remain.
* @private
*/
goog.testing.ContinuationTestCase.prototype.createNextTest_ = function() {
var test = this.next();
if (!test) {
return null;
}
var name = test.name;
goog.testing.TestCase.currentTestName = name;
this.result_.runCount++;
this.log('Running test: ' + name);
return new goog.testing.ContinuationTestCase.Test(
new goog.testing.TestCase.Test(name, this.setUp, this),
test,
new goog.testing.TestCase.Test(name, this.tearDown, this));
};
/**
* Cleans up a finished test and cycles to the next test.
* @private
*/
goog.testing.ContinuationTestCase.prototype.finishTest_ = function() {
var err = this.currentTest_.getError();
if (err) {
this.doError(this.currentTest_, err);
} else {
this.doSuccess(this.currentTest_);
}
goog.testing.TestCase.currentTestName = null;
this.currentTest_ = null;
this.locked_ = false;
this.handler_.removeAll();
this.timeout(goog.bind(this.cycleTests, this), 0);
};
/**
* Executes the next step in the current phase, advancing through each phase as
* all steps are completed.
* @private
*/
goog.testing.ContinuationTestCase.prototype.runNextStep_ = function() {
if (this.locked_) {
// Attempting to run a step before the previous step has finished. Try again
// after that step has released the lock.
return;
}
var phase = this.currentTest_.getCurrentPhase();
if (!phase || !phase.length) {
// No more steps for this test.
this.finishTest_();
return;
}
// Find the next step that is not in a wait state.
var stepIndex = goog.array.findIndex(phase, function(step) {
return !step.waiting;
});
if (stepIndex < 0) {
// All active steps are currently waiting. Return until one wakes up.
return;
}
this.locked_ = true;
var step = phase[stepIndex];
try {
step.execute();
// Remove the successfully completed step. If an error is thrown, all steps
// will be removed for this phase.
goog.array.removeAt(phase, stepIndex);
} catch (e) {
this.currentTest_.setError(e);
// An assertion has failed, or an exception was raised. Clear the current
// phase, whether it is setUp, test, or tearDown.
this.currentTest_.cancelCurrentPhase();
// Cancel the setUp and test phase no matter where the error occurred. The
// tearDown phase will still run if it has pending steps.
this.currentTest_.cancelTestPhase();
}
this.locked_ = false;
this.runNextStep_();
};
/**
* Creates a new test step that will run after a user-specified
* timeout. No guarantee is made on the execution order of the
* continuation, except for those provided by each browser's
* window.setTimeout. In particular, if two continuations are
* registered at the same time with very small delta for their
* durations, this class can not guarantee that the continuation with
* the smaller duration will be executed first.
* @param {Function} continuation The test function to invoke after the timeout.
* @param {number=} opt_duration The length of the timeout in milliseconds.
*/
goog.testing.ContinuationTestCase.prototype.waitForTimeout =
function(continuation, opt_duration) {
var step = this.addStep_(continuation);
step.setTimeout(goog.bind(this.handleComplete_, this, step),
opt_duration || 0);
};
/**
* Creates a new test step that will run after an event has fired. If the event
* does not fire within a reasonable timeout, the test will fail.
* @param {goog.events.EventTarget|EventTarget} eventTarget The target that will
* fire the event.
* @param {string} eventType The type of event to listen for.
* @param {Function} continuation The test function to invoke after the event
* fires.
*/
goog.testing.ContinuationTestCase.prototype.waitForEvent = function(
eventTarget,
eventType,
continuation) {
var step = this.addStep_(continuation);
var duration = goog.testing.ContinuationTestCase.MAX_TIMEOUT;
step.setTimeout(goog.bind(this.handleTimeout_, this, step, duration),
duration);
this.handler_.listenOnce(eventTarget,
eventType,
goog.bind(this.handleComplete_, this, step));
};
/**
* Creates a new test step which will run once a condition becomes true. The
* condition will be polled at a user-specified interval until it becomes true,
* or until a maximum timeout is reached.
* @param {Function} condition The condition to poll.
* @param {Function} continuation The test code to evaluate once the condition
* becomes true.
* @param {number=} opt_interval The polling interval in milliseconds.
* @param {number=} opt_maxTimeout The maximum amount of time to wait for the
* condition in milliseconds (defaults to 1000).
*/
goog.testing.ContinuationTestCase.prototype.waitForCondition = function(
condition,
continuation,
opt_interval,
opt_maxTimeout) {
var interval = opt_interval || 100;
var timeout = opt_maxTimeout || goog.testing.ContinuationTestCase.MAX_TIMEOUT;
var step = this.addStep_(continuation);
this.testCondition_(step, condition, goog.now(), interval, timeout);
};
/**
* Creates a new asynchronous test step which will be added to the current test
* phase.
* @param {Function} func The test function that will be executed for this step.
* @return {!goog.testing.ContinuationTestCase.Step} A new test step.
* @private
*/
goog.testing.ContinuationTestCase.prototype.addStep_ = function(func) {
if (!this.currentTest_) {
throw Error('Cannot add test steps outside of a running test.');
}
var step = new goog.testing.ContinuationTestCase.Step(
this.currentTest_.name,
func,
this.currentTest_.scope);
this.currentTest_.addStep(step);
return step;
};
/**
* Handles completion of a step's wait condition. Advances the test, allowing
* the step's test method to run.
* @param {goog.testing.ContinuationTestCase.Step} step The step that has
* finished waiting.
* @private
*/
goog.testing.ContinuationTestCase.prototype.handleComplete_ = function(step) {
step.clearTimeout();
step.waiting = false;
this.runNextStep_();
};
/**
* Handles the timeout event for a step that has exceeded the maximum time. This
* causes the current test to fail.
* @param {goog.testing.ContinuationTestCase.Step} step The timed-out step.
* @param {number} duration The length of the timeout in milliseconds.
* @private
*/
goog.testing.ContinuationTestCase.prototype.handleTimeout_ =
function(step, duration) {
step.ref = function() {
fail('Continuation timed out after ' + duration + 'ms.');
};
// Since the test is failing, cancel any other pending event listeners.
this.handler_.removeAll();
this.handleComplete_(step);
};
/**
* Tests a wait condition and executes the associated test step once the
* condition is true.
*
* If the condition does not become true before the maximum duration, the
* interval will stop and the test step will fail in the kill timer.
*
* @param {goog.testing.ContinuationTestCase.Step} step The waiting test step.
* @param {Function} condition The test condition.
* @param {number} startTime Time when the test step began waiting.
* @param {number} interval The duration in milliseconds to wait between tests.
* @param {number} timeout The maximum amount of time to wait for the condition
* to become true. Measured from the startTime in milliseconds.
* @private
*/
goog.testing.ContinuationTestCase.prototype.testCondition_ = function(
step,
condition,
startTime,
interval,
timeout) {
var duration = goog.now() - startTime;
if (condition()) {
this.handleComplete_(step);
} else if (duration < timeout) {
step.setTimeout(goog.bind(this.testCondition_,
this,
step,
condition,
startTime,
interval,
timeout),
interval);
} else {
this.handleTimeout_(step, duration);
}
};
/**
* Creates a continuation test case, which consists of multiple test steps that
* occur in several phases.
*
* The steps are distributed between setUp, test, and tearDown phases. During
* the execution of each step, 0 or more steps may be added to the current
* phase. Once all steps in a phase have completed, the next phase will be
* executed.
*
* If any errors occur (such as an assertion failure), the setUp and Test phases
* will be cancelled immediately. The tearDown phase will always start, but may
* be cancelled as well if it raises an error.
*
* @param {goog.testing.TestCase.Test} setUp A setUp test method to run before
* the main test phase.
* @param {goog.testing.TestCase.Test} test A test method to run.
* @param {goog.testing.TestCase.Test} tearDown A tearDown test method to run
* after the test method completes or fails.
* @constructor
* @extends {goog.testing.TestCase.Test}
* @final
*/
goog.testing.ContinuationTestCase.Test = function(setUp, test, tearDown) {
// This test container has a name, but no evaluation function or scope.
goog.testing.TestCase.Test.call(this, test.name, null, null);
/**
* The list of test steps to run during setUp.
* @type {Array<goog.testing.TestCase.Test>}
* @private
*/
this.setUp_ = [setUp];
/**
* The list of test steps to run for the actual test.
* @type {Array<goog.testing.TestCase.Test>}
* @private
*/
this.test_ = [test];
/**
* The list of test steps to run during the tearDown phase.
* @type {Array<goog.testing.TestCase.Test>}
* @private
*/
this.tearDown_ = [tearDown];
};
goog.inherits(goog.testing.ContinuationTestCase.Test,
goog.testing.TestCase.Test);
/**
* The first error encountered during the test run, if any.
* @type {Error}
* @private
*/
goog.testing.ContinuationTestCase.Test.prototype.error_ = null;
/**
* @return {Error} The first error to be raised during the test run or null if
* no errors occurred.
*/
goog.testing.ContinuationTestCase.Test.prototype.getError = function() {
return this.error_;
};
/**
* Sets an error for the test so it can be reported. Only the first error set
* during a test will be reported. Additional errors that occur in later test
* phases will be discarded.
* @param {Error} e An error.
*/
goog.testing.ContinuationTestCase.Test.prototype.setError = function(e) {
this.error_ = this.error_ || e;
};
/**
* @return {Array<goog.testing.TestCase.Test>} The current phase of steps
* being processed. Returns null if all steps have been completed.
*/
goog.testing.ContinuationTestCase.Test.prototype.getCurrentPhase = function() {
if (this.setUp_.length) {
return this.setUp_;
}
if (this.test_.length) {
return this.test_;
}
if (this.tearDown_.length) {
return this.tearDown_;
}
return null;
};
/**
* Adds a new test step to the end of the current phase. The new step will wait
* for a condition to be met before running, or will fail after a timeout.
* @param {goog.testing.ContinuationTestCase.Step} step The test step to add.
*/
goog.testing.ContinuationTestCase.Test.prototype.addStep = function(step) {
var phase = this.getCurrentPhase();
if (phase) {
phase.push(step);
} else {
throw Error('Attempted to add a step to a completed test.');
}
};
/**
* Cancels all remaining steps in the current phase. Called after an error in
* any phase occurs.
*/
goog.testing.ContinuationTestCase.Test.prototype.cancelCurrentPhase =
function() {
this.cancelPhase_(this.getCurrentPhase());
};
/**
* Skips the rest of the setUp and test phases, but leaves the tearDown phase to
* clean up.
*/
goog.testing.ContinuationTestCase.Test.prototype.cancelTestPhase = function() {
this.cancelPhase_(this.setUp_);
this.cancelPhase_(this.test_);
};
/**
* Clears a test phase and cancels any pending steps found.
* @param {Array<goog.testing.TestCase.Test>} phase A list of test steps.
* @private
*/
goog.testing.ContinuationTestCase.Test.prototype.cancelPhase_ =
function(phase) {
while (phase && phase.length) {
var step = phase.pop();
if (step instanceof goog.testing.ContinuationTestCase.Step) {
step.clearTimeout();
}
}
};
/**
* Constructs a single step in a larger continuation test. Each step is similar
* to a typical TestCase test, except it may wait for an event or timeout to
* occur before running the test function.
*
* @param {string} name The test name.
* @param {Function} ref The test function to run.
* @param {Object=} opt_scope The object context to run the test in.
* @constructor
* @extends {goog.testing.TestCase.Test}
* @final
*/
goog.testing.ContinuationTestCase.Step = function(name, ref, opt_scope) {
goog.testing.TestCase.Test.call(this, name, ref, opt_scope);
};
goog.inherits(goog.testing.ContinuationTestCase.Step,
goog.testing.TestCase.Test);
/**
* Whether the step is currently waiting for a condition to continue. All new
* steps begin in wait state.
* @type {boolean}
*/
goog.testing.ContinuationTestCase.Step.prototype.waiting = true;
/**
* A saved reference to window.clearTimeout so that MockClock or other overrides
* don't affect continuation timeouts.
* @type {Function}
* @private
*/
goog.testing.ContinuationTestCase.Step.protectedClearTimeout_ =
window.clearTimeout;
/**
* A saved reference to window.setTimeout so that MockClock or other overrides
* don't affect continuation timeouts.
* @type {Function}
* @private
*/
goog.testing.ContinuationTestCase.Step.protectedSetTimeout_ = window.setTimeout;
/**
* Key to this step's timeout. If the step is waiting for an event, the timeout
* will be used as a kill timer. If the step is waiting
* @type {number}
* @private
*/
goog.testing.ContinuationTestCase.Step.prototype.timeout_;
/**
* Starts a timeout for this step. Each step may have only one timeout active at
* a time.
* @param {Function} func The function to call after the timeout.
* @param {number} duration The number of milliseconds to wait before invoking
* the function.
*/
goog.testing.ContinuationTestCase.Step.prototype.setTimeout =
function(func, duration) {
this.clearTimeout();
var setTimeout = goog.testing.ContinuationTestCase.Step.protectedSetTimeout_;
this.timeout_ = setTimeout(func, duration);
};
/**
* Clears the current timeout if it is active.
*/
goog.testing.ContinuationTestCase.Step.prototype.clearTimeout = function() {
if (this.timeout_) {
var clear = goog.testing.ContinuationTestCase.Step.protectedClearTimeout_;
clear(this.timeout_);
delete this.timeout_;
}
};
@@ -0,0 +1,25 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2009 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<!--
Author: brenneman@google.com (Shawn Brenneman)
-->
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>
Closure Unit Tests - goog.testing.ContinuationTestCase
</title>
<script src="../base.js">
</script>
<script>
goog.require('goog.testing.ContinuationTestCaseTest');
</script>
</head>
<body>
</body>
</html>
@@ -0,0 +1,346 @@
// Copyright 2009 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.
goog.provide('goog.testing.ContinuationTestCaseTest');
goog.setTestOnly('goog.testing.ContinuationTestCaseTest');
goog.require('goog.events');
goog.require('goog.events.EventTarget');
goog.require('goog.testing.ContinuationTestCase');
goog.require('goog.testing.MockClock');
goog.require('goog.testing.PropertyReplacer');
goog.require('goog.testing.TestCase');
goog.require('goog.testing.jsunit');
/**
* @fileoverview This test file uses the ContinuationTestCase to test itself,
* which is a little confusing. It's also difficult to write a truly effective
* test, since testing a failure causes an actual failure in the test runner.
* All tests have been manually verified using a sophisticated combination of
* alerts and false assertions.
*/
var testCase = new goog.testing.ContinuationTestCase('Continuation Test Case');
testCase.autoDiscoverTests();
// Standalone Closure Test Runner.
if (typeof G_testRunner != 'undefined') {
G_testRunner.initialize(testCase);
}
var clock = new goog.testing.MockClock();
var count = 0;
var stubs = new goog.testing.PropertyReplacer();
function setUpPage() {
count = testCase.getCount();
}
/**
* Resets the mock clock. Includes a wait step to verify that setUp routines
* can contain continuations.
*/
function setUp() {
waitForTimeout(function() {
// Pointless assertion to verify that setUp methods can contain waits.
assertEquals(count, testCase.getCount());
}, 0);
clock.reset();
}
/**
* Uninstalls the mock clock if it was installed, and restores the Step timeout
* functions to the default window implementations.
*/
function tearDown() {
clock.uninstall();
stubs.reset();
waitForTimeout(function() {
// Pointless assertion to verify that tearDown methods can contain waits.
assertTrue(testCase.now() >= testCase.startTime_);
}, 0);
}
/**
* Installs the Mock Clock and replaces the Step timeouts with the mock
* implementations.
*/
function installMockClock() {
clock.install();
// Overwrite the "protected" setTimeout and clearTimeout with the versions
// replaced by MockClock. Normal tests should never do this, but we need to
// test the ContinuationTest itself.
stubs.set(goog.testing.ContinuationTestCase.Step, 'protectedClearTimeout_',
window.clearTimeout);
stubs.set(goog.testing.ContinuationTestCase.Step, 'protectedSetTimeout_',
window.setTimeout);
}
/**
* @return {goog.testing.ContinuationTestCase.Step} A generic step in a
* continuation test.
*/
function getSampleStep() {
return new goog.testing.ContinuationTestCase.Step('test', function() {});
}
/**
* @return {goog.testing.ContinuationTestCase.Test} A simple continuation test
* with generic setUp, test, and tearDown functions.
*/
function getSampleTest() {
var setupStep = new goog.testing.TestCase.Test('setup', function() {});
var testStep = new goog.testing.TestCase.Test('test', function() {});
var teardownStep = new goog.testing.TestCase.Test('teardown', function() {});
return new goog.testing.ContinuationTestCase.Test(setupStep,
testStep,
teardownStep);
}
function testStepWaiting() {
var step = getSampleStep();
assertTrue(step.waiting);
}
function testStepSetTimeout() {
installMockClock();
var step = getSampleStep();
var timeoutReached = false;
step.setTimeout(function() {timeoutReached = true}, 100);
clock.tick(50);
assertFalse(timeoutReached);
clock.tick(50);
assertTrue(timeoutReached);
}
function testStepClearTimeout() {
var step = new goog.testing.ContinuationTestCase.Step('test', function() {});
var timeoutReached = false;
step.setTimeout(function() {timeoutReached = true}, 100);
clock.tick(50);
assertFalse(timeoutReached);
step.clearTimeout();
clock.tick(50);
assertFalse(timeoutReached);
}
function testTestPhases() {
var test = getSampleTest();
assertEquals('setup', test.getCurrentPhase()[0].name);
test.cancelCurrentPhase();
assertEquals('test', test.getCurrentPhase()[0].name);
test.cancelCurrentPhase();
assertEquals('teardown', test.getCurrentPhase()[0].name);
test.cancelCurrentPhase();
assertNull(test.getCurrentPhase());
}
function testTestSetError() {
var test = getSampleTest();
var error1 = new Error('Oh noes!');
var error2 = new Error('B0rken.');
assertNull(test.getError());
test.setError(error1);
assertEquals(error1, test.getError());
test.setError(error2);
assertEquals('Once an error has been set, it should not be overwritten.',
error1, test.getError());
}
function testAddStep() {
var test = getSampleTest();
var step = getSampleStep();
// Try adding a step to each phase and then cancelling the phase.
for (var i = 0; i < 3; i++) {
assertEquals(1, test.getCurrentPhase().length);
test.addStep(step);
assertEquals(2, test.getCurrentPhase().length);
assertEquals(step, test.getCurrentPhase()[1]);
test.cancelCurrentPhase();
}
assertNull(test.getCurrentPhase());
}
function testCancelTestPhase() {
var test = getSampleTest();
test.cancelTestPhase();
assertEquals('teardown', test.getCurrentPhase()[0].name);
test = getSampleTest();
test.cancelCurrentPhase();
test.cancelTestPhase();
assertEquals('teardown', test.getCurrentPhase()[0].name);
test = getSampleTest();
test.cancelTestPhase();
test.cancelTestPhase();
assertEquals('teardown', test.getCurrentPhase()[0].name);
}
function testWaitForTimeout() {
var reachedA = false;
var reachedB = false;
var reachedC = false;
waitForTimeout(function a() {
reachedA = true;
assertTrue('A must be true at callback a.', reachedA);
assertFalse('B must be false at callback a.', reachedB);
assertFalse('C must be false at callback a.', reachedC);
}, 10);
waitForTimeout(function b() {
reachedB = true;
assertTrue('A must be true at callback b.', reachedA);
assertTrue('B must be true at callback b.', reachedB);
assertFalse('C must be false at callback b.', reachedC);
}, 20);
waitForTimeout(function c() {
reachedC = true;
assertTrue('A must be true at callback c.', reachedA);
assertTrue('B must be true at callback c.', reachedB);
assertTrue('C must be true at callback c.', reachedC);
}, 20);
assertFalse('a', reachedA);
assertFalse('b', reachedB);
assertFalse('c', reachedC);
}
function testWaitForEvent() {
var et = new goog.events.EventTarget();
var eventFired = false;
goog.events.listen(et, 'testPrefire', function() {
eventFired = true;
et.dispatchEvent('test');
});
waitForEvent(et, 'test', function() {
assertTrue(eventFired);
});
et.dispatchEvent('testPrefire');
}
function testWaitForCondition() {
var counter = 0;
waitForCondition(function() {
return ++counter >= 2;
}, function() {
assertEquals(2, counter);
}, 10, 200);
}
function testOutOfOrderWaits() {
var counter = 0;
// Note that if the delta between the timeout is too small, two
// continuation may be invoked at the same timer tick, using the
// registration order.
waitForTimeout(function() {assertEquals(3, ++counter);}, 200);
waitForTimeout(function() {assertEquals(1, ++counter);}, 0);
waitForTimeout(function() {assertEquals(2, ++counter);}, 100);
}
/*
* Any of the test functions below (except the condition check passed into
* waitForCondition) can raise an assertion successfully. Any level of nested
* test steps should be possible, in any configuration.
*/
var testObj;
function testCrazyNestedWaitFunction() {
testObj = {
lock: true,
et: new goog.events.EventTarget(),
steps: 0
};
waitForTimeout(handleTimeout, 10);
waitForEvent(testObj.et, 'test', handleEvent);
waitForCondition(condition, handleCondition, 1);
}
function handleTimeout() {
testObj.steps++;
assertEquals('handleTimeout should be called first.', 1, testObj.steps);
waitForTimeout(fireEvent, 10);
}
function fireEvent() {
testObj.steps++;
assertEquals('fireEvent should be called second.', 2, testObj.steps);
testObj.et.dispatchEvent('test');
}
function handleEvent() {
testObj.steps++;
assertEquals('handleEvent should be called third.', 3, testObj.steps);
testObj.lock = false;
}
function condition() {
return !testObj.lock;
}
function handleCondition() {
assertFalse(testObj.lock);
testObj.steps++;
assertEquals('handleCondition should be called last.', 4, testObj.steps);
}
@@ -0,0 +1,154 @@
// Copyright 2010 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 Defines DeferredTestCase class. By calling waitForDeferred(),
* tests in DeferredTestCase can wait for a Deferred object to complete its
* callbacks before continuing to the next test.
*
* Example Usage:
*
* var deferredTestCase = goog.testing.DeferredTestCase.createAndInstall();
* // Optionally, set a longer-than-usual step timeout.
* deferredTestCase.stepTimeout = 15 * 1000; // 15 seconds
*
* function testDeferredCallbacks() {
* var callbackTime = goog.now();
* var callbacks = new goog.async.Deferred();
* deferredTestCase.addWaitForAsync('Waiting for 1st callback', callbacks);
* callbacks.addCallback(
* function() {
* assertTrue(
* 'We\'re going back in time!', goog.now() >= callbackTime);
* callbackTime = goog.now();
* });
* deferredTestCase.addWaitForAsync('Waiting for 2nd callback', callbacks);
* callbacks.addCallback(
* function() {
* assertTrue(
* 'We\'re going back in time!', goog.now() >= callbackTime);
* callbackTime = goog.now();
* });
* deferredTestCase.addWaitForAsync('Waiting for last callback', callbacks);
* callbacks.addCallback(
* function() {
* assertTrue(
* 'We\'re going back in time!', goog.now() >= callbackTime);
* callbackTime = goog.now();
* });
*
* deferredTestCase.waitForDeferred(callbacks);
* }
*
* Note that DeferredTestCase still preserves the functionality of
* AsyncTestCase.
*
* @see.goog.async.Deferred
* @see goog.testing.AsyncTestCase
*/
goog.provide('goog.testing.DeferredTestCase');
goog.require('goog.testing.AsyncTestCase');
goog.require('goog.testing.TestCase');
/**
* A test case that can asynchronously wait on a Deferred object.
* @param {string=} opt_name A descriptive name for the test case.
* @constructor
* @extends {goog.testing.AsyncTestCase}
*/
goog.testing.DeferredTestCase = function(opt_name) {
goog.testing.AsyncTestCase.call(this, opt_name);
};
goog.inherits(goog.testing.DeferredTestCase, goog.testing.AsyncTestCase);
/**
* Preferred way of creating a DeferredTestCase. Creates one and initializes it
* with the G_testRunner.
* @param {string=} opt_name A descriptive name for the test case.
* @return {!goog.testing.DeferredTestCase} The created DeferredTestCase.
*/
goog.testing.DeferredTestCase.createAndInstall = function(opt_name) {
var deferredTestCase = new goog.testing.DeferredTestCase(opt_name);
goog.testing.TestCase.initializeTestRunner(deferredTestCase);
return deferredTestCase;
};
/**
* Handler for when the test produces an error.
* @param {Error|string} err The error object.
* @protected
* @throws Always throws a ControlBreakingException.
*/
goog.testing.DeferredTestCase.prototype.onError = function(err) {
this.doAsyncError(err);
};
/**
* Handler for when the test succeeds.
* @protected
*/
goog.testing.DeferredTestCase.prototype.onSuccess = function() {
this.continueTesting();
};
/**
* Adds a callback to update the wait message of this async test case. Using
* this method generously also helps to document the test flow.
* @param {string} msg The update wait status message.
* @param {goog.async.Deferred} d The deferred object to add the waitForAsync
* callback to.
* @see goog.testing.AsyncTestCase#waitForAsync
*/
goog.testing.DeferredTestCase.prototype.addWaitForAsync = function(msg, d) {
d.addCallback(goog.bind(this.waitForAsync, this, msg));
};
/**
* Wires up given Deferred object to the test case, then starts the
* goog.async.Deferred object's callback.
* @param {!string|goog.async.Deferred} a The wait status message or the
* deferred object to wait for.
* @param {goog.async.Deferred=} opt_b The deferred object to wait for.
*/
goog.testing.DeferredTestCase.prototype.waitForDeferred = function(a, opt_b) {
var waitMsg;
var deferred;
switch (arguments.length) {
case 1:
deferred = a;
waitMsg = null;
break;
case 2:
deferred = opt_b;
waitMsg = a;
break;
default: // Shouldn't be here in compiled mode
throw Error('Invalid number of arguments');
}
deferred.addCallbacks(this.onSuccess, this.onError, this);
if (!waitMsg) {
waitMsg = 'Waiting for deferred in ' + this.getCurrentStepName();
}
this.waitForAsync( /** @type {!string} */ (waitMsg));
deferred.callback(true);
};
@@ -0,0 +1,23 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2010 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<!--
-->
<head>
<title>
Closure Unit Tests - goog.testing.DeferredTestCase Asyncronous Tests
</title>
<script src="../base.js">
</script>
<script>
goog.require('goog.testing.DeferredTestCaseTest');
</script>
</head>
<body>
</body>
</html>
@@ -0,0 +1,137 @@
// Copyright 2010 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.
goog.provide('goog.testing.DeferredTestCaseTest');
goog.setTestOnly('goog.testing.DeferredTestCaseTest');
goog.require('goog.async.Deferred');
goog.require('goog.testing.DeferredTestCase');
goog.require('goog.testing.TestCase');
goog.require('goog.testing.TestRunner');
goog.require('goog.testing.jsunit');
goog.require('goog.testing.recordFunction');
var deferredTestCase =
goog.testing.DeferredTestCase.createAndInstall(document.title);
var testTestCase;
var runner;
// Optionally, set a longer-than-usual step timeout.
deferredTestCase.stepTimeout = 15 * 1000; // 15 seconds
// This is the sample code in deferredtestcase.js
function testDeferredCallbacks() {
var callbackTime = goog.now();
var callbacks = new goog.async.Deferred();
deferredTestCase.addWaitForAsync('Waiting for 1st callback', callbacks);
callbacks.addCallback(
function() {
assertTrue(
'We\'re going back in time!', goog.now() >= callbackTime);
callbackTime = goog.now();
});
deferredTestCase.addWaitForAsync('Waiting for 2nd callback', callbacks);
callbacks.addCallback(
function() {
assertTrue(
'We\'re going back in time!', goog.now() >= callbackTime);
callbackTime = goog.now();
});
deferredTestCase.addWaitForAsync('Waiting for last callback', callbacks);
callbacks.addCallback(
function() {
assertTrue(
'We\'re going back in time!', goog.now() >= callbackTime);
callbackTime = goog.now();
});
deferredTestCase.waitForDeferred(callbacks);
}
function createDeferredTestCase(d) {
testTestCase = new goog.testing.DeferredTestCase('Foobar TestCase');
testTestCase.add(new goog.testing.TestCase.Test(
'Foobar Test',
function() {
this.waitForDeferred(d);
},
testTestCase));
var testCompleteCallback = new goog.async.Deferred();
testTestCase.setCompletedCallback(
function() {
testCompleteCallback.callback(true);
});
// We're not going to use the runner to run the test, but we attach one
// here anyway because without a runner TestCase throws an exception in
// finalize().
var runner = new goog.testing.TestRunner();
runner.initialize(testTestCase);
return testCompleteCallback;
}
function testDeferredWait() {
var d = new goog.async.Deferred();
deferredTestCase.addWaitForAsync('Foobar', d);
d.addCallback(function() {
return goog.async.Deferred.succeed(true);
});
deferredTestCase.waitForDeferred(d);
}
function testNonAsync() {
assertTrue(true);
}
function testPassWithTestRunner() {
var d = new goog.async.Deferred();
d.addCallback(function() {
return goog.async.Deferred.succeed(true);
});
var testCompleteDeferred = createDeferredTestCase(d);
testTestCase.execute();
var deferredCallbackOnPass = new goog.async.Deferred();
deferredCallbackOnPass.addCallback(function() {
return testCompleteDeferred;
});
deferredCallbackOnPass.addCallback(function() {
assertTrue('Test case should have succeded.', testTestCase.isSuccess());
});
deferredTestCase.waitForDeferred(deferredCallbackOnPass);
}
function testFailWithTestRunner() {
var d = new goog.async.Deferred();
d.addCallback(function() {
return goog.async.Deferred.fail(true);
});
var testCompleteDeferred = createDeferredTestCase(d);
// Mock doAsyncError to instead let the test completes successfully,
// but record the failure. The test works as is because the failing
// deferred is not actually asynchronous.
var mockDoAsyncError = goog.testing.recordFunction(function() {
testTestCase.continueTesting();
});
testTestCase.doAsyncError = mockDoAsyncError;
testTestCase.execute();
assertEquals(1, mockDoAsyncError.getCallCount());
}
@@ -0,0 +1,624 @@
// 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 Testing utilities for DOM related tests.
*
* @author robbyw@google.com (Robby Walker)
*/
goog.provide('goog.testing.dom');
goog.require('goog.array');
goog.require('goog.asserts');
goog.require('goog.dom');
goog.require('goog.dom.NodeIterator');
goog.require('goog.dom.NodeType');
goog.require('goog.dom.TagIterator');
goog.require('goog.dom.TagName');
goog.require('goog.dom.classlist');
goog.require('goog.iter');
goog.require('goog.object');
goog.require('goog.string');
goog.require('goog.style');
goog.require('goog.testing.asserts');
goog.require('goog.userAgent');
/**
* @return {!Node} A DIV node with a unique ID identifying the
* {@code END_TAG_MARKER_}.
* @private
*/
goog.testing.dom.createEndTagMarker_ = function() {
var marker = goog.dom.createElement(goog.dom.TagName.DIV);
marker.id = goog.getUid(marker);
return marker;
};
/**
* A unique object to use as an end tag marker.
* @private {!Node}
* @const
*/
goog.testing.dom.END_TAG_MARKER_ = goog.testing.dom.createEndTagMarker_();
/**
* Tests if the given iterator over nodes matches the given Array of node
* descriptors. Throws an error if any match fails.
* @param {goog.iter.Iterator} it An iterator over nodes.
* @param {Array<Node|number|string>} array Array of node descriptors to match
* against. Node descriptors can be any of the following:
* Node: Test if the two nodes are equal.
* number: Test node.nodeType == number.
* string starting with '#': Match the node's id with the text
* after "#".
* other string: Match the text node's contents.
*/
goog.testing.dom.assertNodesMatch = function(it, array) {
var i = 0;
goog.iter.forEach(it, function(node) {
if (array.length <= i) {
fail('Got more nodes than expected: ' + goog.testing.dom.describeNode_(
node));
}
var expected = array[i];
if (goog.dom.isNodeLike(expected)) {
assertEquals('Nodes should match at position ' + i, expected, node);
} else if (goog.isNumber(expected)) {
assertEquals('Node types should match at position ' + i, expected,
node.nodeType);
} else if (expected.charAt(0) == '#') {
assertEquals('Expected element at position ' + i,
goog.dom.NodeType.ELEMENT, node.nodeType);
var expectedId = expected.substr(1);
assertEquals('IDs should match at position ' + i,
expectedId, node.id);
} else {
assertEquals('Expected text node at position ' + i,
goog.dom.NodeType.TEXT, node.nodeType);
assertEquals('Node contents should match at position ' + i,
expected, node.nodeValue);
}
i++;
});
assertEquals('Used entire match array', array.length, i);
};
/**
* Exposes a node as a string.
* @param {Node} node A node.
* @return {string} A string representation of the node.
*/
goog.testing.dom.exposeNode = function(node) {
return (node.tagName || node.nodeValue) + (node.id ? '#' + node.id : '') +
':"' + (node.innerHTML || '') + '"';
};
/**
* Exposes the nodes of a range wrapper as a string.
* @param {goog.dom.AbstractRange} range A range.
* @return {string} A string representation of the range.
*/
goog.testing.dom.exposeRange = function(range) {
// This is deliberately not implemented as
// goog.dom.AbstractRange.prototype.toString, because it is non-authoritative.
// Two equivalent ranges may have very different exposeRange values, and
// two different ranges may have equal exposeRange values.
// (The mapping of ranges to DOM nodes/offsets is a many-to-many mapping).
if (!range) {
return 'null';
}
return goog.testing.dom.exposeNode(range.getStartNode()) + ':' +
range.getStartOffset() + ' to ' +
goog.testing.dom.exposeNode(range.getEndNode()) + ':' +
range.getEndOffset();
};
/**
* Determines if the current user agent matches the specified string. Returns
* false if the string does specify at least one user agent but does not match
* the running agent.
* @param {string} userAgents Space delimited string of user agents.
* @return {boolean} Whether the user agent was matched. Also true if no user
* agent was listed in the expectation string.
* @private
*/
goog.testing.dom.checkUserAgents_ = function(userAgents) {
if (goog.string.startsWith(userAgents, '!')) {
if (goog.string.contains(userAgents, ' ')) {
throw new Error('Only a single negative user agent may be specified');
}
return !goog.userAgent[userAgents.substr(1)];
}
var agents = userAgents.split(' ');
var hasUserAgent = false;
for (var i = 0, len = agents.length; i < len; i++) {
var cls = agents[i];
if (cls in goog.userAgent) {
hasUserAgent = true;
if (goog.userAgent[cls]) {
return true;
}
}
}
// If we got here, there was a user agent listed but we didn't match it.
return !hasUserAgent;
};
/**
* Map function that converts end tags to a specific object.
* @param {Node} node The node to map.
* @param {undefined} ignore Always undefined.
* @param {!goog.iter.Iterator<Node>} iterator The iterator.
* @return {Node} The resulting iteration item.
* @private
*/
goog.testing.dom.endTagMap_ = function(node, ignore, iterator) {
return iterator.isEndTag() ? goog.testing.dom.END_TAG_MARKER_ : node;
};
/**
* Check if the given node is important. A node is important if it is a
* non-empty text node, a non-annotated element, or an element annotated to
* match on this user agent.
* @param {Node} node The node to test.
* @return {boolean} Whether this node should be included for iteration.
* @private
*/
goog.testing.dom.nodeFilter_ = function(node) {
if (node.nodeType == goog.dom.NodeType.TEXT) {
// If a node is part of a string of text nodes and it has spaces in it,
// we allow it since it's going to affect the merging of nodes done below.
if (goog.string.isBreakingWhitespace(node.nodeValue) &&
(!node.previousSibling ||
node.previousSibling.nodeType != goog.dom.NodeType.TEXT) &&
(!node.nextSibling ||
node.nextSibling.nodeType != goog.dom.NodeType.TEXT)) {
return false;
}
// Allow optional text to be specified as [[BROWSER1 BROWSER2]]Text
var match = node.nodeValue.match(/^\[\[(.+)\]\]/);
if (match) {
return goog.testing.dom.checkUserAgents_(match[1]);
}
} else if (node.className) {
return goog.testing.dom.checkUserAgents_(node.className);
}
return true;
};
/**
* Determines the text to match from the given node, removing browser
* specification strings.
* @param {Node} node The node expected to match.
* @return {string} The text, stripped of browser specification strings.
* @private
*/
goog.testing.dom.getExpectedText_ = function(node) {
// Strip off the browser specifications.
return node.nodeValue.match(/^(\[\[.+\]\])?(.*)/)[2];
};
/**
* Describes the given node.
* @param {Node} node The node to describe.
* @return {string} A description of the node.
* @private
*/
goog.testing.dom.describeNode_ = function(node) {
if (node.nodeType == goog.dom.NodeType.TEXT) {
return '[Text: ' + node.nodeValue + ']';
} else {
return '<' + node.tagName + (node.id ? ' #' + node.id : '') + ' .../>';
}
};
/**
* Assert that the html in {@code actual} is substantially similar to
* htmlPattern. This method tests for the same set of styles, for the same
* order of nodes, and the presence of attributes. Breaking whitespace nodes
* are ignored. Elements can be
* annotated with classnames corresponding to keys in goog.userAgent and will be
* expected to show up in that user agent and expected not to show up in
* others.
* @param {string} htmlPattern The pattern to match.
* @param {!Node} actual The element to check: its contents are matched
* against the HTML pattern.
* @param {boolean=} opt_strictAttributes If false, attributes that appear in
* htmlPattern must be in actual, but actual can have attributes not
* present in htmlPattern. If true, htmlPattern and actual must have the
* same set of attributes. Default is false.
*/
goog.testing.dom.assertHtmlContentsMatch = function(htmlPattern, actual,
opt_strictAttributes) {
var div = goog.dom.createDom(goog.dom.TagName.DIV);
div.innerHTML = htmlPattern;
var errorSuffix = '\nExpected\n' + htmlPattern + '\nActual\n' +
actual.innerHTML;
var actualIt = goog.iter.filter(
goog.iter.map(new goog.dom.TagIterator(actual),
goog.testing.dom.endTagMap_),
goog.testing.dom.nodeFilter_);
var expectedIt = goog.iter.filter(new goog.dom.NodeIterator(div),
goog.testing.dom.nodeFilter_);
var actualNode;
var preIterated = false;
var advanceActualNode = function() {
// If the iterator has already been advanced, don't advance it again.
if (!preIterated) {
actualNode = /** @type {Node} */ (goog.iter.nextOrValue(actualIt, null));
}
preIterated = false;
// Advance the iterator so long as it is return end tags.
while (actualNode == goog.testing.dom.END_TAG_MARKER_) {
actualNode = /** @type {Node} */ (goog.iter.nextOrValue(actualIt, null));
}
};
// HACK(brenneman): IE has unique ideas about whitespace handling when setting
// innerHTML. This results in elision of leading whitespace in the expected
// nodes where doing so doesn't affect visible rendering. As a workaround, we
// remove the leading whitespace in the actual nodes where necessary.
//
// The collapsible variable tracks whether we should collapse the whitespace
// in the next Text node we encounter.
var IE_TEXT_COLLAPSE =
goog.userAgent.IE && !goog.userAgent.isVersionOrHigher('9');
var collapsible = true;
var number = 0;
goog.iter.forEach(expectedIt, function(expectedNode) {
expectedNode = /** @type {Node} */ (expectedNode);
advanceActualNode();
assertNotNull('Finished actual HTML before finishing expected HTML at ' +
'node number ' + number + ': ' +
goog.testing.dom.describeNode_(expectedNode) + errorSuffix,
actualNode);
// Do no processing for expectedNode == div.
if (expectedNode == div) {
return;
}
assertEquals('Should have the same node type, got ' +
goog.testing.dom.describeNode_(actualNode) + ' but expected ' +
goog.testing.dom.describeNode_(expectedNode) + '.' + errorSuffix,
expectedNode.nodeType, actualNode.nodeType);
if (expectedNode.nodeType == goog.dom.NodeType.ELEMENT) {
var expectedElem = goog.asserts.assertElement(expectedNode);
var actualElem = goog.asserts.assertElement(actualNode);
assertEquals('Tag names should match' + errorSuffix,
expectedElem.tagName, actualElem.tagName);
assertObjectEquals('Should have same styles' + errorSuffix,
goog.style.parseStyleAttribute(expectedElem.style.cssText),
goog.style.parseStyleAttribute(actualElem.style.cssText));
goog.testing.dom.assertAttributesEqual_(errorSuffix, expectedElem,
actualElem, !!opt_strictAttributes);
if (IE_TEXT_COLLAPSE &&
goog.style.getCascadedStyle(actualElem, 'display') != 'inline') {
// Text may be collapsed after any non-inline element.
collapsible = true;
}
} else {
// Concatenate text nodes until we reach a non text node.
var actualText = actualNode.nodeValue;
preIterated = true;
while ((actualNode = /** @type {Node} */
(goog.iter.nextOrValue(actualIt, null))) &&
actualNode.nodeType == goog.dom.NodeType.TEXT) {
actualText += actualNode.nodeValue;
}
if (IE_TEXT_COLLAPSE) {
// Collapse the leading whitespace, unless the string consists entirely
// of whitespace.
if (collapsible && !goog.string.isEmptyOrWhitespace(actualText)) {
actualText = goog.string.trimLeft(actualText);
}
// Prepare to collapse whitespace in the next Text node if this one does
// not end in a whitespace character.
collapsible = /\s$/.test(actualText);
}
var expectedText = goog.testing.dom.getExpectedText_(expectedNode);
if ((actualText && !goog.string.isBreakingWhitespace(actualText)) ||
(expectedText && !goog.string.isBreakingWhitespace(expectedText))) {
var normalizedActual = actualText.replace(/\s+/g, ' ');
var normalizedExpected = expectedText.replace(/\s+/g, ' ');
assertEquals('Text should match' + errorSuffix, normalizedExpected,
normalizedActual);
}
}
number++;
});
advanceActualNode();
assertNull('Finished expected HTML before finishing actual HTML' +
errorSuffix, goog.iter.nextOrValue(actualIt, null));
};
/**
* Assert that the html in {@code actual} is substantially similar to
* htmlPattern. This method tests for the same set of styles, and for the same
* order of nodes. Breaking whitespace nodes are ignored. Elements can be
* annotated with classnames corresponding to keys in goog.userAgent and will be
* expected to show up in that user agent and expected not to show up in
* others.
* @param {string} htmlPattern The pattern to match.
* @param {string} actual The html to check.
*/
goog.testing.dom.assertHtmlMatches = function(htmlPattern, actual) {
var div = goog.dom.createDom(goog.dom.TagName.DIV);
div.innerHTML = actual;
goog.testing.dom.assertHtmlContentsMatch(htmlPattern, div);
};
/**
* Finds the first text node descendant of root with the given content. Note
* that this operates on a text node level, so if text nodes get split this
* may not match the user visible text. Using normalize() may help here.
* @param {string|RegExp} textOrRegexp The text to find, or a regular
* expression to find a match of.
* @param {Element} root The element to search in.
* @return {Node} The first text node that matches, or null if none is found.
*/
goog.testing.dom.findTextNode = function(textOrRegexp, root) {
var it = new goog.dom.NodeIterator(root);
var ret = goog.iter.nextOrValue(goog.iter.filter(it, function(node) {
if (node.nodeType == goog.dom.NodeType.TEXT) {
if (goog.isString(textOrRegexp)) {
return node.nodeValue == textOrRegexp;
} else {
return !!node.nodeValue.match(textOrRegexp);
}
} else {
return false;
}
}), null);
return /** @type {Node} */ (ret);
};
/**
* Assert the end points of a range.
*
* Notice that "Are two ranges visually identical?" and "Do two ranges have
* the same endpoint?" are independent questions. Two visually identical ranges
* may have different endpoints. And two ranges with the same endpoints may
* be visually different.
*
* @param {Node} start The expected start node.
* @param {number} startOffset The expected start offset.
* @param {Node} end The expected end node.
* @param {number} endOffset The expected end offset.
* @param {goog.dom.AbstractRange} range The actual range.
*/
goog.testing.dom.assertRangeEquals = function(start, startOffset, end,
endOffset, range) {
assertEquals('Unexpected start node', start, range.getStartNode());
assertEquals('Unexpected end node', end, range.getEndNode());
assertEquals('Unexpected start offset', startOffset, range.getStartOffset());
assertEquals('Unexpected end offset', endOffset, range.getEndOffset());
};
/**
* Gets the value of a DOM attribute in deterministic way.
* @param {!Node} node A node.
* @param {string} name Attribute name.
* @return {*} Attribute value.
* @private
*/
goog.testing.dom.getAttributeValue_ = function(node, name) {
// These hacks avoid nondetermistic results in the following cases:
// IE7: document.createElement('input').height returns a random number.
// FF3: getAttribute('disabled') returns different value for <div disabled="">
// and <div disabled="disabled">
// WebKit: Two radio buttons with the same name can't be checked at the same
// time, even if only one of them is in the document.
if (goog.userAgent.WEBKIT && node.tagName == 'INPUT' &&
node['type'] == 'radio' && name == 'checked') {
return false;
}
return goog.isDef(node[name]) &&
typeof node.getAttribute(name) != typeof node[name] ?
node[name] : node.getAttribute(name);
};
/**
* Assert that the attributes of two Nodes are the same (ignoring any
* instances of the style attribute).
* @param {string} errorSuffix String to add to end of error messages.
* @param {!Element} expectedElem The element whose attributes we are expecting.
* @param {!Element} actualElem The element with the actual attributes.
* @param {boolean} strictAttributes If false, attributes that appear in
* expectedNode must also be in actualNode, but actualNode can have
* attributes not present in expectedNode. If true, expectedNode and
* actualNode must have the same set of attributes.
* @private
*/
goog.testing.dom.assertAttributesEqual_ = function(errorSuffix,
expectedElem, actualElem, strictAttributes) {
if (strictAttributes) {
goog.testing.dom.compareClassAttribute_(expectedElem, actualElem);
}
var expectedAttributes = expectedElem.attributes;
var actualAttributes = actualElem.attributes;
for (var i = 0, len = expectedAttributes.length; i < len; i++) {
var expectedName = expectedAttributes[i].name;
var expectedValue = goog.testing.dom.getAttributeValue_(expectedElem,
expectedName);
var actualAttribute = actualAttributes[expectedName];
var actualValue = goog.testing.dom.getAttributeValue_(actualElem,
expectedName);
// IE enumerates attribute names in the expected node that are not present,
// causing an undefined actualAttribute.
if (!expectedValue && !actualValue) {
continue;
}
if (expectedName == 'id' && goog.userAgent.IE) {
goog.testing.dom.compareIdAttributeForIe_(
/** @type {string} */ (expectedValue), actualAttribute,
strictAttributes, errorSuffix);
continue;
}
if (goog.testing.dom.ignoreAttribute_(expectedName)) {
continue;
}
assertNotUndefined('Expected to find attribute with name ' +
expectedName + ', in element ' +
goog.testing.dom.describeNode_(actualElem) + errorSuffix,
actualAttribute);
assertEquals('Expected attribute ' + expectedName +
' has a different value ' + errorSuffix,
expectedValue,
goog.testing.dom.getAttributeValue_(actualElem, actualAttribute.name));
}
if (strictAttributes) {
for (i = 0; i < actualAttributes.length; i++) {
var actualName = actualAttributes[i].name;
var actualAttribute = actualAttributes.getNamedItem(actualName);
if (!actualAttribute || goog.testing.dom.ignoreAttribute_(actualName)) {
continue;
}
assertNotUndefined('Unexpected attribute with name ' +
actualName + ' in element ' +
goog.testing.dom.describeNode_(actualElem) + errorSuffix,
expectedAttributes[actualName]);
}
}
};
/**
* Assert the class attribute of actualElem is the same as the one in
* expectedElem, ignoring classes that are useragents.
* @param {!Element} expectedElem The DOM element whose class we expect.
* @param {!Element} actualElem The DOM element with the actual class.
* @private
*/
goog.testing.dom.compareClassAttribute_ = function(expectedElem,
actualElem) {
var classes = goog.dom.classlist.get(expectedElem);
var expectedClasses = [];
for (var i = 0, len = classes.length; i < len; i++) {
if (!(classes[i] in goog.userAgent)) {
expectedClasses.push(classes[i]);
}
}
expectedClasses.sort();
var actualClasses = goog.array.toArray(goog.dom.classlist.get(actualElem));
actualClasses.sort();
assertArrayEquals(
'Expected class was: ' + expectedClasses.join(' ') +
', but actual class was: ' + actualElem.className,
expectedClasses, actualClasses);
};
/**
* Set of attributes IE adds to elements randomly.
* @type {Object}
* @private
*/
goog.testing.dom.BAD_IE_ATTRIBUTES_ = goog.object.createSet(
'methods', 'CHECKED', 'dataFld', 'dataFormatAs', 'dataSrc');
/**
* Whether to ignore the attribute.
* @param {string} name Name of the attribute.
* @return {boolean} True if the attribute should be ignored.
* @private
*/
goog.testing.dom.ignoreAttribute_ = function(name) {
if (name == 'style' || name == 'class') {
return true;
}
return goog.userAgent.IE && goog.testing.dom.BAD_IE_ATTRIBUTES_[name];
};
/**
* Compare id attributes for IE. In IE, if an element lacks an id attribute
* in the original HTML, the element object will still have such an attribute,
* but its value will be the empty string.
* @param {string} expectedValue The expected value of the id attribute.
* @param {Attr} actualAttribute The actual id attribute.
* @param {boolean} strictAttributes Whether strict attribute checking should be
* done.
* @param {string} errorSuffix String to append to error messages.
* @private
*/
goog.testing.dom.compareIdAttributeForIe_ = function(expectedValue,
actualAttribute, strictAttributes, errorSuffix) {
if (expectedValue === '') {
if (strictAttributes) {
assertTrue('Unexpected attribute with name id in element ' +
errorSuffix, actualAttribute.value == '');
}
} else {
assertNotUndefined('Expected to find attribute with name id, in element ' +
errorSuffix, actualAttribute);
assertNotEquals('Expected to find attribute with name id, in element ' +
errorSuffix, '', actualAttribute.value);
assertEquals('Expected attribute has a different value ' + errorSuffix,
expectedValue, actualAttribute.value);
}
};
@@ -0,0 +1,28 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<!--
Copyright 2008 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<head>
<!--
This test has not yet been updated to run on IE8. See http://b/hotlist?id=36311
-->
<!--meta http-equiv="X-UA-Compatible" content="IE=edge"-->
<meta http-equiv="X-UA-Compatible" content="IE=EmulateIE7" />
<title>
Closure Unit Tests - goog.testing.dom
</title>
<script src="../base.js">
</script>
<script>
goog.require('goog.testing.domTest');
</script>
</head>
<body>
<div id="root">
</div>
</body>
</html>
@@ -0,0 +1,434 @@
// 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.
goog.provide('goog.testing.domTest');
goog.setTestOnly('goog.testing.domTest');
goog.require('goog.dom');
goog.require('goog.dom.TagName');
goog.require('goog.testing.dom');
goog.require('goog.testing.jsunit');
goog.require('goog.userAgent');
var root;
function setUpPage() {
root = goog.dom.getElement('root');
}
function setUp() {
root.innerHTML = '';
}
function testFindNode() {
// Test the easiest case.
root.innerHTML = 'a<br>b';
assertEquals(goog.testing.dom.findTextNode('a', root), root.firstChild);
assertEquals(goog.testing.dom.findTextNode('b', root), root.lastChild);
assertNull(goog.testing.dom.findTextNode('c', root));
}
function testFindNodeDuplicate() {
// Test duplicate.
root.innerHTML = 'c<br>c';
assertEquals('Should return first duplicate',
goog.testing.dom.findTextNode('c', root), root.firstChild);
}
function findNodeWithHierarchy() {
// Test a more complicated hierarchy.
root.innerHTML = '<div>a<p>b<span>c</span>d</p>e</div>';
assertEquals(goog.dom.TagName.DIV,
goog.testing.dom.findTextNode('a', root).parentNode.tagName);
assertEquals(goog.dom.TagName.P,
goog.testing.dom.findTextNode('b', root).parentNode.tagName);
assertEquals(goog.dom.TagName.SPAN,
goog.testing.dom.findTextNode('c', root).parentNode.tagName);
assertEquals(goog.dom.TagName.P,
goog.testing.dom.findTextNode('d', root).parentNode.tagName);
assertEquals(goog.dom.TagName.DIV,
goog.testing.dom.findTextNode('e', root).parentNode.tagName);
}
function setUpAssertHtmlMatches() {
var tag1, tag2;
if (goog.userAgent.IE) {
tag1 = goog.dom.TagName.DIV;
} else if (goog.userAgent.WEBKIT) {
tag1 = goog.dom.TagName.P;
tag2 = goog.dom.TagName.BR;
} else if (goog.userAgent.GECKO) {
tag1 = goog.dom.TagName.SPAN;
tag2 = goog.dom.TagName.BR;
}
var parent = goog.dom.createDom(goog.dom.TagName.DIV);
root.appendChild(parent);
parent.style.fontSize = '2em';
parent.style.display = 'none';
if (!goog.userAgent.WEBKIT) {
parent.appendChild(goog.dom.createTextNode('NonWebKitText'));
}
if (tag1) {
var e1 = goog.dom.createDom(tag1);
parent.appendChild(e1);
parent = e1;
}
if (tag2) {
parent.appendChild(goog.dom.createDom(tag2));
}
parent.appendChild(goog.dom.createTextNode('Text'));
if (goog.userAgent.WEBKIT) {
root.firstChild.appendChild(goog.dom.createTextNode('WebKitText'));
}
}
function testAssertHtmlContentsMatch() {
setUpAssertHtmlMatches();
goog.testing.dom.assertHtmlContentsMatch(
'<div style="display: none; font-size: 2em">' +
'[[!WEBKIT]]NonWebKitText<div class="IE"><p class="WEBKIT">' +
'<span class="GECKO"><br class="GECKO WEBKIT">Text</span></p></div>' +
'</div>[[WEBKIT]]WebKitText',
root);
}
function testAssertHtmlMismatchText() {
setUpAssertHtmlMatches();
var e = assertThrows('Should fail due to mismatched text', function() {
goog.testing.dom.assertHtmlContentsMatch(
'<div style="display: none; font-size: 2em">' +
'[[IE GECKO]]NonWebKitText<div class="IE"><p class="WEBKIT">' +
'<span class="GECKO"><br class="GECKO WEBKIT">Bad</span></p></div>' +
'</div>[[WEBKIT]]Extra',
root);
});
assertContains('Text should match', e.message);
}
function testAssertHtmlMismatchTag() {
setUpAssertHtmlMatches();
var e = assertThrows('Should fail due to mismatched tag', function() {
goog.testing.dom.assertHtmlContentsMatch(
'<span style="display: none; font-size: 2em">' +
'[[IE GECKO]]NonWebKitText<div class="IE"><p class="WEBKIT">' +
'<span class="GECKO"><br class="GECKO WEBKIT">Text</span></p></div>' +
'</span>[[WEBKIT]]Extra',
root);
});
assertContains('Tag names should match', e.message);
}
function testAssertHtmlMismatchStyle() {
setUpAssertHtmlMatches();
var e = assertThrows('Should fail due to mismatched style', function() {
goog.testing.dom.assertHtmlContentsMatch(
'<div style="display: none; font-size: 3em">' +
'[[IE GECKO]]NonWebKitText<div class="IE"><p class="WEBKIT">' +
'<span class="GECKO"><br class="GECKO WEBKIT">Text</span></p></div>' +
'</div>[[WEBKIT]]Extra',
root);
});
assertContains('Should have same styles', e.message);
}
function testAssertHtmlMismatchOptionalText() {
setUpAssertHtmlMatches();
var e = assertThrows('Should fail due to mismatched text', function() {
goog.testing.dom.assertHtmlContentsMatch(
'<div style="display: none; font-size: 2em">' +
'[[IE GECKO]]Bad<div class="IE"><p class="WEBKIT">' +
'<span class="GECKO"><br class="GECKO WEBKIT">Text</span></p></div>' +
'</div>[[WEBKIT]]Bad',
root);
});
assertContains('Text should match', e.message);
}
function testAssertHtmlMismatchExtraActualAfterText() {
root.innerHTML = '<div>abc</div>def';
var e = assertThrows('Should fail due to extra actual nodes', function() {
goog.testing.dom.assertHtmlContentsMatch('<div>abc</div>', root);
});
assertContains('Finished expected HTML before', e.message);
}
function testAssertHtmlMismatchExtraActualAfterElement() {
root.innerHTML = '<br>def';
var e = assertThrows('Should fail due to extra actual nodes', function() {
goog.testing.dom.assertHtmlContentsMatch('<br>', root);
});
assertContains('Finished expected HTML before', e.message);
}
function testAssertHtmlMatchesWithSplitTextNodes() {
root.appendChild(goog.dom.createTextNode('1'));
root.appendChild(goog.dom.createTextNode('2'));
root.appendChild(goog.dom.createTextNode('3'));
goog.testing.dom.assertHtmlContentsMatch('123', root);
}
function testAssertHtmlMatchesWithDifferentlyOrderedAttributes() {
root.innerHTML = '<div foo="a" bar="b" class="className"></div>';
goog.testing.dom.assertHtmlContentsMatch(
'<div bar="b" class="className" foo="a"></div>', root, true);
}
function testAssertHtmlMismatchWithDifferentNumberOfAttributes() {
root.innerHTML = '<div foo="a" bar="b"></div>';
var e = assertThrows(function() {
goog.testing.dom.assertHtmlContentsMatch(
'<div foo="a"></div>', root, true);
});
assertContains('Unexpected attribute with name bar in element', e.message);
}
function testAssertHtmlMismatchWithDifferentAttributeNames() {
root.innerHTML = '<div foo="a" bar="b"></div>';
var e = assertThrows(function() {
goog.testing.dom.assertHtmlContentsMatch(
'<div foo="a" baz="b"></div>', root, true);
});
assertContains('Expected to find attribute with name baz', e.message);
}
function testAssertHtmlMismatchWithDifferentClassNames() {
root.innerHTML = '<div class="className1"></div>';
var e = assertThrows(function() {
goog.testing.dom.assertHtmlContentsMatch(
'<div class="className2"></div>', root, true);
});
assertContains(
'Expected class was: className2, but actual class was: className1',
e.message);
}
function testAssertHtmlMatchesWithClassNameAndUserAgentSpecified() {
root.innerHTML =
'<div>' + (goog.userAgent.GECKO ? '<div class="foo"></div>' : '') +
'</div>';
goog.testing.dom.assertHtmlContentsMatch(
'<div><div class="foo GECKO"></div></div>',
root, true);
}
function testAssertHtmlMatchesWithClassesInDifferentOrder() {
root.innerHTML = '<div class="class1 class2"></div>';
goog.testing.dom.assertHtmlContentsMatch(
'<div class="class2 class1"></div>', root, true);
}
function testAssertHtmlMismatchWithDifferentAttributeValues() {
root.innerHTML = '<div foo="b" bar="a"></div>';
var e = assertThrows(function() {
goog.testing.dom.assertHtmlContentsMatch(
'<div foo="a" bar="a"></div>', root, true);
});
assertContains('Expected attribute foo has a different value', e.message);
}
function testAssertHtmlMatchesWhenStrictAttributesIsFalse() {
root.innerHTML = '<div foo="a" bar="b"></div>';
goog.testing.dom.assertHtmlContentsMatch('<div foo="a"></div>', root);
}
function testAssertHtmlMatchesForMethodsAttribute() {
root.innerHTML = '<a methods="get"></a>';
goog.testing.dom.assertHtmlContentsMatch('<a></a>', root);
goog.testing.dom.assertHtmlContentsMatch('<a methods="get"></a>', root);
goog.testing.dom.assertHtmlContentsMatch('<a methods="get"></a>', root,
true);
}
function testAssertHtmlMatchesForMethodsAttribute() {
root.innerHTML = '<input></input>';
goog.testing.dom.assertHtmlContentsMatch('<input></input>', root);
goog.testing.dom.assertHtmlContentsMatch('<input></input>', root, true);
}
function testAssertHtmlMatchesForIdAttribute() {
root.innerHTML = '<div id="foo"></div>';
goog.testing.dom.assertHtmlContentsMatch('<div></div>', root);
goog.testing.dom.assertHtmlContentsMatch('<div id="foo"></div>', root);
goog.testing.dom.assertHtmlContentsMatch('<div id="foo"></div>', root,
true);
}
function testAssertHtmlMatchesWhenIdIsNotSpecified() {
root.innerHTML = '<div id="someId"></div>';
goog.testing.dom.assertHtmlContentsMatch('<div></div>', root);
}
function testAssertHtmlMismatchWhenIdIsNotSpecified() {
root.innerHTML = '<div id="someId"></div>';
var e = assertThrows(function() {
goog.testing.dom.assertHtmlContentsMatch('<div></div>', root, true);
});
assertContains('Unexpected attribute with name id in element', e.message);
}
function testAssertHtmlMismatchWhenIdIsSpecified() {
root.innerHTML = '<div></div>';
var e = assertThrows(function() {
goog.testing.dom.assertHtmlContentsMatch('<div id="someId"></div>', root);
});
assertContains('Expected to find attribute with name id, in element',
e.message);
e = assertThrows(function() {
goog.testing.dom.assertHtmlContentsMatch('<div id="someId"></div>', root,
true);
});
assertContains('Expected to find attribute with name id, in element',
e.message);
}
function testAssertHtmlMatchesWhenIdIsEmpty() {
root.innerHTML = '<div></div>';
goog.testing.dom.assertHtmlContentsMatch('<div></div>', root);
goog.testing.dom.assertHtmlContentsMatch('<div></div>', root, true);
}
function testAssertHtmlMatchesWithDisabledAttribute() {
var disabledShortest = '<input disabled="disabled">';
var disabledShort = '<input disabled="">';
var disabledLong = '<input disabled="disabled">';
var enabled = '<input>';
root.innerHTML = disabledLong;
goog.testing.dom.assertHtmlContentsMatch(disabledShortest, root, true);
goog.testing.dom.assertHtmlContentsMatch(disabledShort, root, true);
goog.testing.dom.assertHtmlContentsMatch(disabledLong, root, true);
var e = assertThrows('Should fail due to mismatched text', function() {
goog.testing.dom.assertHtmlContentsMatch(enabled, root, true);
});
// Attribute value mismatch in IE.
// Unexpected attribute error in other browsers.
assertContains('disabled', e.message);
}
function testAssertHtmlMatchesWithCheckedAttribute() {
var checkedShortest = '<input type="radio" name="x" checked="checked">';
var checkedShort = '<input type="radio" name="x" checked="">';
var checkedLong = '<input type="radio" name="x" checked="checked">';
var unchecked = '<input type="radio" name="x">';
root.innerHTML = checkedLong;
goog.testing.dom.assertHtmlContentsMatch(checkedShortest, root, true);
goog.testing.dom.assertHtmlContentsMatch(checkedShort, root, true);
goog.testing.dom.assertHtmlContentsMatch(checkedLong, root, true);
if (!goog.userAgent.IE) {
// CHECKED attribute is ignored because it's among BAD_IE_ATTRIBUTES_.
var e = assertThrows('Should fail due to mismatched text', function() {
goog.testing.dom.assertHtmlContentsMatch(unchecked, root, true);
});
assertContains('Unexpected attribute with name checked', e.message);
}
}
function testAssertHtmlMatchesWithWhitespace() {
root.innerHTML = '';
root.appendChild(goog.dom.createTextNode(' A '));
goog.testing.dom.assertHtmlContentsMatch(' A ', root);
root.innerHTML = '';
root.appendChild(goog.dom.createTextNode(' A '));
root.appendChild(goog.dom.createDom('span', null, ' B '));
root.appendChild(goog.dom.createTextNode(' C '));
goog.testing.dom.assertHtmlContentsMatch(
' A <span> B </span> C ', root);
root.innerHTML = '';
root.appendChild(goog.dom.createTextNode(' A'));
root.appendChild(goog.dom.createDom('span', null, ' B'));
root.appendChild(goog.dom.createTextNode(' C'));
goog.testing.dom.assertHtmlContentsMatch(
' A<span> B</span> C', root);
}
function testAssertHtmlMatchesWithWhitespaceAndNesting() {
root.innerHTML = '';
root.appendChild(goog.dom.createDom('div', null,
goog.dom.createDom('b', null, ' A '),
goog.dom.createDom('b', null, ' B ')));
root.appendChild(goog.dom.createDom('div', null,
goog.dom.createDom('b', null, ' C '),
goog.dom.createDom('b', null, ' D ')));
goog.testing.dom.assertHtmlContentsMatch(
'<div><b> A </b><b> B </b></div>' +
'<div><b> C </b><b> D </b></div>', root);
root.innerHTML = '';
root.appendChild(goog.dom.createDom('b', null,
goog.dom.createDom('b', null,
goog.dom.createDom('b', null, ' A '))));
root.appendChild(goog.dom.createDom('b', null, ' B '));
goog.testing.dom.assertHtmlContentsMatch(
'<b><b><b> A </b></b></b><b> B </b>', root);
root.innerHTML = '';
root.appendChild(goog.dom.createDom('div', null,
goog.dom.createDom('b', null,
goog.dom.createDom('b', null, ' A '))));
root.appendChild(goog.dom.createDom('b', null, ' B '));
goog.testing.dom.assertHtmlContentsMatch(
'<div><b><b> A </b></b></div><b> B </b>', root);
root.innerHTML = '&nbsp;';
goog.testing.dom.assertHtmlContentsMatch(
'&nbsp;', root);
}
function testAssertHtmlMatches() {
// Since assertHtmlMatches is based on assertHtmlContentsMatch, we leave the
// majority of edge case testing to the above. Here we just do a sanity
// check.
goog.testing.dom.assertHtmlMatches('<div>abc</div>', '<div>abc</div>');
goog.testing.dom.assertHtmlMatches('<div>abc</div>', '<div>abc</div> ');
goog.testing.dom.assertHtmlMatches(
'<div style="font-size: 1px; color: red">abc</div>',
'<div style="color: red; font-size: 1px;;">abc</div>');
var e = assertThrows('Should fail due to mismatched text', function() {
goog.testing.dom.assertHtmlMatches('<div>abc</div>', '<div>abd</div>');
});
assertContains('Text should match', e.message);
}
@@ -0,0 +1,293 @@
// Copyright 2009 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 Testing utilities for editor specific DOM related tests.
*
*/
goog.provide('goog.testing.editor.dom');
goog.require('goog.dom.NodeType');
goog.require('goog.dom.TagIterator');
goog.require('goog.dom.TagWalkType');
goog.require('goog.iter');
goog.require('goog.string');
goog.require('goog.testing.asserts');
/**
* Returns the previous (in document order) node from the given node that is a
* non-empty text node, or null if none is found or opt_stopAt is not an
* ancestor of node. Note that if the given node has children, the search will
* start from the end tag of the node, meaning all its descendants will be
* included in the search, unless opt_skipDescendants is true.
* @param {Node} node Node to start searching from.
* @param {Node=} opt_stopAt Node to stop searching at (search will be
* restricted to this node's subtree), defaults to the body of the document
* containing node.
* @param {boolean=} opt_skipDescendants Whether to skip searching the given
* node's descentants.
* @return {Text} The previous (in document order) node from the given node
* that is a non-empty text node, or null if none is found.
*/
goog.testing.editor.dom.getPreviousNonEmptyTextNode = function(
node, opt_stopAt, opt_skipDescendants) {
return goog.testing.editor.dom.getPreviousNextNonEmptyTextNodeHelper_(
node, opt_stopAt, opt_skipDescendants, true);
};
/**
* Returns the next (in document order) node from the given node that is a
* non-empty text node, or null if none is found or opt_stopAt is not an
* ancestor of node. Note that if the given node has children, the search will
* start from the start tag of the node, meaning all its descendants will be
* included in the search, unless opt_skipDescendants is true.
* @param {Node} node Node to start searching from.
* @param {Node=} opt_stopAt Node to stop searching at (search will be
* restricted to this node's subtree), defaults to the body of the document
* containing node.
* @param {boolean=} opt_skipDescendants Whether to skip searching the given
* node's descentants.
* @return {Text} The next (in document order) node from the given node that
* is a non-empty text node, or null if none is found or opt_stopAt is not
* an ancestor of node.
*/
goog.testing.editor.dom.getNextNonEmptyTextNode = function(
node, opt_stopAt, opt_skipDescendants) {
return goog.testing.editor.dom.getPreviousNextNonEmptyTextNodeHelper_(
node, opt_stopAt, opt_skipDescendants, false);
};
/**
* Helper that returns the previous or next (in document order) node from the
* given node that is a non-empty text node, or null if none is found or
* opt_stopAt is not an ancestor of node. Note that if the given node has
* children, the search will start from the end or start tag of the node
* (depending on whether it's searching for the previous or next node), meaning
* all its descendants will be included in the search, unless
* opt_skipDescendants is true.
* @param {Node} node Node to start searching from.
* @param {Node=} opt_stopAt Node to stop searching at (search will be
* restricted to this node's subtree), defaults to the body of the document
* containing node.
* @param {boolean=} opt_skipDescendants Whether to skip searching the given
* node's descentants.
* @param {boolean=} opt_isPrevious Whether to search for the previous non-empty
* text node instead of the next one.
* @return {Text} The next (in document order) node from the given node that
* is a non-empty text node, or null if none is found or opt_stopAt is not
* an ancestor of node.
* @private
*/
goog.testing.editor.dom.getPreviousNextNonEmptyTextNodeHelper_ = function(
node, opt_stopAt, opt_skipDescendants, opt_isPrevious) {
opt_stopAt = opt_stopAt || node.ownerDocument.body;
// Initializing the iterator to iterate over the children of opt_stopAt
// makes it stop only when it finishes iterating through all of that
// node's children, even though we will start at a different node and exit
// that starting node's subtree in the process.
var iter = new goog.dom.TagIterator(opt_stopAt, opt_isPrevious);
// TODO(user): Move this logic to a new method in TagIterator such as
// skipToNode().
// Then we set the iterator to start at the given start node, not opt_stopAt.
var walkType; // Let TagIterator set the initial walk type by default.
var depth = goog.testing.editor.dom.getRelativeDepth_(node, opt_stopAt);
if (depth == -1) {
return null; // Fail because opt_stopAt is not an ancestor of node.
}
if (node.nodeType == goog.dom.NodeType.ELEMENT) {
if (opt_skipDescendants) {
// Specifically set the initial walk type so that we skip the descendant
// subtree by starting at the start if going backwards or at the end if
// going forwards.
walkType = opt_isPrevious ? goog.dom.TagWalkType.START_TAG :
goog.dom.TagWalkType.END_TAG;
} else {
// We're starting "inside" an element node so the depth needs to be one
// deeper than the node's actual depth. That's how TagIterator works!
depth++;
}
}
iter.setPosition(node, walkType, depth);
// Advance the iterator so it skips the start node.
try {
iter.next();
} catch (e) {
return null; // It could have been a leaf node.
}
// Now just get the first non-empty text node the iterator finds.
var filter = goog.iter.filter(iter,
goog.testing.editor.dom.isNonEmptyTextNode_);
try {
return /** @type {Text} */ (filter.next());
} catch (e) { // No next item is available so return null.
return null;
}
};
/**
* Returns whether the given node is a non-empty text node.
* @param {Node} node Node to be checked.
* @return {boolean} Whether the given node is a non-empty text node.
* @private
*/
goog.testing.editor.dom.isNonEmptyTextNode_ = function(node) {
return !!node && node.nodeType == goog.dom.NodeType.TEXT && node.length > 0;
};
/**
* Returns the depth of the given node relative to the given parent node, or -1
* if the given node is not a descendant of the given parent node. E.g. if
* node == parentNode returns 0, if node.parentNode == parentNode returns 1,
* etc.
* @param {Node} node Node whose depth to get.
* @param {Node} parentNode Node relative to which the depth should be
* calculated.
* @return {number} The depth of the given node relative to the given parent
* node, or -1 if the given node is not a descendant of the given parent
* node.
* @private
*/
goog.testing.editor.dom.getRelativeDepth_ = function(node, parentNode) {
var depth = 0;
while (node) {
if (node == parentNode) {
return depth;
}
node = node.parentNode;
depth++;
}
return -1;
};
/**
* Assert that the range is surrounded by the given strings. This is useful
* because different browsers can place the range endpoints inside different
* nodes even when visually the range looks the same. Also, there may be empty
* text nodes in the way (again depending on the browser) making it difficult to
* use assertRangeEquals.
* @param {string} before String that should occur immediately before the start
* point of the range. If this is the empty string, assert will only succeed
* if there is no text before the start point of the range.
* @param {string} after String that should occur immediately after the end
* point of the range. If this is the empty string, assert will only succeed
* if there is no text after the end point of the range.
* @param {goog.dom.AbstractRange} range The range to be tested.
* @param {Node=} opt_stopAt Node to stop searching at (search will be
* restricted to this node's subtree).
*/
goog.testing.editor.dom.assertRangeBetweenText = function(before,
after,
range,
opt_stopAt) {
var previousText =
goog.testing.editor.dom.getTextFollowingRange_(range, true, opt_stopAt);
if (before == '') {
assertNull('Expected nothing before range but found <' + previousText + '>',
previousText);
} else {
assertNotNull('Expected <' + before + '> before range but found nothing',
previousText);
assertTrue('Expected <' + before + '> before range but found <' +
previousText + '>',
goog.string.endsWith(
/** @type {string} */ (previousText), before));
}
var nextText =
goog.testing.editor.dom.getTextFollowingRange_(range, false, opt_stopAt);
if (after == '') {
assertNull('Expected nothing after range but found <' + nextText + '>',
nextText);
} else {
assertNotNull('Expected <' + after + '> after range but found nothing',
nextText);
assertTrue('Expected <' + after + '> after range but found <' +
nextText + '>',
goog.string.startsWith(
/** @type {string} */ (nextText), after));
}
};
/**
* Returns the text that follows the given range, where the term "follows" means
* "comes immediately before the start of the range" if isBefore is true, and
* "comes immediately after the end of the range" if isBefore is false, or null
* if no non-empty text node is found.
* @param {goog.dom.AbstractRange} range The range to search from.
* @param {boolean} isBefore Whether to search before the range instead of
* after it.
* @param {Node=} opt_stopAt Node to stop searching at (search will be
* restricted to this node's subtree).
* @return {?string} The text that follows the given range, or null if no
* non-empty text node is found.
* @private
*/
goog.testing.editor.dom.getTextFollowingRange_ = function(range,
isBefore,
opt_stopAt) {
var followingTextNode;
var endpointNode = isBefore ? range.getStartNode() : range.getEndNode();
var endpointOffset = isBefore ? range.getStartOffset() : range.getEndOffset();
var getFollowingTextNode =
isBefore ? goog.testing.editor.dom.getPreviousNonEmptyTextNode :
goog.testing.editor.dom.getNextNonEmptyTextNode;
if (endpointNode.nodeType == goog.dom.NodeType.TEXT) {
// Range endpoint is in a text node.
var endText = endpointNode.nodeValue;
if (isBefore ? endpointOffset > 0 : endpointOffset < endText.length) {
// There is text in this node following the endpoint so return the portion
// that follows the endpoint.
return isBefore ? endText.substr(0, endpointOffset) :
endText.substr(endpointOffset);
} else {
// There is no text following the endpoint so look for the follwing text
// node.
followingTextNode = getFollowingTextNode(endpointNode, opt_stopAt);
return followingTextNode && followingTextNode.nodeValue;
}
} else {
// Range endpoint is in an element node.
var numChildren = endpointNode.childNodes.length;
if (isBefore ? endpointOffset > 0 : endpointOffset < numChildren) {
// There is at least one child following the endpoint.
var followingChild =
endpointNode.childNodes[isBefore ? endpointOffset - 1 :
endpointOffset];
if (goog.testing.editor.dom.isNonEmptyTextNode_(followingChild)) {
// The following child has text so return that.
return followingChild.nodeValue;
} else {
// The following child has no text so look for the following text node.
followingTextNode = getFollowingTextNode(followingChild, opt_stopAt);
return followingTextNode && followingTextNode.nodeValue;
}
} else {
// There is no child following the endpoint, so search from the endpoint
// node, but don't search its children because they are not following the
// endpoint!
followingTextNode = getFollowingTextNode(endpointNode, opt_stopAt, true);
return followingTextNode && followingTextNode.nodeValue;
}
}
};
@@ -0,0 +1,28 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<!--
@author marcosalmeida@google.com (Marcos Almeida)
-->
<title>
Closure Unit Tests - goog.testing.editor.dom
</title>
<script src="../../base.js">
</script>
<script>
goog.require('goog.testing.editor.domTest');
</script>
</head>
<body>
<div id="root">
</div>
</body>
</html>
<!--
Copyright 2009 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
@@ -0,0 +1,290 @@
// Copyright 2009 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.
goog.provide('goog.testing.editor.domTest');
goog.setTestOnly('goog.testing.editor.domTest');
goog.require('goog.dom');
goog.require('goog.dom.TagName');
goog.require('goog.functions');
goog.require('goog.testing.editor.dom');
goog.require('goog.testing.jsunit');
var root;
var parentNode, childNode1, childNode2, childNode3;
var first, middle, last;
function setUpPage() {
root = goog.dom.getElement('root');
}
function tearDown() {
root.innerHTML = '';
}
function setUpNonEmptyTests() {
childNode1 = goog.dom.createElement(goog.dom.TagName.DIV);
childNode2 = goog.dom.createElement(goog.dom.TagName.DIV);
childNode3 = goog.dom.createElement(goog.dom.TagName.DIV);
parentNode = goog.dom.createDom(goog.dom.TagName.DIV,
null,
childNode1,
childNode2,
childNode3);
goog.dom.appendChild(root, parentNode);
childNode1.appendChild(goog.dom.createTextNode('One'));
childNode1.appendChild(goog.dom.createTextNode(''));
childNode2.appendChild(goog.dom.createElement(goog.dom.TagName.BR));
childNode2.appendChild(goog.dom.createTextNode('TwoA'));
childNode2.appendChild(goog.dom.createTextNode('TwoB'));
childNode2.appendChild(goog.dom.createElement(goog.dom.TagName.BR));
childNode3.appendChild(goog.dom.createTextNode(''));
childNode3.appendChild(goog.dom.createTextNode('Three'));
}
function testGetNextNonEmptyTextNode() {
setUpNonEmptyTests();
var nodeOne =
goog.testing.editor.dom.getNextNonEmptyTextNode(parentNode);
assertEquals('Should have found the next non-empty text node',
'One',
nodeOne.nodeValue);
var nodeTwoA =
goog.testing.editor.dom.getNextNonEmptyTextNode(nodeOne);
assertEquals('Should have found the next non-empty text node',
'TwoA',
nodeTwoA.nodeValue);
var nodeTwoB =
goog.testing.editor.dom.getNextNonEmptyTextNode(nodeTwoA);
assertEquals('Should have found the next non-empty text node',
'TwoB',
nodeTwoB.nodeValue);
var nodeThree =
goog.testing.editor.dom.getNextNonEmptyTextNode(nodeTwoB);
assertEquals('Should have found the next non-empty text node',
'Three',
nodeThree.nodeValue);
var nodeNull =
goog.testing.editor.dom.getNextNonEmptyTextNode(nodeThree, parentNode);
assertNull('Should not have found any non-empty text node', nodeNull);
var nodeStop =
goog.testing.editor.dom.getNextNonEmptyTextNode(nodeOne, childNode1);
assertNull('Should have stopped before finding a node', nodeStop);
var nodeBeforeStop =
goog.testing.editor.dom.getNextNonEmptyTextNode(nodeTwoA, childNode2);
assertEquals('Should have found the next non-empty text node',
'TwoB',
nodeBeforeStop.nodeValue);
}
function testGetPreviousNonEmptyTextNode() {
setUpNonEmptyTests();
var nodeThree =
goog.testing.editor.dom.getPreviousNonEmptyTextNode(parentNode);
assertEquals('Should have found the previous non-empty text node',
'Three',
nodeThree.nodeValue);
var nodeTwoB =
goog.testing.editor.dom.getPreviousNonEmptyTextNode(nodeThree);
assertEquals('Should have found the previous non-empty text node',
'TwoB',
nodeTwoB.nodeValue);
var nodeTwoA =
goog.testing.editor.dom.getPreviousNonEmptyTextNode(nodeTwoB);
assertEquals('Should have found the previous non-empty text node',
'TwoA',
nodeTwoA.nodeValue);
var nodeOne =
goog.testing.editor.dom.getPreviousNonEmptyTextNode(nodeTwoA);
assertEquals('Should have found the previous non-empty text node',
'One',
nodeOne.nodeValue);
var nodeNull =
goog.testing.editor.dom.getPreviousNonEmptyTextNode(nodeOne,
parentNode);
assertNull('Should not have found any non-empty text node', nodeNull);
var nodeStop =
goog.testing.editor.dom.getPreviousNonEmptyTextNode(nodeThree,
childNode3);
assertNull('Should have stopped before finding a node', nodeStop);
var nodeBeforeStop =
goog.testing.editor.dom.getPreviousNonEmptyTextNode(nodeTwoB,
childNode2);
assertEquals('Should have found the previous non-empty text node',
'TwoA',
nodeBeforeStop.nodeValue);
}
function setUpAssertRangeBetweenText() {
// Create the following structure: <[01]><[]><[23]>
// Where <> delimits spans, [] delimits text nodes, 01 and 23 are text.
// We will test all 10 positions in between 0 and 2. All should pass.
first = goog.dom.createDom(goog.dom.TagName.SPAN, null, '01');
middle = goog.dom.createElement(goog.dom.TagName.SPAN);
var emptyTextNode = goog.dom.createTextNode('');
goog.dom.appendChild(middle, emptyTextNode);
last = goog.dom.createDom(goog.dom.TagName.SPAN, null, '23');
goog.dom.appendChild(root, first);
goog.dom.appendChild(root, middle);
goog.dom.appendChild(root, last);
}
function createFakeRange(startNode, startOffset, opt_endNode, opt_endOffset) {
opt_endNode = opt_endNode || startNode;
opt_endOffset = opt_endOffset || startOffset;
return {
getStartNode: goog.functions.constant(startNode),
getStartOffset: goog.functions.constant(startOffset),
getEndNode: goog.functions.constant(opt_endNode),
getEndOffset: goog.functions.constant(opt_endOffset)
};
}
function testAssertRangeBetweenText0() {
setUpAssertRangeBetweenText();
goog.testing.editor.dom.assertRangeBetweenText('0', '1',
createFakeRange(first.firstChild, 1));
}
function testAssertRangeBetweenText1() {
setUpAssertRangeBetweenText();
goog.testing.editor.dom.assertRangeBetweenText('1', '2',
createFakeRange(first.firstChild, 2));
}
function testAssertRangeBetweenText2() {
setUpAssertRangeBetweenText();
goog.testing.editor.dom.assertRangeBetweenText('1', '2',
createFakeRange(first, 1));
}
function testAssertRangeBetweenText3() {
setUpAssertRangeBetweenText();
goog.testing.editor.dom.assertRangeBetweenText('1', '2',
createFakeRange(root, 1));
}
function testAssertRangeBetweenText4() {
setUpAssertRangeBetweenText();
goog.testing.editor.dom.assertRangeBetweenText('1', '2',
createFakeRange(middle, 0));
}
function testAssertRangeBetweenText5() {
setUpAssertRangeBetweenText();
goog.testing.editor.dom.assertRangeBetweenText('1', '2',
createFakeRange(middle.firstChild, 0));
}
function testAssertRangeBetweenText6() {
setUpAssertRangeBetweenText();
goog.testing.editor.dom.assertRangeBetweenText('1', '2',
createFakeRange(middle, 1));
}
function testAssertRangeBetweenText7() {
setUpAssertRangeBetweenText();
goog.testing.editor.dom.assertRangeBetweenText('1', '2',
createFakeRange(root, 2));
}
function testAssertRangeBetweenText8() {
setUpAssertRangeBetweenText();
goog.testing.editor.dom.assertRangeBetweenText('1', '2',
createFakeRange(last, 0));
}
function testAssertRangeBetweenText9() {
setUpAssertRangeBetweenText();
goog.testing.editor.dom.assertRangeBetweenText('1', '2',
createFakeRange(last.firstChild, 0));
}
function testAssertRangeBetweenTextBefore() {
setUpAssertRangeBetweenText();
// Test that it works when the cursor is at the beginning of all text.
goog.testing.editor.dom.assertRangeBetweenText('', '0',
createFakeRange(first.firstChild, 0),
root); // Restrict to root div so it won't find /n's and script.
}
function testAssertRangeBetweenTextAfter() {
setUpAssertRangeBetweenText();
// Test that it works when the cursor is at the end of all text.
goog.testing.editor.dom.assertRangeBetweenText('3', '',
createFakeRange(last.firstChild, 2),
root); // Restrict to root div so it won't find /n's and script.
}
function testAssertRangeBetweenTextFail1() {
setUpAssertRangeBetweenText();
var e = assertThrows('assertRangeBetweenText should have failed',
function() {
goog.testing.editor.dom.assertRangeBetweenText('1', '3',
createFakeRange(first.firstChild, 2));
});
assertContains('Assert reason incorrect',
'Expected <3> after range but found <23>', e.message);
}
function testAssertRangeBetweenTextFail2() {
setUpAssertRangeBetweenText();
var e = assertThrows('assertRangeBetweenText should have failed',
function() {
goog.testing.editor.dom.assertRangeBetweenText('1', '2',
createFakeRange(first.firstChild, 2, last.firstChild, 1));
});
assertContains('Assert reason incorrect',
'Expected <2> after range but found <3>', e.message);
}
function testAssertRangeBetweenTextBeforeFail() {
setUpAssertRangeBetweenText();
// Test that it gives the right message when the cursor is at the beginning
// of all text but you're expecting something before it.
var e = assertThrows('assertRangeBetweenText should have failed',
function() {
goog.testing.editor.dom.assertRangeBetweenText('-1', '0',
createFakeRange(first.firstChild, 0),
root); // Restrict to root div so it won't find /n's and script.
});
assertContains('Assert reason incorrect',
'Expected <-1> before range but found nothing', e.message);
}
function testAssertRangeBetweenTextAfterFail() {
setUpAssertRangeBetweenText();
// Test that it gives the right message when the cursor is at the end
// of all text but you're expecting something after it.
var e = assertThrows('assertRangeBetweenText should have failed',
function() {
goog.testing.editor.dom.assertRangeBetweenText('3', '4',
createFakeRange(last.firstChild, 2),
root); // Restrict to root div so it won't find /n's and script.
});
assertContains('Assert reason incorrect',
'Expected <4> after range but found nothing', e.message);
}
@@ -0,0 +1,116 @@
// 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 Mock of goog.editor.field.
*
* @author robbyw@google.com (Robby Walker)
*/
goog.provide('goog.testing.editor.FieldMock');
goog.require('goog.dom');
goog.require('goog.dom.Range');
goog.require('goog.editor.Field');
goog.require('goog.testing.LooseMock');
goog.require('goog.testing.mockmatchers');
/**
* Mock of goog.editor.Field.
* @param {Window=} opt_window Window the field would edit. Defaults to
* {@code window}.
* @param {Window=} opt_appWindow "AppWindow" of the field, which can be
* different from {@code opt_window} when mocking a field that uses an
* iframe. Defaults to {@code opt_window}.
* @param {goog.dom.AbstractRange=} opt_range An object (mock or real) to be
* returned by getRange(). If ommitted, a new goog.dom.Range is created
* from the window every time getRange() is called.
* @constructor
* @extends {goog.testing.LooseMock}
* @suppress {missingProperties} Mocks do not fit in the type system well.
* @final
*/
goog.testing.editor.FieldMock =
function(opt_window, opt_appWindow, opt_range) {
goog.testing.LooseMock.call(this, goog.editor.Field);
opt_window = opt_window || window;
opt_appWindow = opt_appWindow || opt_window;
this.getAppWindow();
this.$anyTimes();
this.$returns(opt_appWindow);
this.getRange();
this.$anyTimes();
this.$does(function() {
return opt_range || goog.dom.Range.createFromWindow(opt_window);
});
this.getEditableDomHelper();
this.$anyTimes();
this.$returns(goog.dom.getDomHelper(opt_window.document));
this.usesIframe();
this.$anyTimes();
this.getBaseZindex();
this.$anyTimes();
this.$returns(0);
this.restoreSavedRange(goog.testing.mockmatchers.ignoreArgument);
this.$anyTimes();
this.$does(function(range) {
if (range) {
range.restore();
}
this.focus();
});
// These methods cannot be set on the prototype, because the prototype
// gets stepped on by the mock framework.
var inModalMode = false;
/**
* @return {boolean} Whether we're in modal interaction mode.
*/
this.inModalMode = function() {
return inModalMode;
};
/**
* @param {boolean} mode Sets whether we're in modal interaction mode.
*/
this.setModalMode = function(mode) {
inModalMode = mode;
};
var uneditable = false;
/**
* @return {boolean} Whether the field is uneditable.
*/
this.isUneditable = function() {
return uneditable;
};
/**
* @param {boolean} isUneditable Whether the field is uneditable.
*/
this.setUneditable = function(isUneditable) {
uneditable = isUneditable;
};
};
goog.inherits(goog.testing.editor.FieldMock, goog.testing.LooseMock);
@@ -0,0 +1,180 @@
// 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 Class that allows for simple text editing tests.
*
* @author robbyw@google.com (Robby Walker)
*/
goog.provide('goog.testing.editor.TestHelper');
goog.require('goog.Disposable');
goog.require('goog.dom');
goog.require('goog.dom.Range');
goog.require('goog.editor.BrowserFeature');
goog.require('goog.editor.node');
goog.require('goog.editor.plugins.AbstractBubblePlugin');
goog.require('goog.testing.dom');
/**
* Create a new test controller.
* @param {Element} root The root editable element.
* @constructor
* @extends {goog.Disposable}
* @final
*/
goog.testing.editor.TestHelper = function(root) {
if (!root) {
throw Error('Null root');
}
goog.Disposable.call(this);
/**
* Convenience variable for root DOM element.
* @type {!Element}
* @private
*/
this.root_ = root;
/**
* The starting HTML of the editable element.
* @type {string}
* @private
*/
this.savedHtml_ = '';
};
goog.inherits(goog.testing.editor.TestHelper, goog.Disposable);
/**
* Selects a new root element.
* @param {Element} root The root editable element.
*/
goog.testing.editor.TestHelper.prototype.setRoot = function(root) {
if (!root) {
throw Error('Null root');
}
this.root_ = root;
};
/**
* Make the root element editable. Alse saves its HTML to be restored
* in tearDown.
*/
goog.testing.editor.TestHelper.prototype.setUpEditableElement = function() {
this.savedHtml_ = this.root_.innerHTML;
if (goog.editor.BrowserFeature.HAS_CONTENT_EDITABLE) {
this.root_.contentEditable = true;
} else {
this.root_.ownerDocument.designMode = 'on';
}
this.root_.setAttribute('g_editable', 'true');
};
/**
* Reset the element previously initialized, restoring its HTML and making it
* non editable.
* @suppress {accessControls} Private state of
* {@link goog.editor.plugins.AbstractBubblePlugin} is accessed for test
* purposes.
*/
goog.testing.editor.TestHelper.prototype.tearDownEditableElement = function() {
if (goog.editor.BrowserFeature.HAS_CONTENT_EDITABLE) {
this.root_.contentEditable = false;
} else {
this.root_.ownerDocument.designMode = 'off';
}
goog.dom.removeChildren(this.root_);
this.root_.innerHTML = this.savedHtml_;
this.root_.removeAttribute('g_editable');
if (goog.editor.plugins && goog.editor.plugins.AbstractBubblePlugin) {
// Remove old bubbles.
for (var key in goog.editor.plugins.AbstractBubblePlugin.bubbleMap_) {
goog.editor.plugins.AbstractBubblePlugin.bubbleMap_[key].dispose();
}
// Ensure we get a new bubble for each test.
goog.editor.plugins.AbstractBubblePlugin.bubbleMap_ = {};
}
};
/**
* Assert that the html in 'root' is substantially similar to htmlPattern.
* This method tests for the same set of styles, and for the same order of
* nodes. Breaking whitespace nodes are ignored. Elements can be annotated
* with classnames corresponding to keys in goog.userAgent and will be
* expected to show up in that user agent and expected not to show up in
* others.
* @param {string} htmlPattern The pattern to match.
*/
goog.testing.editor.TestHelper.prototype.assertHtmlMatches = function(
htmlPattern) {
goog.testing.dom.assertHtmlContentsMatch(htmlPattern, this.root_);
};
/**
* Finds the first text node descendant of root with the given content.
* @param {string|RegExp} textOrRegexp The text to find, or a regular
* expression to find a match of.
* @return {Node} The first text node that matches, or null if none is found.
*/
goog.testing.editor.TestHelper.prototype.findTextNode = function(textOrRegexp) {
return goog.testing.dom.findTextNode(textOrRegexp, this.root_);
};
/**
* Select from the given from offset in the given from node to the given
* to offset in the optionally given to node. If nodes are passed in, uses them,
* otherwise uses findTextNode to find the nodes to select. Selects a caret
* if opt_to and opt_toOffset are not given.
* @param {Node|string} from Node or text of the node to start the selection at.
* @param {number} fromOffset Offset within the above node to start the
* selection at.
* @param {Node|string=} opt_to Node or text of the node to end the selection
* at.
* @param {number=} opt_toOffset Offset within the above node to end the
* selection at.
*/
goog.testing.editor.TestHelper.prototype.select = function(from, fromOffset,
opt_to, opt_toOffset) {
var end;
var start = end = goog.isString(from) ? this.findTextNode(from) : from;
var endOffset;
var startOffset = endOffset = fromOffset;
if (opt_to && goog.isNumber(opt_toOffset)) {
end = goog.isString(opt_to) ? this.findTextNode(opt_to) : opt_to;
endOffset = opt_toOffset;
}
goog.dom.Range.createFromNodes(start, startOffset, end, endOffset).select();
};
/** @override */
goog.testing.editor.TestHelper.prototype.disposeInternal = function() {
if (goog.editor.node.isEditableContainer(this.root_)) {
this.tearDownEditableElement();
}
delete this.root_;
goog.testing.editor.TestHelper.base(this, 'disposeInternal');
};
@@ -0,0 +1,25 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<!--
Copyright 2008 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>
Closure Unit Tests - goog.testing.editor.TestHelper
</title>
<script src="../../base.js">
</script>
<script>
goog.require('goog.testing.editor.TestHelperTest');
</script>
</head>
<body>
<div id="root">
</div>
<div id="root2">Root 2</div>
</body>
</html>
@@ -0,0 +1,175 @@
// 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.
goog.provide('goog.testing.editor.TestHelperTest');
goog.setTestOnly('goog.testing.editor.TestHelperTest');
goog.require('goog.dom');
goog.require('goog.dom.TagName');
goog.require('goog.editor.node');
goog.require('goog.testing.editor.TestHelper');
goog.require('goog.testing.jsunit');
goog.require('goog.userAgent');
var root;
var helper;
function setUp() {
root = goog.dom.getElement('root');
goog.dom.removeChildren(root);
helper = new goog.testing.editor.TestHelper(root);
}
function tearDown() {
helper.dispose();
}
function testSetRoot() {
helper.setRoot(goog.dom.getElement('root2'));
helper.assertHtmlMatches('Root 2');
}
function testSetupEditableElement() {
helper.setUpEditableElement();
assertTrue(goog.editor.node.isEditableContainer(root));
}
function testTearDownEditableElement() {
helper.setUpEditableElement();
assertTrue(goog.editor.node.isEditableContainer(root));
helper.tearDownEditableElement();
assertFalse(goog.editor.node.isEditableContainer(root));
}
function testFindNode() {
// Test the easiest case.
root.innerHTML = 'a<br>b';
assertEquals(helper.findTextNode('a'), root.firstChild);
assertEquals(helper.findTextNode('b'), root.lastChild);
assertNull(helper.findTextNode('c'));
}
function testFindNodeDuplicate() {
// Test duplicate.
root.innerHTML = 'c<br>c';
assertEquals('Should return first duplicate', helper.findTextNode('c'),
root.firstChild);
}
function findNodeWithHierarchy() {
// Test a more complicated hierarchy.
root.innerHTML = '<div>a<p>b<span>c</span>d</p>e</div>';
assertEquals(goog.dom.TagName.DIV,
helper.findTextNode('a').parentNode.tagName);
assertEquals(goog.dom.TagName.P,
helper.findTextNode('b').parentNode.tagName);
assertEquals(goog.dom.TagName.SPAN,
helper.findTextNode('c').parentNode.tagName);
assertEquals(goog.dom.TagName.P,
helper.findTextNode('d').parentNode.tagName);
assertEquals(goog.dom.TagName.DIV,
helper.findTextNode('e').parentNode.tagName);
}
function setUpAssertHtmlMatches() {
var tag1, tag2;
if (goog.userAgent.IE) {
tag1 = goog.dom.TagName.DIV;
} else if (goog.userAgent.WEBKIT) {
tag1 = goog.dom.TagName.P;
tag2 = goog.dom.TagName.BR;
} else if (goog.userAgent.GECKO) {
tag1 = goog.dom.TagName.SPAN;
tag2 = goog.dom.TagName.BR;
}
var parent = goog.dom.createDom(goog.dom.TagName.DIV);
root.appendChild(parent);
parent.style.fontSize = '2em';
parent.style.display = 'none';
if (goog.userAgent.IE || goog.userAgent.GECKO) {
parent.appendChild(goog.dom.createTextNode('NonWebKitText'));
}
if (tag1) {
var e1 = goog.dom.createDom(tag1);
parent.appendChild(e1);
parent = e1;
}
if (tag2) {
parent.appendChild(goog.dom.createDom(tag2));
}
parent.appendChild(goog.dom.createTextNode('Text'));
if (goog.userAgent.WEBKIT) {
root.firstChild.appendChild(goog.dom.createTextNode('WebKitText'));
}
}
function testAssertHtmlMatches() {
setUpAssertHtmlMatches();
helper.assertHtmlMatches('<div style="display: none; font-size: 2em">' +
'[[IE GECKO]]NonWebKitText<div class="IE"><p class="WEBKIT">' +
'<span class="GECKO"><br class="GECKO WEBKIT">Text</span></p></div>' +
'</div>[[WEBKIT]]WebKitText');
}
function testAssertHtmlMismatchText() {
setUpAssertHtmlMatches();
var e = assertThrows('Should fail due to mismatched text', function() {
helper.assertHtmlMatches('<div style="display: none; font-size: 2em">' +
'[[IE GECKO]]NonWebKitText<div class="IE"><p class="WEBKIT">' +
'<span class="GECKO"><br class="GECKO WEBKIT">Bad</span></p></div>' +
'</div>[[WEBKIT]]Extra');
});
assertContains('Text should match', e.message);
}
function testAssertHtmlMismatchTag() {
setUpAssertHtmlMatches();
var e = assertThrows('Should fail due to mismatched tag', function() {
helper.assertHtmlMatches('<span style="display: none; font-size: 2em">' +
'[[IE GECKO]]NonWebKitText<div class="IE"><p class="WEBKIT">' +
'<span class="GECKO"><br class="GECKO WEBKIT">Text</span></p></div>' +
'</span>[[WEBKIT]]Extra');
});
assertContains('Tag names should match', e.message);
}
function testAssertHtmlMismatchStyle() {
setUpAssertHtmlMatches();
var e = assertThrows('Should fail due to mismatched style', function() {
helper.assertHtmlMatches('<div style="display: none; font-size: 3em">' +
'[[IE GECKO]]NonWebKitText<div class="IE"><p class="WEBKIT">' +
'<span class="GECKO"><br class="GECKO WEBKIT">Text</span></p></div>' +
'</div>[[WEBKIT]]Extra');
});
assertContains('Should have same styles', e.message);
}
function testAssertHtmlMismatchOptionalText() {
setUpAssertHtmlMatches();
var e = assertThrows('Should fail due to mismatched style', function() {
helper.assertHtmlMatches('<div style="display: none; font-size: 2em">' +
'[[IE GECKO]]Bad<div class="IE"><p class="WEBKIT">' +
'<span class="GECKO"><br class="GECKO WEBKIT">Text</span></p></div>' +
'</div>[[WEBKIT]]Bad');
});
assertContains('Text should match', e.message);
}
@@ -0,0 +1,87 @@
// Copyright 2010 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 Event observer.
*
* Provides an event observer that holds onto events that it handles. This
* can be used in unit testing to verify an event target's events --
* that the order count, types, etc. are correct.
*
* Example usage:
* <pre>
* var observer = new goog.testing.events.EventObserver();
* var widget = new foo.Widget();
* goog.events.listen(widget, ['select', 'submit'], observer);
* // Simulate user action of 3 select events and 2 submit events.
* assertEquals(3, observer.getEvents('select').length);
* assertEquals(2, observer.getEvents('submit').length);
* </pre>
*
* @author nnaze@google.com (Nathan Naze)
*/
goog.provide('goog.testing.events.EventObserver');
goog.require('goog.array');
/**
* Event observer. Implements a handleEvent interface so it may be used as
* a listener in listening functions and methods.
* @see goog.events.listen
* @see goog.events.EventHandler
* @constructor
* @final
*/
goog.testing.events.EventObserver = function() {
/**
* A list of events handled by the observer in order of handling, oldest to
* newest.
* @type {!Array<!goog.events.Event>}
* @private
*/
this.events_ = [];
};
/**
* Handles an event and remembers it. Event listening functions and methods
* will call this method when this observer is used as a listener.
* @see goog.events.listen
* @see goog.events.EventHandler
* @param {!goog.events.Event} e Event to handle.
*/
goog.testing.events.EventObserver.prototype.handleEvent = function(e) {
this.events_.push(e);
};
/**
* @param {string=} opt_type If given, only return events of this type.
* @return {!Array<!goog.events.Event>} The events handled, oldest to newest.
*/
goog.testing.events.EventObserver.prototype.getEvents = function(opt_type) {
var events = goog.array.clone(this.events_);
if (opt_type) {
events = goog.array.filter(events, function(event) {
return event.type == opt_type;
});
}
return events;
};
@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2010 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>
Closure Unit Tests - goog.testing.events.EventObserver
</title>
<script src="../../base.js">
</script>
<script>
goog.require('goog.testing.events.EventObserverTest');
</script>
</head>
<body>
</body>
</html>
@@ -0,0 +1,67 @@
// Copyright 2010 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.
goog.provide('goog.testing.events.EventObserverTest');
goog.setTestOnly('goog.testing.events.EventObserverTest');
goog.require('goog.array');
goog.require('goog.events');
goog.require('goog.events.Event');
goog.require('goog.events.EventTarget');
goog.require('goog.testing.events.EventObserver');
goog.require('goog.testing.jsunit');
// Return an event's type
function getEventType(e) {
return e.type;
}
function testGetEvents() {
var observer = new goog.testing.events.EventObserver();
var target = new goog.events.EventTarget();
goog.events.listen(target, ['foo', 'bar', 'baz'], observer);
var eventTypes = [
'bar', 'baz', 'foo', 'qux', 'quux', 'corge', 'foo', 'baz'];
goog.array.forEach(eventTypes, goog.bind(target.dispatchEvent, target));
var replayEvents = observer.getEvents();
assertArrayEquals('Only the listened-for event types should be remembered',
['bar', 'baz', 'foo', 'foo', 'baz'],
goog.array.map(observer.getEvents(), getEventType));
assertArrayEquals(['bar'],
goog.array.map(observer.getEvents('bar'), getEventType));
assertArrayEquals(['baz', 'baz'],
goog.array.map(observer.getEvents('baz'), getEventType));
assertArrayEquals(['foo', 'foo'],
goog.array.map(observer.getEvents('foo'), getEventType));
}
function testHandleEvent() {
var events = [
new goog.events.Event('foo'),
new goog.events.Event('bar'),
new goog.events.Event('baz')
];
var observer = new goog.testing.events.EventObserver();
goog.array.forEach(events, goog.bind(observer.handleEvent, observer));
assertArrayEquals(events, observer.getEvents());
assertArrayEquals([events[0]], observer.getEvents('foo'));
assertArrayEquals([events[1]], observer.getEvents('bar'));
assertArrayEquals([events[2]], observer.getEvents('baz'));
}
@@ -0,0 +1,727 @@
// 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 Event Simulation.
*
* Utility functions for simulating events at the Closure level. All functions
* in this package generate events by calling goog.events.fireListeners,
* rather than interfacing with the browser directly. This is intended for
* testing purposes, and should not be used in production code.
*
* The decision to use Closure events and dispatchers instead of the browser's
* native events and dispatchers was conscious and deliberate. Native event
* dispatchers have their own set of quirks and edge cases. Pure JS dispatchers
* are more robust and transparent.
*
* If you think you need a testing mechanism that uses native Event objects,
* please, please email closure-tech first to explain your use case before you
* sink time into this.
*
* @author nicksantos@google.com (Nick Santos)
*/
goog.provide('goog.testing.events');
goog.provide('goog.testing.events.Event');
goog.require('goog.Disposable');
goog.require('goog.asserts');
goog.require('goog.dom.NodeType');
goog.require('goog.events');
goog.require('goog.events.BrowserEvent');
goog.require('goog.events.BrowserFeature');
goog.require('goog.events.EventTarget');
goog.require('goog.events.EventType');
goog.require('goog.events.KeyCodes');
goog.require('goog.object');
goog.require('goog.style');
goog.require('goog.userAgent');
/**
* goog.events.BrowserEvent expects an Event so we provide one for JSCompiler.
*
* This clones a lot of the functionality of goog.events.Event. This used to
* use a mixin, but the mixin results in confusing the two types when compiled.
*
* @param {string} type Event Type.
* @param {Object=} opt_target Reference to the object that is the target of
* this event.
* @constructor
* @extends {Event}
*/
goog.testing.events.Event = function(type, opt_target) {
this.type = type;
this.target = /** @type {EventTarget} */ (opt_target || null);
this.currentTarget = this.target;
};
/**
* Whether to cancel the event in internal capture/bubble processing for IE.
* @type {boolean}
* @public
* @suppress {underscore|visibility} Technically public, but referencing this
* outside this package is strongly discouraged.
*/
goog.testing.events.Event.prototype.propagationStopped_ = false;
/** @override */
goog.testing.events.Event.prototype.defaultPrevented = false;
/**
* Return value for in internal capture/bubble processing for IE.
* @type {boolean}
* @public
* @suppress {underscore|visibility} Technically public, but referencing this
* outside this package is strongly discouraged.
*/
goog.testing.events.Event.prototype.returnValue_ = true;
/** @override */
goog.testing.events.Event.prototype.stopPropagation = function() {
this.propagationStopped_ = true;
};
/** @override */
goog.testing.events.Event.prototype.preventDefault = function() {
this.defaultPrevented = true;
this.returnValue_ = false;
};
/**
* Asserts an event target exists. This will fail if target is not defined.
*
* TODO(nnaze): Gradually add this to the methods in this file, and eventually
* update the method signatures to not take nullables. See http://b/8961907
*
* @param {EventTarget} target A target to assert.
* @return {!EventTarget} The target, guaranteed to exist.
* @private
*/
goog.testing.events.assertEventTarget_ = function(target) {
return goog.asserts.assert(target, 'EventTarget should be defined.');
};
/**
* A static helper function that sets the mouse position to the event.
* @param {Event} event A simulated native event.
* @param {goog.math.Coordinate=} opt_coords Mouse position. Defaults to event's
* target's position (if available), otherwise (0, 0).
* @private
*/
goog.testing.events.setEventClientXY_ = function(event, opt_coords) {
if (!opt_coords && event.target &&
event.target.nodeType == goog.dom.NodeType.ELEMENT) {
try {
opt_coords =
goog.style.getClientPosition(/** @type {!Element} **/ (event.target));
} catch (ex) {
// IE sometimes throws if it can't get the position.
}
}
event.clientX = opt_coords ? opt_coords.x : 0;
event.clientY = opt_coords ? opt_coords.y : 0;
// Pretend the browser window is at (0, 0).
event.screenX = event.clientX;
event.screenY = event.clientY;
};
/**
* Simulates a mousedown, mouseup, and then click on the given event target,
* with the left mouse button.
* @param {EventTarget} target The target for the event.
* @param {goog.events.BrowserEvent.MouseButton=} opt_button Mouse button;
* defaults to {@code goog.events.BrowserEvent.MouseButton.LEFT}.
* @param {goog.math.Coordinate=} opt_coords Mouse position. Defaults to event's
* target's position (if available), otherwise (0, 0).
* @param {Object=} opt_eventProperties Event properties to be mixed into the
* BrowserEvent.
* @return {boolean} The returnValue of the sequence: false if preventDefault()
* was called on any of the events, true otherwise.
*/
goog.testing.events.fireClickSequence =
function(target, opt_button, opt_coords, opt_eventProperties) {
// Fire mousedown, mouseup, and click. Then return the bitwise AND of the 3.
return !!(goog.testing.events.fireMouseDownEvent(
target, opt_button, opt_coords, opt_eventProperties) &
goog.testing.events.fireMouseUpEvent(
target, opt_button, opt_coords, opt_eventProperties) &
goog.testing.events.fireClickEvent(
target, opt_button, opt_coords, opt_eventProperties));
};
/**
* Simulates the sequence of events fired by the browser when the user double-
* clicks the given target.
* @param {EventTarget} target The target for the event.
* @param {goog.math.Coordinate=} opt_coords Mouse position. Defaults to event's
* target's position (if available), otherwise (0, 0).
* @param {Object=} opt_eventProperties Event properties to be mixed into the
* BrowserEvent.
* @return {boolean} The returnValue of the sequence: false if preventDefault()
* was called on any of the events, true otherwise.
*/
goog.testing.events.fireDoubleClickSequence = function(
target, opt_coords, opt_eventProperties) {
// Fire mousedown, mouseup, click, mousedown, mouseup, click, dblclick.
// Then return the bitwise AND of the 7.
var btn = goog.events.BrowserEvent.MouseButton.LEFT;
return !!(goog.testing.events.fireMouseDownEvent(
target, btn, opt_coords, opt_eventProperties) &
goog.testing.events.fireMouseUpEvent(
target, btn, opt_coords, opt_eventProperties) &
goog.testing.events.fireClickEvent(
target, btn, opt_coords, opt_eventProperties) &
// IE fires a selectstart instead of the second mousedown in a
// dblclick, but we don't care about selectstart.
(goog.userAgent.IE ||
goog.testing.events.fireMouseDownEvent(
target, btn, opt_coords, opt_eventProperties)) &
goog.testing.events.fireMouseUpEvent(
target, btn, opt_coords, opt_eventProperties) &
// IE doesn't fire the second click in a dblclick.
(goog.userAgent.IE ||
goog.testing.events.fireClickEvent(
target, btn, opt_coords, opt_eventProperties)) &
goog.testing.events.fireDoubleClickEvent(
target, opt_coords, opt_eventProperties));
};
/**
* Simulates a complete keystroke (keydown, keypress, and keyup). Note that
* if preventDefault is called on the keydown, the keypress will not fire.
*
* @param {EventTarget} target The target for the event.
* @param {number} keyCode The keycode of the key pressed.
* @param {Object=} opt_eventProperties Event properties to be mixed into the
* BrowserEvent.
* @return {boolean} The returnValue of the sequence: false if preventDefault()
* was called on any of the events, true otherwise.
*/
goog.testing.events.fireKeySequence = function(
target, keyCode, opt_eventProperties) {
return goog.testing.events.fireNonAsciiKeySequence(target, keyCode, keyCode,
opt_eventProperties);
};
/**
* Simulates a complete keystroke (keydown, keypress, and keyup) when typing
* a non-ASCII character. Same as fireKeySequence, the keypress will not fire
* if preventDefault is called on the keydown.
*
* @param {EventTarget} target The target for the event.
* @param {number} keyCode The keycode of the keydown and keyup events.
* @param {number} keyPressKeyCode The keycode of the keypress event.
* @param {Object=} opt_eventProperties Event properties to be mixed into the
* BrowserEvent.
* @return {boolean} The returnValue of the sequence: false if preventDefault()
* was called on any of the events, true otherwise.
*/
goog.testing.events.fireNonAsciiKeySequence = function(
target, keyCode, keyPressKeyCode, opt_eventProperties) {
var keydown =
new goog.testing.events.Event(goog.events.EventType.KEYDOWN, target);
var keyup =
new goog.testing.events.Event(goog.events.EventType.KEYUP, target);
var keypress =
new goog.testing.events.Event(goog.events.EventType.KEYPRESS, target);
keydown.keyCode = keyup.keyCode = keyCode;
keypress.keyCode = keyPressKeyCode;
if (opt_eventProperties) {
goog.object.extend(keydown, opt_eventProperties);
goog.object.extend(keyup, opt_eventProperties);
goog.object.extend(keypress, opt_eventProperties);
}
// Fire keydown, keypress, and keyup. Note that if the keydown is
// prevent-defaulted, then the keypress will not fire.
var result = true;
if (!goog.testing.events.isBrokenGeckoMacActionKey_(keydown)) {
result = goog.testing.events.fireBrowserEvent(keydown);
}
if (goog.events.KeyCodes.firesKeyPressEvent(
keyCode, undefined, keydown.shiftKey, keydown.ctrlKey,
keydown.altKey) && result) {
result &= goog.testing.events.fireBrowserEvent(keypress);
}
return !!(result & goog.testing.events.fireBrowserEvent(keyup));
};
/**
* @param {goog.testing.events.Event} e The event.
* @return {boolean} Whether this is the Gecko/Mac's Meta-C/V/X, which
* is broken and requires special handling.
* @private
*/
goog.testing.events.isBrokenGeckoMacActionKey_ = function(e) {
return goog.userAgent.MAC && goog.userAgent.GECKO &&
(e.keyCode == goog.events.KeyCodes.C ||
e.keyCode == goog.events.KeyCodes.X ||
e.keyCode == goog.events.KeyCodes.V) && e.metaKey;
};
/**
* Simulates a mouseover event on the given target.
* @param {EventTarget} target The target for the event.
* @param {EventTarget} relatedTarget The related target for the event (e.g.,
* the node that the mouse is being moved out of).
* @param {goog.math.Coordinate=} opt_coords Mouse position. Defaults to event's
* target's position (if available), otherwise (0, 0).
* @return {boolean} The returnValue of the event: false if preventDefault() was
* called on it, true otherwise.
*/
goog.testing.events.fireMouseOverEvent = function(target, relatedTarget,
opt_coords) {
var mouseover =
new goog.testing.events.Event(goog.events.EventType.MOUSEOVER, target);
mouseover.relatedTarget = relatedTarget;
goog.testing.events.setEventClientXY_(mouseover, opt_coords);
return goog.testing.events.fireBrowserEvent(mouseover);
};
/**
* Simulates a mousemove event on the given target.
* @param {EventTarget} target The target for the event.
* @param {goog.math.Coordinate=} opt_coords Mouse position. Defaults to event's
* target's position (if available), otherwise (0, 0).
* @return {boolean} The returnValue of the event: false if preventDefault() was
* called on it, true otherwise.
*/
goog.testing.events.fireMouseMoveEvent = function(target, opt_coords) {
var mousemove =
new goog.testing.events.Event(goog.events.EventType.MOUSEMOVE, target);
goog.testing.events.setEventClientXY_(mousemove, opt_coords);
return goog.testing.events.fireBrowserEvent(mousemove);
};
/**
* Simulates a mouseout event on the given target.
* @param {EventTarget} target The target for the event.
* @param {EventTarget} relatedTarget The related target for the event (e.g.,
* the node that the mouse is being moved into).
* @param {goog.math.Coordinate=} opt_coords Mouse position. Defaults to event's
* target's position (if available), otherwise (0, 0).
* @return {boolean} The returnValue of the event: false if preventDefault() was
* called on it, true otherwise.
*/
goog.testing.events.fireMouseOutEvent = function(target, relatedTarget,
opt_coords) {
var mouseout =
new goog.testing.events.Event(goog.events.EventType.MOUSEOUT, target);
mouseout.relatedTarget = relatedTarget;
goog.testing.events.setEventClientXY_(mouseout, opt_coords);
return goog.testing.events.fireBrowserEvent(mouseout);
};
/**
* Simulates a mousedown event on the given target.
* @param {EventTarget} target The target for the event.
* @param {goog.events.BrowserEvent.MouseButton=} opt_button Mouse button;
* defaults to {@code goog.events.BrowserEvent.MouseButton.LEFT}.
* @param {goog.math.Coordinate=} opt_coords Mouse position. Defaults to event's
* target's position (if available), otherwise (0, 0).
* @param {Object=} opt_eventProperties Event properties to be mixed into the
* BrowserEvent.
* @return {boolean} The returnValue of the event: false if preventDefault() was
* called on it, true otherwise.
*/
goog.testing.events.fireMouseDownEvent =
function(target, opt_button, opt_coords, opt_eventProperties) {
var button = opt_button || goog.events.BrowserEvent.MouseButton.LEFT;
button = !goog.events.BrowserFeature.HAS_W3C_BUTTON ?
goog.events.BrowserEvent.IEButtonMap[button] : button;
return goog.testing.events.fireMouseButtonEvent_(
goog.events.EventType.MOUSEDOWN, target, button, opt_coords,
opt_eventProperties);
};
/**
* Simulates a mouseup event on the given target.
* @param {EventTarget} target The target for the event.
* @param {goog.events.BrowserEvent.MouseButton=} opt_button Mouse button;
* defaults to {@code goog.events.BrowserEvent.MouseButton.LEFT}.
* @param {goog.math.Coordinate=} opt_coords Mouse position. Defaults to event's
* target's position (if available), otherwise (0, 0).
* @param {Object=} opt_eventProperties Event properties to be mixed into the
* BrowserEvent.
* @return {boolean} The returnValue of the event: false if preventDefault() was
* called on it, true otherwise.
*/
goog.testing.events.fireMouseUpEvent =
function(target, opt_button, opt_coords, opt_eventProperties) {
var button = opt_button || goog.events.BrowserEvent.MouseButton.LEFT;
button = !goog.events.BrowserFeature.HAS_W3C_BUTTON ?
goog.events.BrowserEvent.IEButtonMap[button] : button;
return goog.testing.events.fireMouseButtonEvent_(
goog.events.EventType.MOUSEUP, target, button, opt_coords,
opt_eventProperties);
};
/**
* Simulates a click event on the given target. IE only supports click with
* the left mouse button.
* @param {EventTarget} target The target for the event.
* @param {goog.events.BrowserEvent.MouseButton=} opt_button Mouse button;
* defaults to {@code goog.events.BrowserEvent.MouseButton.LEFT}.
* @param {goog.math.Coordinate=} opt_coords Mouse position. Defaults to event's
* target's position (if available), otherwise (0, 0).
* @param {Object=} opt_eventProperties Event properties to be mixed into the
* BrowserEvent.
* @return {boolean} The returnValue of the event: false if preventDefault() was
* called on it, true otherwise.
*/
goog.testing.events.fireClickEvent =
function(target, opt_button, opt_coords, opt_eventProperties) {
return goog.testing.events.fireMouseButtonEvent_(goog.events.EventType.CLICK,
target, opt_button, opt_coords, opt_eventProperties);
};
/**
* Simulates a double-click event on the given target. Always double-clicks
* with the left mouse button since no browser supports double-clicking with
* any other buttons.
* @param {EventTarget} target The target for the event.
* @param {goog.math.Coordinate=} opt_coords Mouse position. Defaults to event's
* target's position (if available), otherwise (0, 0).
* @param {Object=} opt_eventProperties Event properties to be mixed into the
* BrowserEvent.
* @return {boolean} The returnValue of the event: false if preventDefault() was
* called on it, true otherwise.
*/
goog.testing.events.fireDoubleClickEvent =
function(target, opt_coords, opt_eventProperties) {
return goog.testing.events.fireMouseButtonEvent_(
goog.events.EventType.DBLCLICK, target,
goog.events.BrowserEvent.MouseButton.LEFT, opt_coords,
opt_eventProperties);
};
/**
* Helper function to fire a mouse event.
* with the left mouse button since no browser supports double-clicking with
* any other buttons.
* @param {string} type The event type.
* @param {EventTarget} target The target for the event.
* @param {number=} opt_button Mouse button; defaults to
* {@code goog.events.BrowserEvent.MouseButton.LEFT}.
* @param {goog.math.Coordinate=} opt_coords Mouse position. Defaults to event's
* target's position (if available), otherwise (0, 0).
* @param {Object=} opt_eventProperties Event properties to be mixed into the
* BrowserEvent.
* @return {boolean} The returnValue of the event: false if preventDefault() was
* called on it, true otherwise.
* @private
*/
goog.testing.events.fireMouseButtonEvent_ =
function(type, target, opt_button, opt_coords, opt_eventProperties) {
var e =
new goog.testing.events.Event(type, target);
e.button = opt_button || goog.events.BrowserEvent.MouseButton.LEFT;
goog.testing.events.setEventClientXY_(e, opt_coords);
if (opt_eventProperties) {
goog.object.extend(e, opt_eventProperties);
}
return goog.testing.events.fireBrowserEvent(e);
};
/**
* Simulates a contextmenu event on the given target.
* @param {EventTarget} target The target for the event.
* @param {goog.math.Coordinate=} opt_coords Mouse position. Defaults to event's
* target's position (if available), otherwise (0, 0).
* @return {boolean} The returnValue of the event: false if preventDefault() was
* called on it, true otherwise.
*/
goog.testing.events.fireContextMenuEvent = function(target, opt_coords) {
var button = (goog.userAgent.MAC && goog.userAgent.WEBKIT) ?
goog.events.BrowserEvent.MouseButton.LEFT :
goog.events.BrowserEvent.MouseButton.RIGHT;
var contextmenu =
new goog.testing.events.Event(goog.events.EventType.CONTEXTMENU, target);
contextmenu.button = !goog.events.BrowserFeature.HAS_W3C_BUTTON ?
goog.events.BrowserEvent.IEButtonMap[button] : button;
contextmenu.ctrlKey = goog.userAgent.MAC;
goog.testing.events.setEventClientXY_(contextmenu, opt_coords);
return goog.testing.events.fireBrowserEvent(contextmenu);
};
/**
* Simulates a mousedown, contextmenu, and the mouseup on the given event
* target, with the right mouse button.
* @param {EventTarget} target The target for the event.
* @param {goog.math.Coordinate=} opt_coords Mouse position. Defaults to event's
* target's position (if available), otherwise (0, 0).
* @return {boolean} The returnValue of the sequence: false if preventDefault()
* was called on any of the events, true otherwise.
*/
goog.testing.events.fireContextMenuSequence = function(target, opt_coords) {
var props = goog.userAgent.MAC ? {ctrlKey: true} : {};
var button = (goog.userAgent.MAC && goog.userAgent.WEBKIT) ?
goog.events.BrowserEvent.MouseButton.LEFT :
goog.events.BrowserEvent.MouseButton.RIGHT;
var result = goog.testing.events.fireMouseDownEvent(target,
button, opt_coords, props);
if (goog.userAgent.WINDOWS) {
// All browsers are consistent on Windows.
result &= goog.testing.events.fireMouseUpEvent(target,
button, opt_coords) &
goog.testing.events.fireContextMenuEvent(target, opt_coords);
} else {
result &= goog.testing.events.fireContextMenuEvent(target, opt_coords);
// GECKO on Mac and Linux always fires the mouseup after the contextmenu.
// WEBKIT is really weird.
//
// On Linux, it sometimes fires mouseup, but most of the time doesn't.
// It's really hard to reproduce consistently. I think there's some
// internal race condition. If contextmenu is preventDefaulted, then
// mouseup always fires.
//
// On Mac, it always fires mouseup and then fires a click.
result &= goog.testing.events.fireMouseUpEvent(target,
button, opt_coords, props);
if (goog.userAgent.WEBKIT && goog.userAgent.MAC) {
result &= goog.testing.events.fireClickEvent(
target, button, opt_coords, props);
}
}
return !!result;
};
/**
* Simulates a popstate event on the given target.
* @param {EventTarget} target The target for the event.
* @param {Object} state History state object.
* @return {boolean} The returnValue of the event: false if preventDefault() was
* called on it, true otherwise.
*/
goog.testing.events.firePopStateEvent = function(target, state) {
var e = new goog.testing.events.Event(goog.events.EventType.POPSTATE, target);
e.state = state;
return goog.testing.events.fireBrowserEvent(e);
};
/**
* Simulate a blur event on the given target.
* @param {EventTarget} target The target for the event.
* @return {boolean} The value returned by firing the blur browser event,
* which returns false iff 'preventDefault' was invoked.
*/
goog.testing.events.fireBlurEvent = function(target) {
var e = new goog.testing.events.Event(
goog.events.EventType.BLUR, target);
return goog.testing.events.fireBrowserEvent(e);
};
/**
* Simulate a focus event on the given target.
* @param {EventTarget} target The target for the event.
* @return {boolean} The value returned by firing the focus browser event,
* which returns false iff 'preventDefault' was invoked.
*/
goog.testing.events.fireFocusEvent = function(target) {
var e = new goog.testing.events.Event(
goog.events.EventType.FOCUS, target);
return goog.testing.events.fireBrowserEvent(e);
};
/**
* Simulates an event's capturing and bubbling phases.
* @param {Event} event A simulated native event. It will be wrapped in a
* normalized BrowserEvent and dispatched to Closure listeners on all
* ancestors of its target (inclusive).
* @return {boolean} The returnValue of the event: false if preventDefault() was
* called on it, true otherwise.
*/
goog.testing.events.fireBrowserEvent = function(event) {
event.returnValue_ = true;
// generate a list of ancestors
var ancestors = [];
for (var current = event.target; current; current = current.parentNode) {
ancestors.push(current);
}
// dispatch capturing listeners
for (var j = ancestors.length - 1;
j >= 0 && !event.propagationStopped_;
j--) {
goog.events.fireListeners(ancestors[j], event.type, true,
new goog.events.BrowserEvent(event, ancestors[j]));
}
// dispatch bubbling listeners
for (var j = 0;
j < ancestors.length && !event.propagationStopped_;
j++) {
goog.events.fireListeners(ancestors[j], event.type, false,
new goog.events.BrowserEvent(event, ancestors[j]));
}
return event.returnValue_;
};
/**
* Simulates a touchstart event on the given target.
* @param {EventTarget} target The target for the event.
* @param {goog.math.Coordinate=} opt_coords Touch position. Defaults to event's
* target's position (if available), otherwise (0, 0).
* @param {Object=} opt_eventProperties Event properties to be mixed into the
* BrowserEvent.
* @return {boolean} The returnValue of the event: false if preventDefault() was
* called on it, true otherwise.
*/
goog.testing.events.fireTouchStartEvent = function(
target, opt_coords, opt_eventProperties) {
// TODO: Support multi-touch events with array of coordinates.
var touchstart =
new goog.testing.events.Event(goog.events.EventType.TOUCHSTART, target);
goog.testing.events.setEventClientXY_(touchstart, opt_coords);
if (opt_eventProperties) {
goog.object.extend(touchstart, opt_eventProperties);
}
return goog.testing.events.fireBrowserEvent(touchstart);
};
/**
* Simulates a touchmove event on the given target.
* @param {EventTarget} target The target for the event.
* @param {goog.math.Coordinate=} opt_coords Touch position. Defaults to event's
* target's position (if available), otherwise (0, 0).
* @param {Object=} opt_eventProperties Event properties to be mixed into the
* BrowserEvent.
* @return {boolean} The returnValue of the event: false if preventDefault() was
* called on it, true otherwise.
*/
goog.testing.events.fireTouchMoveEvent = function(
target, opt_coords, opt_eventProperties) {
// TODO: Support multi-touch events with array of coordinates.
var touchmove =
new goog.testing.events.Event(goog.events.EventType.TOUCHMOVE, target);
goog.testing.events.setEventClientXY_(touchmove, opt_coords);
if (opt_eventProperties) {
goog.object.extend(touchmove, opt_eventProperties);
}
return goog.testing.events.fireBrowserEvent(touchmove);
};
/**
* Simulates a touchend event on the given target.
* @param {EventTarget} target The target for the event.
* @param {goog.math.Coordinate=} opt_coords Touch position. Defaults to event's
* target's position (if available), otherwise (0, 0).
* @param {Object=} opt_eventProperties Event properties to be mixed into the
* BrowserEvent.
* @return {boolean} The returnValue of the event: false if preventDefault() was
* called on it, true otherwise.
*/
goog.testing.events.fireTouchEndEvent = function(
target, opt_coords, opt_eventProperties) {
// TODO: Support multi-touch events with array of coordinates.
var touchend =
new goog.testing.events.Event(goog.events.EventType.TOUCHEND, target);
goog.testing.events.setEventClientXY_(touchend, opt_coords);
if (opt_eventProperties) {
goog.object.extend(touchend, opt_eventProperties);
}
return goog.testing.events.fireBrowserEvent(touchend);
};
/**
* Simulates a simple touch sequence on the given target.
* @param {EventTarget} target The target for the event.
* @param {goog.math.Coordinate=} opt_coords Touch position. Defaults to event
* target's position (if available), otherwise (0, 0).
* @param {Object=} opt_eventProperties Event properties to be mixed into the
* BrowserEvent.
* @return {boolean} The returnValue of the sequence: false if preventDefault()
* was called on any of the events, true otherwise.
*/
goog.testing.events.fireTouchSequence = function(
target, opt_coords, opt_eventProperties) {
// TODO: Support multi-touch events with array of coordinates.
// Fire touchstart, touchmove, touchend then return the bitwise AND of the 3.
return !!(goog.testing.events.fireTouchStartEvent(
target, opt_coords, opt_eventProperties) &
goog.testing.events.fireTouchEndEvent(
target, opt_coords, opt_eventProperties));
};
/**
* Mixins a listenable into the given object. This turns the object
* into a goog.events.Listenable. This is useful, for example, when
* you need to mock a implementation of listenable and still want it
* to work with goog.events.
* @param {!Object} obj The object to mixin into.
*/
goog.testing.events.mixinListenable = function(obj) {
var listenable = new goog.events.EventTarget();
listenable.setTargetForTesting(obj);
var listenablePrototype = goog.events.EventTarget.prototype;
var disposablePrototype = goog.Disposable.prototype;
for (var key in listenablePrototype) {
if (listenablePrototype.hasOwnProperty(key) ||
disposablePrototype.hasOwnProperty(key)) {
var member = listenablePrototype[key];
if (goog.isFunction(member)) {
obj[key] = goog.bind(member, listenable);
} else {
obj[key] = member;
}
}
}
};
@@ -0,0 +1,41 @@
<!--
Copyright 2008 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<!--
Author: nicksantos@google.com (Nick Santos)
-->
<title>
Closure Unit Tests - goog.testing.events
</title>
<script src="../../base.js">
</script>
<script>
goog.require('goog.testing.eventsTest');
</script>
</head>
<body>
<div id="root">
</div>
<input id="testButton" type="input" value="Click Me" />
<div id="input">
Prevent Default these events:
<br />
</div>
<div id="log" style="position:absolute;right:0;top:0">
Logged events:
</div>
<div id="parentEl">
<div id="childEl">
hello!
</div>
</div>
</body>
</html>
@@ -0,0 +1,624 @@
// 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.
goog.provide('goog.testing.eventsTest');
goog.setTestOnly('goog.testing.eventsTest');
goog.require('goog.array');
goog.require('goog.dom');
goog.require('goog.events');
goog.require('goog.events.EventType');
goog.require('goog.events.KeyCodes');
goog.require('goog.math.Coordinate');
goog.require('goog.string');
goog.require('goog.style');
goog.require('goog.testing.PropertyReplacer');
goog.require('goog.testing.events');
goog.require('goog.testing.jsunit');
goog.require('goog.testing.recordFunction');
goog.require('goog.userAgent');
var firedEventTypes;
var firedEventCoordinates;
var firedScreenCoordinates;
var firedShiftKeys;
var firedKeyCodes;
var root;
var log;
var input;
var testButton;
var parentEl;
var childEl;
var coordinate = new goog.math.Coordinate(123, 456);
var stubs = new goog.testing.PropertyReplacer();
var eventCount;
function setUpPage() {
root = goog.dom.getElement('root');
log = goog.dom.getElement('log');
input = goog.dom.getElement('input');
testButton = goog.dom.getElement('testButton');
parentEl = goog.dom.getElement('parentEl');
childEl = goog.dom.getElement('childEl');
}
function setUp() {
stubs.reset();
goog.events.removeAll(root);
goog.events.removeAll(log);
goog.events.removeAll(input);
goog.events.removeAll(testButton);
goog.events.removeAll(parentEl);
goog.events.removeAll(childEl);
root.innerHTML = '';
firedEventTypes = [];
firedEventCoordinates = [];
firedScreenCoordinates = [];
firedShiftKeys = [];
firedKeyCodes = [];
for (var key in goog.events.EventType) {
goog.events.listen(root, goog.events.EventType[key], function(e) {
firedEventTypes.push(e.type);
var coord = new goog.math.Coordinate(e.clientX, e.clientY);
firedEventCoordinates.push(coord);
firedScreenCoordinates.push(
new goog.math.Coordinate(e.screenX, e.screenY));
firedShiftKeys.push(!!e.shiftKey);
firedKeyCodes.push(e.keyCode);
});
}
eventCount = {
parentBubble: 0,
parentCapture: 0,
childCapture: 0,
childBubble: 0
};
// Event listeners for the capture/bubble test.
goog.events.listen(parentEl, goog.events.EventType.CLICK,
function(e) {
eventCount.parentCapture++;
assertEquals(parentEl, e.currentTarget);
assertEquals(childEl, e.target);
}, true);
goog.events.listen(childEl, goog.events.EventType.CLICK,
function(e) {
eventCount.childCapture++;
assertEquals(childEl, e.currentTarget);
assertEquals(childEl, e.target);
}, true);
goog.events.listen(childEl, goog.events.EventType.CLICK,
function(e) {
eventCount.childBubble++;
assertEquals(childEl, e.currentTarget);
assertEquals(childEl, e.target);
});
goog.events.listen(parentEl, goog.events.EventType.CLICK,
function(e) {
eventCount.parentBubble++;
assertEquals(parentEl, e.currentTarget);
assertEquals(childEl, e.target);
});
}
function tearDownPage() {
for (var key in goog.events.EventType) {
var type = goog.events.EventType[key];
if (type == 'mousemove' || type == 'mouseout' || type == 'mouseover') {
continue;
}
goog.dom.appendChild(input,
goog.dom.createDom('label', null,
goog.dom.createDom('input',
{'id': type, 'type': 'checkbox'}),
type,
goog.dom.createDom('br')));
goog.events.listen(testButton, type, function(e) {
if (goog.dom.getElement(e.type).checked) {
e.preventDefault();
}
log.innerHTML += goog.string.subs('<br />%s (%s, %s)',
e.type, e.clientX, e.clientY);
});
}
}
function testMouseOver() {
goog.testing.events.fireMouseOverEvent(root, null);
goog.testing.events.fireMouseOverEvent(root, null, coordinate);
assertEventTypes(['mouseover', 'mouseover']);
assertCoordinates([goog.style.getClientPosition(root), coordinate]);
}
function testMouseOut() {
goog.testing.events.fireMouseOutEvent(root, null);
goog.testing.events.fireMouseOutEvent(root, null, coordinate);
assertEventTypes(['mouseout', 'mouseout']);
assertCoordinates([goog.style.getClientPosition(root), coordinate]);
}
function testFocus() {
goog.testing.events.fireFocusEvent(root);
assertEventTypes(['focus']);
}
function testBlur() {
goog.testing.events.fireBlurEvent(root);
assertEventTypes(['blur']);
}
function testClickSequence() {
assertTrue(goog.testing.events.fireClickSequence(root));
assertEventTypes(['mousedown', 'mouseup', 'click']);
var rootPosition = goog.style.getClientPosition(root);
assertCoordinates([rootPosition, rootPosition, rootPosition]);
}
function testClickSequenceWithCoordinate() {
assertTrue(goog.testing.events.fireClickSequence(root, null, coordinate));
assertCoordinates([coordinate, coordinate, coordinate]);
assertArrayEquals([false, false, false], firedShiftKeys);
}
function testTouchStart() {
goog.testing.events.fireTouchStartEvent(root);
goog.testing.events.fireTouchStartEvent(root, coordinate);
assertEventTypes(['touchstart', 'touchstart']);
assertCoordinates([goog.style.getClientPosition(root), coordinate]);
}
function testTouchMove() {
goog.testing.events.fireTouchMoveEvent(root);
goog.testing.events.fireTouchMoveEvent(root, coordinate, {touches: []});
assertEventTypes(['touchmove', 'touchmove']);
assertCoordinates([goog.style.getClientPosition(root), coordinate]);
}
function testTouchEnd() {
goog.testing.events.fireTouchEndEvent(root);
goog.testing.events.fireTouchEndEvent(root, coordinate);
assertEventTypes(['touchend', 'touchend']);
assertCoordinates([goog.style.getClientPosition(root), coordinate]);
}
function testTouchSequence() {
assertTrue(goog.testing.events.fireTouchSequence(root));
assertEventTypes(['touchstart', 'touchend']);
var rootPosition = goog.style.getClientPosition(root);
assertCoordinates([rootPosition, rootPosition]);
}
function testTouchSequenceWithCoordinate() {
assertTrue(goog.testing.events.fireTouchSequence(root, coordinate));
assertCoordinates([coordinate, coordinate]);
}
function testClickSequenceWithEventProperty() {
assertTrue(goog.testing.events.fireClickSequence(
root, null, undefined, { shiftKey: true }));
assertArrayEquals([true, true, true], firedShiftKeys);
}
function testClickSequenceCancellingMousedown() {
preventDefaultEventType('mousedown');
assertFalse(goog.testing.events.fireClickSequence(root));
assertEventTypes(['mousedown', 'mouseup', 'click']);
}
function testClickSequenceCancellingMousedownWithCoordinate() {
preventDefaultEventType('mousedown');
assertFalse(goog.testing.events.fireClickSequence(root, null, coordinate));
assertCoordinates([coordinate, coordinate, coordinate]);
}
function testClickSequenceCancellingMouseup() {
preventDefaultEventType('mouseup');
assertFalse(goog.testing.events.fireClickSequence(root));
assertEventTypes(['mousedown', 'mouseup', 'click']);
}
function testClickSequenceCancellingMouseupWithCoordinate() {
preventDefaultEventType('mouseup');
assertFalse(goog.testing.events.fireClickSequence(root, null, coordinate));
assertCoordinates([coordinate, coordinate, coordinate]);
}
function testClickSequenceCancellingClick() {
preventDefaultEventType('click');
assertFalse(goog.testing.events.fireClickSequence(root));
assertEventTypes(['mousedown', 'mouseup', 'click']);
}
function testClickSequenceCancellingClickWithCoordinate() {
preventDefaultEventType('click');
assertFalse(goog.testing.events.fireClickSequence(root, null, coordinate));
assertCoordinates([coordinate, coordinate, coordinate]);
}
// For a double click, IE fires selectstart instead of the second mousedown,
// but we don't simulate selectstart. Also, IE doesn't fire the second click.
var DBLCLICK_SEQ = (goog.userAgent.IE ?
['mousedown',
'mouseup',
'click',
'mouseup',
'dblclick'] :
['mousedown',
'mouseup',
'click',
'mousedown',
'mouseup',
'click',
'dblclick']);
var DBLCLICK_SEQ_COORDS = goog.array.repeat(coordinate, DBLCLICK_SEQ.length);
function testDoubleClickSequence() {
assertTrue(goog.testing.events.fireDoubleClickSequence(root));
assertEventTypes(DBLCLICK_SEQ);
}
function testDoubleClickSequenceWithCoordinate() {
assertTrue(goog.testing.events.fireDoubleClickSequence(root, coordinate));
assertCoordinates(DBLCLICK_SEQ_COORDS);
}
function testDoubleClickSequenceCancellingMousedown() {
preventDefaultEventType('mousedown');
assertFalse(goog.testing.events.fireDoubleClickSequence(root));
assertEventTypes(DBLCLICK_SEQ);
}
function testDoubleClickSequenceCancellingMousedownWithCoordinate() {
preventDefaultEventType('mousedown');
assertFalse(goog.testing.events.fireDoubleClickSequence(root, coordinate));
assertCoordinates(DBLCLICK_SEQ_COORDS);
}
function testDoubleClickSequenceCancellingMouseup() {
preventDefaultEventType('mouseup');
assertFalse(goog.testing.events.fireDoubleClickSequence(root));
assertEventTypes(DBLCLICK_SEQ);
}
function testDoubleClickSequenceCancellingMouseupWithCoordinate() {
preventDefaultEventType('mouseup');
assertFalse(goog.testing.events.fireDoubleClickSequence(root, coordinate));
assertCoordinates(DBLCLICK_SEQ_COORDS);
}
function testDoubleClickSequenceCancellingClick() {
preventDefaultEventType('click');
assertFalse(goog.testing.events.fireDoubleClickSequence(root));
assertEventTypes(DBLCLICK_SEQ);
}
function testDoubleClickSequenceCancellingClickWithCoordinate() {
preventDefaultEventType('click');
assertFalse(goog.testing.events.fireDoubleClickSequence(root, coordinate));
assertCoordinates(DBLCLICK_SEQ_COORDS);
}
function testDoubleClickSequenceCancellingDoubleClick() {
preventDefaultEventType('dblclick');
assertFalse(goog.testing.events.fireDoubleClickSequence(root));
assertEventTypes(DBLCLICK_SEQ);
}
function testDoubleClickSequenceCancellingDoubleClickWithCoordinate() {
preventDefaultEventType('dblclick');
assertFalse(goog.testing.events.fireDoubleClickSequence(root, coordinate));
assertCoordinates(DBLCLICK_SEQ_COORDS);
}
function testKeySequence() {
assertTrue(goog.testing.events.fireKeySequence(
root, goog.events.KeyCodes.ZERO));
assertEventTypes(['keydown', 'keypress', 'keyup']);
}
function testKeySequenceCancellingKeydown() {
preventDefaultEventType('keydown');
assertFalse(goog.testing.events.fireKeySequence(
root, goog.events.KeyCodes.ZERO));
assertEventTypes(['keydown', 'keyup']);
}
function testKeySequenceCancellingKeypress() {
preventDefaultEventType('keypress');
assertFalse(goog.testing.events.fireKeySequence(
root, goog.events.KeyCodes.ZERO));
assertEventTypes(['keydown', 'keypress', 'keyup']);
}
function testKeySequenceCancellingKeyup() {
preventDefaultEventType('keyup');
assertFalse(goog.testing.events.fireKeySequence(
root, goog.events.KeyCodes.ZERO));
assertEventTypes(['keydown', 'keypress', 'keyup']);
}
function testKeySequenceWithEscapeKey() {
assertTrue(goog.testing.events.fireKeySequence(
root, goog.events.KeyCodes.ESC));
if (goog.userAgent.WEBKIT && goog.userAgent.isVersionOrHigher('525')) {
assertEventTypes(['keydown', 'keyup']);
} else {
assertEventTypes(['keydown', 'keypress', 'keyup']);
}
}
function testKeySequenceForMacActionKeysNegative() {
stubs.set(goog.userAgent, 'GECKO', false);
goog.testing.events.fireKeySequence(
root, goog.events.KeyCodes.C, {'metaKey': true});
assertEventTypes(['keydown', 'keypress', 'keyup']);
}
function testKeySequenceForMacActionKeysPositive() {
stubs.set(goog.userAgent, 'GECKO', true);
stubs.set(goog.userAgent, 'MAC', true);
goog.testing.events.fireKeySequence(
root, goog.events.KeyCodes.C, {'metaKey': true});
assertEventTypes(['keypress', 'keyup']);
}
function testKeySequenceForOptionKeysOnMac() {
// Mac uses an option (or alt) key to type non-ASCII characters. This test
// verifies we can emulate key events sent when typing such non-ASCII
// characters.
stubs.set(goog.userAgent, 'WEBKIT', true);
stubs.set(goog.userAgent, 'MAC', true);
var optionKeyCodes = [
[0xc0, 0x00e6], // option+'
[0xbc, 0x2264], // option+,
[0xbd, 0x2013], // option+-
[0xbe, 0x2265], // option+.
[0xbf, 0x00f7], // option+/
[0x30, 0x00ba], // option+0
[0x31, 0x00a1], // option+1
[0x32, 0x2122], // option+2
[0x33, 0x00a3], // option+3
[0x34, 0x00a2], // option+4
[0x35, 0x221e], // option+5
[0x36, 0x00a7], // option+6
[0x37, 0x00b6], // option+7
[0x38, 0x2022], // option+8
[0x39, 0x00aa], // option+9
[0xba, 0x2026], // option+;
[0xbb, 0x2260], // option+=
[0xdb, 0x201c], // option+[
[0xdc, 0x00ab], // option+\
[0xdd, 0x2018], // option+]
[0x41, 0x00e5], // option+a
[0x42, 0x222b], // option+b
[0x43, 0x00e7], // option+c
[0x44, 0x2202], // option+d
[0x45, 0x00b4], // option+e
[0x46, 0x0192], // option+f
[0x47, 0x00a9], // option+g
[0x48, 0x02d9], // option+h
[0x49, 0x02c6], // option+i
[0x4a, 0x2206], // option+j
[0x4b, 0x02da], // option+k
[0x4c, 0x00ac], // option+l
[0x4d, 0x00b5], // option+m
[0x4e, 0x02dc], // option+n
[0x4f, 0x00f8], // option+o
[0x50, 0x03c0], // option+p
[0x51, 0x0153], // option+q
[0x52, 0x00ae], // option+r
[0x53, 0x00df], // option+s
[0x54, 0x2020], // option+t
[0x56, 0x221a], // option+v
[0x57, 0x2211], // option+w
[0x58, 0x2248], // option+x
[0x59, 0x00a5], // option+y
[0x5a, 0x03a9] // option+z
];
for (var i = 0; i < optionKeyCodes.length; ++i) {
firedEventTypes = [];
firedKeyCodes = [];
var keyCode = optionKeyCodes[i][0];
var keyPressKeyCode = optionKeyCodes[i][1];
goog.testing.events.fireNonAsciiKeySequence(
root, keyCode, keyPressKeyCode, {'altKey': true});
assertEventTypes(['keydown', 'keypress', 'keyup']);
assertArrayEquals([keyCode, keyPressKeyCode, keyCode], firedKeyCodes);
}
}
var CONTEXTMENU_SEQ =
goog.userAgent.WINDOWS ? ['mousedown', 'mouseup', 'contextmenu'] :
goog.userAgent.GECKO ? ['mousedown', 'contextmenu', 'mouseup'] :
goog.userAgent.WEBKIT && goog.userAgent.MAC ?
['mousedown', 'contextmenu', 'mouseup', 'click'] :
['mousedown', 'contextmenu', 'mouseup'];
function testContextMenuSequence() {
assertTrue(goog.testing.events.fireContextMenuSequence(root));
assertEventTypes(CONTEXTMENU_SEQ);
}
function testContextMenuSequenceWithCoordinate() {
assertTrue(goog.testing.events.fireContextMenuSequence(root, coordinate));
assertEventTypes(CONTEXTMENU_SEQ);
assertCoordinates(goog.array.repeat(coordinate, CONTEXTMENU_SEQ.length));
}
function testContextMenuSequenceCancellingMousedown() {
preventDefaultEventType('mousedown');
assertFalse(goog.testing.events.fireContextMenuSequence(root));
assertEventTypes(CONTEXTMENU_SEQ);
}
function testContextMenuSequenceCancellingMouseup() {
preventDefaultEventType('mouseup');
assertFalse(goog.testing.events.fireContextMenuSequence(root));
assertEventTypes(CONTEXTMENU_SEQ);
}
function testContextMenuSequenceCancellingContextMenu() {
preventDefaultEventType('contextmenu');
assertFalse(goog.testing.events.fireContextMenuSequence(root));
assertEventTypes(CONTEXTMENU_SEQ);
}
function testContextMenuSequenceFakeMacWebkit() {
stubs.set(goog.userAgent, 'WINDOWS', false);
stubs.set(goog.userAgent, 'MAC', true);
stubs.set(goog.userAgent, 'WEBKIT', true);
assertTrue(goog.testing.events.fireContextMenuSequence(root));
assertEventTypes(['mousedown', 'contextmenu', 'mouseup', 'click']);
}
function testCaptureBubble_simple() {
assertTrue(goog.testing.events.fireClickEvent(childEl));
assertObjectEquals({
parentCapture: 1,
childCapture: 1,
childBubble: 1,
parentBubble: 1
}, eventCount);
}
function testCaptureBubble_preventDefault() {
goog.events.listen(childEl, goog.events.EventType.CLICK,
function(e) {
e.preventDefault();
});
assertFalse(goog.testing.events.fireClickEvent(childEl));
assertObjectEquals({
parentCapture: 1,
childCapture: 1,
childBubble: 1,
parentBubble: 1
}, eventCount);
}
function testCaptureBubble_stopPropagationParentCapture() {
goog.events.listen(parentEl, goog.events.EventType.CLICK,
function(e) {
e.stopPropagation();
}, true /* capture */);
assertTrue(goog.testing.events.fireClickEvent(childEl));
assertObjectEquals({
parentCapture: 1,
childCapture: 0,
childBubble: 0,
parentBubble: 0
}, eventCount);
}
function testCaptureBubble_stopPropagationChildCapture() {
goog.events.listen(childEl, goog.events.EventType.CLICK,
function(e) {
e.stopPropagation();
}, true /* capture */);
assertTrue(goog.testing.events.fireClickEvent(childEl));
assertObjectEquals({
parentCapture: 1,
childCapture: 1,
childBubble: 0,
parentBubble: 0
}, eventCount);
}
function testCaptureBubble_stopPropagationChildBubble() {
goog.events.listen(childEl, goog.events.EventType.CLICK,
function(e) {
e.stopPropagation();
});
assertTrue(goog.testing.events.fireClickEvent(childEl));
assertObjectEquals({
parentCapture: 1,
childCapture: 1,
childBubble: 1,
parentBubble: 0
}, eventCount);
}
function testCaptureBubble_stopPropagationParentBubble() {
goog.events.listen(parentEl, goog.events.EventType.CLICK,
function(e) {
e.stopPropagation();
});
assertTrue(goog.testing.events.fireClickEvent(childEl));
assertObjectEquals({
parentCapture: 1,
childCapture: 1,
childBubble: 1,
parentBubble: 1
}, eventCount);
}
function testMixinListenable() {
var obj = {};
obj.doFoo = goog.testing.recordFunction();
goog.testing.events.mixinListenable(obj);
obj.doFoo();
assertEquals(1, obj.doFoo.getCallCount());
var handler = goog.testing.recordFunction();
goog.events.listen(obj, 'test', handler);
obj.dispatchEvent('test');
assertEquals(1, handler.getCallCount());
assertEquals(obj, handler.getLastCall().getArgument(0).target);
goog.events.unlisten(obj, 'test', handler);
obj.dispatchEvent('test');
assertEquals(1, handler.getCallCount());
goog.events.listen(obj, 'test', handler);
obj.dispose();
obj.dispatchEvent('test');
assertEquals(1, handler.getCallCount());
}
/**
* Assert that the list of events given was fired, in that order.
*/
function assertEventTypes(list) {
assertArrayEquals(list, firedEventTypes);
}
/**
* Assert that the list of event coordinates given was caught, in that order.
*/
function assertCoordinates(list) {
assertArrayEquals(list, firedEventCoordinates);
assertArrayEquals(list, firedScreenCoordinates);
}
/** Prevent default the event of the given type on the root element. */
function preventDefaultEventType(type) {
goog.events.listen(root, type, preventDefault);
}
function preventDefault(e) {
e.preventDefault();
}
@@ -0,0 +1,42 @@
// Copyright 2009 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 Mock matchers for event related arguments.
*/
goog.provide('goog.testing.events.EventMatcher');
goog.require('goog.events.Event');
goog.require('goog.testing.mockmatchers.ArgumentMatcher');
/**
* A matcher that verifies that an argument is a {@code goog.events.Event} of a
* particular type.
* @param {string} type The single type the event argument must be of.
* @constructor
* @extends {goog.testing.mockmatchers.ArgumentMatcher}
* @final
*/
goog.testing.events.EventMatcher = function(type) {
goog.testing.mockmatchers.ArgumentMatcher.call(this,
function(obj) {
return obj instanceof goog.events.Event &&
obj.type == type;
}, 'isEventOfType(' + type + ')');
};
goog.inherits(goog.testing.events.EventMatcher,
goog.testing.mockmatchers.ArgumentMatcher);
@@ -0,0 +1,25 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2009 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<!--
-->
<title>
Closure Unit Tests - goog.testing.events.EventMatcher
</title>
<script src="../../base.js">
</script>
<script>
goog.require('goog.testing.events.EventMatcherTest');
</script>
</head>
<body>
</body>
</html>
@@ -0,0 +1,35 @@
// Copyright 2009 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.
goog.provide('goog.testing.events.EventMatcherTest');
goog.setTestOnly('goog.testing.events.EventMatcherTest');
goog.require('goog.events.Event');
goog.require('goog.testing.events.EventMatcher');
goog.require('goog.testing.jsunit');
function testEventMatcher() {
var matcher = new goog.testing.events.EventMatcher('foo');
assertFalse(matcher.matches(undefined));
assertFalse(matcher.matches(null));
assertFalse(matcher.matches({type: 'foo'}));
assertFalse(matcher.matches(new goog.events.Event('bar')));
assertTrue(matcher.matches(new goog.events.Event('foo')));
var FooEvent = function() {
goog.events.Event.call(this, 'foo');
};
goog.inherits(FooEvent, goog.events.Event);
assertTrue(matcher.matches(new FooEvent()));
}
@@ -0,0 +1,65 @@
// Copyright 2012 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 NetworkStatusMonitor test double.
* @author dbk@google.com (David Barrett-Kahn)
*/
goog.provide('goog.testing.events.OnlineHandler');
goog.require('goog.events.EventTarget');
goog.require('goog.net.NetworkStatusMonitor');
/**
* NetworkStatusMonitor test double.
* @param {boolean} initialState The initial online state of the mock.
* @constructor
* @extends {goog.events.EventTarget}
* @implements {goog.net.NetworkStatusMonitor}
* @final
*/
goog.testing.events.OnlineHandler = function(initialState) {
goog.testing.events.OnlineHandler.base(this, 'constructor');
/**
* Whether the mock is online.
* @private {boolean}
*/
this.online_ = initialState;
};
goog.inherits(goog.testing.events.OnlineHandler, goog.events.EventTarget);
/** @override */
goog.testing.events.OnlineHandler.prototype.isOnline = function() {
return this.online_;
};
/**
* Sets the online state.
* @param {boolean} newOnlineState The new online state.
*/
goog.testing.events.OnlineHandler.prototype.setOnline =
function(newOnlineState) {
if (newOnlineState != this.online_) {
this.online_ = newOnlineState;
this.dispatchEvent(newOnlineState ?
goog.net.NetworkStatusMonitor.EventType.ONLINE :
goog.net.NetworkStatusMonitor.EventType.OFFLINE);
}
};
@@ -0,0 +1,26 @@
<!DOCTYPE html>
<!--
Copyright 2012 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<html>
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<!--
Author: dbk@google.com (David Barrett-Kahn)
-->
<title>
Closure Unit Tests - goog.testing.events
</title>
<script src="../../base.js">
</script>
<script>
goog.require('goog.testing.events.OnlineHandlerTest');
</script>
</head>
<body>
</body>
</html>
@@ -0,0 +1,84 @@
// Copyright 2012 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.
goog.provide('goog.testing.events.OnlineHandlerTest');
goog.setTestOnly('goog.testing.events.OnlineHandlerTest');
goog.require('goog.events');
goog.require('goog.net.NetworkStatusMonitor');
goog.require('goog.testing.events.EventObserver');
goog.require('goog.testing.events.OnlineHandler');
goog.require('goog.testing.jsunit');
var handler;
var observer;
function tearDown() {
handler = null;
observer = null;
}
function testInitialValue() {
createHandler(true);
assertEquals(true, handler.isOnline());
createHandler(false);
assertEquals(false, handler.isOnline());
}
function testStateChange() {
createHandler(true);
assertEventCounts(0 /* expectedOnlineEvents */,
0 /* expectedOfflineEvents */);
// Expect no events.
handler.setOnline(true);
assertEquals(true, handler.isOnline());
assertEventCounts(0 /* expectedOnlineEvents */,
0 /* expectedOfflineEvents */);
// Expect one offline event.
handler.setOnline(false);
assertEquals(false, handler.isOnline());
assertEventCounts(0 /* expectedOnlineEvents */,
1 /* expectedOfflineEvents */);
// Expect no events.
handler.setOnline(false);
assertEquals(false, handler.isOnline());
assertEventCounts(0 /* expectedOnlineEvents */,
1 /* expectedOfflineEvents */);
// Expect one online event.
handler.setOnline(true);
assertEquals(true, handler.isOnline());
assertEventCounts(1 /* expectedOnlineEvents */,
1 /* expectedOfflineEvents */);
}
function createHandler(initialValue) {
handler = new goog.testing.events.OnlineHandler(initialValue);
observer = new goog.testing.events.EventObserver();
goog.events.listen(handler,
[goog.net.NetworkStatusMonitor.EventType.ONLINE,
goog.net.NetworkStatusMonitor.EventType.OFFLINE],
observer);
}
function assertEventCounts(expectedOnlineEvents, expectedOfflineEvents) {
assertEquals(expectedOnlineEvents, observer.getEvents(
goog.net.NetworkStatusMonitor.EventType.ONLINE).length);
assertEquals(expectedOfflineEvents, observer.getEvents(
goog.net.NetworkStatusMonitor.EventType.OFFLINE).length);
}
@@ -0,0 +1,237 @@
// 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 Helper class to allow for expected unit test failures.
*
* @author robbyw@google.com (Robby Walker)
*/
goog.provide('goog.testing.ExpectedFailures');
goog.require('goog.debug.DivConsole');
goog.require('goog.dom');
goog.require('goog.dom.TagName');
goog.require('goog.events');
goog.require('goog.events.EventType');
goog.require('goog.log');
goog.require('goog.style');
goog.require('goog.testing.JsUnitException');
goog.require('goog.testing.TestCase');
goog.require('goog.testing.asserts');
/**
* Helper class for allowing some unit tests to fail, particularly designed to
* mark tests that should be fixed on a given browser.
*
* <pre>
* var expectedFailures = new goog.testing.ExpectedFailures();
*
* function tearDown() {
* expectedFailures.handleTearDown();
* }
*
* function testSomethingThatBreaksInWebKit() {
* expectedFailures.expectFailureFor(goog.userAgent.WEBKIT);
*
* try {
* ...
* assert(somethingThatFailsInWebKit);
* ...
* } catch (e) {
* expectedFailures.handleException(e);
* }
* }
* </pre>
*
* @constructor
* @final
*/
goog.testing.ExpectedFailures = function() {
goog.testing.ExpectedFailures.setUpConsole_();
this.reset_();
};
/**
* The lazily created debugging console.
* @type {goog.debug.DivConsole?}
* @private
*/
goog.testing.ExpectedFailures.console_ = null;
/**
* Logger for the expected failures.
* @type {goog.log.Logger}
* @private
*/
goog.testing.ExpectedFailures.prototype.logger_ =
goog.log.getLogger('goog.testing.ExpectedFailures');
/**
* Whether or not we are expecting failure.
* @type {boolean}
* @private
*/
goog.testing.ExpectedFailures.prototype.expectingFailure_;
/**
* The string to emit upon an expected failure.
* @type {string}
* @private
*/
goog.testing.ExpectedFailures.prototype.failureMessage_;
/**
* An array of suppressed failures.
* @type {Array<!Error>}
* @private
*/
goog.testing.ExpectedFailures.prototype.suppressedFailures_;
/**
* Sets up the debug console, if it isn't already set up.
* @private
*/
goog.testing.ExpectedFailures.setUpConsole_ = function() {
if (!goog.testing.ExpectedFailures.console_) {
var xButton = goog.dom.createDom(goog.dom.TagName.DIV, {
'style': 'position: absolute; border-left:1px solid #333;' +
'border-bottom:1px solid #333; right: 0; top: 0; width: 1em;' +
'height: 1em; cursor: pointer; background-color: #cde;' +
'text-align: center; color: black'
}, 'X');
var div = goog.dom.createDom(goog.dom.TagName.DIV, {
'style': 'position: absolute; border: 1px solid #333; right: 10px;' +
'top : 10px; width: 400px; display: none'
}, xButton);
document.body.appendChild(div);
goog.events.listen(xButton, goog.events.EventType.CLICK, function() {
goog.style.setElementShown(div, false);
});
goog.testing.ExpectedFailures.console_ = new goog.debug.DivConsole(div);
goog.log.addHandler(goog.testing.ExpectedFailures.prototype.logger_,
goog.bind(goog.style.setElementShown, null, div, true));
goog.log.addHandler(goog.testing.ExpectedFailures.prototype.logger_,
goog.bind(goog.testing.ExpectedFailures.console_.addLogRecord,
goog.testing.ExpectedFailures.console_));
}
};
/**
* Register to expect failure for the given condition. Multiple calls to this
* function act as a boolean OR. The first applicable message will be used.
* @param {boolean} condition Whether to expect failure.
* @param {string=} opt_message Descriptive message of this expected failure.
*/
goog.testing.ExpectedFailures.prototype.expectFailureFor = function(
condition, opt_message) {
this.expectingFailure_ = this.expectingFailure_ || condition;
if (condition) {
this.failureMessage_ = this.failureMessage_ || opt_message || '';
}
};
/**
* Determines if the given exception was expected.
* @param {Object} ex The exception to check.
* @return {boolean} Whether the exception was expected.
*/
goog.testing.ExpectedFailures.prototype.isExceptionExpected = function(ex) {
return this.expectingFailure_ && ex instanceof goog.testing.JsUnitException;
};
/**
* Handle an exception, suppressing it if it is a unit test failure that we
* expected.
* @param {Error} ex The exception to handle.
*/
goog.testing.ExpectedFailures.prototype.handleException = function(ex) {
if (this.isExceptionExpected(ex)) {
goog.log.info(this.logger_, 'Suppressing test failure in ' +
goog.testing.TestCase.currentTestName + ':' +
(this.failureMessage_ ? '\n(' + this.failureMessage_ + ')' : ''),
ex);
this.suppressedFailures_.push(ex);
return;
}
// Rethrow the exception if we weren't expecting it or if it is a normal
// exception.
throw ex;
};
/**
* Run the given function, catching any expected failures.
* @param {Function} func The function to run.
* @param {boolean=} opt_lenient Whether to ignore if the expected failures
* didn't occur. In this case a warning will be logged in handleTearDown.
*/
goog.testing.ExpectedFailures.prototype.run = function(func, opt_lenient) {
try {
func();
} catch (ex) {
this.handleException(ex);
}
if (!opt_lenient && this.expectingFailure_ &&
!this.suppressedFailures_.length) {
fail(this.getExpectationMessage_());
}
};
/**
* @return {string} A warning describing an expected failure that didn't occur.
* @private
*/
goog.testing.ExpectedFailures.prototype.getExpectationMessage_ = function() {
return 'Expected a test failure in \'' +
goog.testing.TestCase.currentTestName + '\' but the test passed.';
};
/**
* Handle the tearDown phase of a test, alerting the user if an expected test
* was not suppressed.
*/
goog.testing.ExpectedFailures.prototype.handleTearDown = function() {
if (this.expectingFailure_ && !this.suppressedFailures_.length) {
goog.log.warning(this.logger_, this.getExpectationMessage_());
}
this.reset_();
};
/**
* Reset internal state.
* @private
*/
goog.testing.ExpectedFailures.prototype.reset_ = function() {
this.expectingFailure_ = false;
this.failureMessage_ = '';
this.suppressedFailures_ = [];
};
@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2008 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>
Closure Unit Tests - goog.testing.ExpectedFailures
</title>
<script src="../base.js">
</script>
<script>
goog.require('goog.testing.ExpectedFailuresTest');
</script>
</head>
<body>
</body>
</html>
@@ -0,0 +1,121 @@
// 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.
goog.provide('goog.testing.ExpectedFailuresTest');
goog.setTestOnly('goog.testing.ExpectedFailuresTest');
goog.require('goog.debug.Logger');
goog.require('goog.testing.ExpectedFailures');
goog.require('goog.testing.JsUnitException');
goog.require('goog.testing.jsunit');
var count, expectedFailures, lastLevel, lastMessage;
// Stub out the logger.
goog.testing.ExpectedFailures.prototype.logger_.log = function(level,
message) {
lastLevel = level;
lastMessage = message;
count++;
};
function setUp() {
expectedFailures = new goog.testing.ExpectedFailures();
count = 0;
lastLevel = lastMessage = '';
}
// Individual test methods.
function testNoExpectedFailure() {
expectedFailures.handleTearDown();
}
function testPreventExpectedFailure() {
expectedFailures.expectFailureFor(true);
expectedFailures.handleException(new goog.testing.JsUnitException('', ''));
assertEquals('Should have logged a message', 1, count);
assertEquals('Should have logged an info message',
goog.debug.Logger.Level.INFO, lastLevel);
assertContains('Should log a suppression message',
'Suppressing test failure', lastMessage);
expectedFailures.handleTearDown();
assertEquals('Should not have logged another message', 1, count);
}
function testDoNotPreventException() {
var ex = 'exception';
expectedFailures.expectFailureFor(false);
var e = assertThrows('Should have rethrown exception', function() {
expectedFailures.handleException(ex);
});
assertEquals('Should rethrow same exception', ex, e);
}
function testExpectedFailureDidNotOccur() {
expectedFailures.expectFailureFor(true);
expectedFailures.handleTearDown();
assertEquals('Should have logged a message', 1, count);
assertEquals('Should have logged a warning',
goog.debug.Logger.Level.WARNING, lastLevel);
assertContains('Should log a suppression message',
'Expected a test failure', lastMessage);
}
function testRun() {
expectedFailures.expectFailureFor(true);
expectedFailures.run(function() {
fail('Expected failure');
});
assertEquals('Should have logged a message', 1, count);
assertEquals('Should have logged an info message',
goog.debug.Logger.Level.INFO, lastLevel);
assertContains('Should log a suppression message',
'Suppressing test failure', lastMessage);
expectedFailures.handleTearDown();
assertEquals('Should not have logged another message', 1, count);
}
function testRunStrict() {
expectedFailures.expectFailureFor(true);
var ex = assertThrows(function() {
expectedFailures.run(function() {
// Doesn't fail!
});
});
assertContains(
"Expected a test failure in 'testRunStrict' but the test passed.",
ex.message);
}
function testRunLenient() {
expectedFailures.expectFailureFor(true);
expectedFailures.run(function() {
// Doesn't fail!
}, true);
expectedFailures.handleTearDown();
assertEquals('Should have logged a message', 1, count);
assertEquals('Should have logged a warning',
goog.debug.Logger.Level.WARNING, lastLevel);
assertContains('Should log a suppression message',
'Expected a test failure', lastMessage);
}
@@ -0,0 +1,135 @@
// Copyright 2011 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 Mock blob object.
*
*/
goog.provide('goog.testing.fs.Blob');
goog.require('goog.crypt.base64');
/**
* A mock Blob object. The data is stored as a string.
*
* @param {string=} opt_data The string data encapsulated by the blob.
* @param {string=} opt_type The mime type of the blob.
* @constructor
*/
goog.testing.fs.Blob = function(opt_data, opt_type) {
/**
* @see http://www.w3.org/TR/FileAPI/#dfn-type
* @type {string}
*/
this.type = opt_type || '';
this.setDataInternal(opt_data || '');
};
/**
* The string data encapsulated by the blob.
* @type {string}
* @private
*/
goog.testing.fs.Blob.prototype.data_;
/**
* @see http://www.w3.org/TR/FileAPI/#dfn-size
* @type {number}
*/
goog.testing.fs.Blob.prototype.size;
/**
* Creates a blob with bytes of a blob ranging from the optional start
* parameter up to but not including the optional end parameter, and with a type
* attribute that is the value of the optional contentType parameter.
* @see http://www.w3.org/TR/FileAPI/#dfn-slice
* @param {number=} opt_start The start byte offset.
* @param {number=} opt_end The end point of a slice.
* @param {string=} opt_contentType The type of the resulting Blob.
* @return {!goog.testing.fs.Blob} The result blob of the slice operation.
*/
goog.testing.fs.Blob.prototype.slice = function(
opt_start, opt_end, opt_contentType) {
var relativeStart;
if (goog.isNumber(opt_start)) {
relativeStart = (opt_start < 0) ?
Math.max(this.data_.length + opt_start, 0) :
Math.min(opt_start, this.data_.length);
} else {
relativeStart = 0;
}
var relativeEnd;
if (goog.isNumber(opt_end)) {
relativeEnd = (opt_end < 0) ?
Math.max(this.data_.length + opt_end, 0) :
Math.min(opt_end, this.data_.length);
} else {
relativeEnd = this.data_.length;
}
var span = Math.max(relativeEnd - relativeStart, 0);
return new goog.testing.fs.Blob(
this.data_.substr(relativeStart, span),
opt_contentType);
};
/**
* @return {string} The string data encapsulated by the blob.
* @override
*/
goog.testing.fs.Blob.prototype.toString = function() {
return this.data_;
};
/**
* @return {!ArrayBuffer} The string data encapsulated by the blob as an
* ArrayBuffer.
*/
goog.testing.fs.Blob.prototype.toArrayBuffer = function() {
var buf = new ArrayBuffer(this.data_.length * 2);
var arr = new Uint16Array(buf);
for (var i = 0; i < this.data_.length; i++) {
arr[i] = this.data_.charCodeAt(i);
}
return buf;
};
/**
* @return {string} The string data encapsulated by the blob as a data: URI.
*/
goog.testing.fs.Blob.prototype.toDataUrl = function() {
return 'data:' + this.type + ';base64,' +
goog.crypt.base64.encodeString(this.data_);
};
/**
* Sets the internal contents of the blob. This should only be called by other
* functions inside the {@code goog.testing.fs} namespace.
*
* @param {string} data The data for this Blob.
*/
goog.testing.fs.Blob.prototype.setDataInternal = function(data) {
this.data_ = data;
this.size = data.length;
};
@@ -0,0 +1,23 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2011 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<!--
-->
<head>
<title>
Closure Unit Tests - goog.testing.fs.Blob
</title>
<script src="../../base.js">
</script>
<script>
goog.require('goog.testing.fs.BlobTest');
</script>
</head>
<body>
</body>
</html>
@@ -0,0 +1,65 @@
// Copyright 2011 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.
goog.provide('goog.testing.fs.BlobTest');
goog.setTestOnly('goog.testing.fs.BlobTest');
goog.require('goog.testing.fs.Blob');
goog.require('goog.testing.jsunit');
function testAttributes() {
var blob = new goog.testing.fs.Blob();
assertEquals(0, blob.size);
assertEquals('', blob.type);
blob = new goog.testing.fs.Blob('foo bar baz');
assertEquals(11, blob.size);
assertEquals('', blob.type);
blob = new goog.testing.fs.Blob('foo bar baz', 'text/plain');
assertEquals(11, blob.size);
assertEquals('text/plain', blob.type);
}
function testToString() {
assertEquals('', new goog.testing.fs.Blob().toString());
assertEquals('foo bar', new goog.testing.fs.Blob('foo bar').toString());
}
function testSlice() {
var blob = new goog.testing.fs.Blob('abcdef');
assertEquals('bc', blob.slice(1, 3).toString());
assertEquals('def', blob.slice(3, 10).toString());
assertEquals('abcd', blob.slice(0, -2).toString());
assertEquals('', blob.slice(10, 1).toString());
assertEquals('b', blob.slice(-5, 2).toString());
assertEquals('abcdef', blob.slice().toString());
assertEquals('abc', blob.slice(/* opt_start */ undefined, 3).toString());
assertEquals('def', blob.slice(3).toString());
assertEquals('text/plain', blob.slice(1, 2, 'text/plain').type);
}
function testSetDataInternal() {
var blob = new goog.testing.fs.Blob();
blob.setDataInternal('asdf');
assertEquals('asdf', blob.toString());
assertEquals(4, blob.size);
blob.setDataInternal('');
assertEquals('', blob.toString());
assertEquals(0, blob.size);
}
@@ -0,0 +1,23 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2011 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<!--
-->
<head>
<title>
Closure Unit Tests - goog.testing.fs.DirectoryEntry
</title>
<script src="../../base.js">
</script>
<script>
goog.require('goog.testing.fs.DirectoryEntryTest');
</script>
</head>
<body>
</body>
</html>
@@ -0,0 +1,319 @@
// Copyright 2011 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.
goog.provide('goog.testing.fs.DirectoryEntryTest');
goog.setTestOnly('goog.testing.fs.DirectoryEntryTest');
goog.require('goog.array');
goog.require('goog.fs.DirectoryEntry');
goog.require('goog.fs.Error');
goog.require('goog.testing.AsyncTestCase');
goog.require('goog.testing.MockClock');
goog.require('goog.testing.fs.FileSystem');
goog.require('goog.testing.jsunit');
var asyncTestCase = goog.testing.AsyncTestCase.createAndInstall();
var fs, dir, mockClock;
function setUp() {
mockClock = new goog.testing.MockClock(true);
fs = new goog.testing.fs.FileSystem();
dir = fs.getRoot().createDirectorySync('foo');
dir.createDirectorySync('subdir').createFileSync('subfile');
dir.createFileSync('file');
}
function tearDown() {
mockClock.uninstall();
}
function testIsFile() {
assertFalse(dir.isFile());
}
function testIsDirectory() {
assertTrue(dir.isDirectory());
}
function testRemoveWithChildren() {
dir.getFileSync('bar', goog.fs.DirectoryEntry.Behavior.CREATE);
expectError(dir.remove(), goog.fs.Error.ErrorCode.INVALID_MODIFICATION);
}
function testRemoveWithoutChildren() {
var emptyDir = dir.getDirectorySync(
'empty', goog.fs.DirectoryEntry.Behavior.CREATE);
emptyDir.remove().
addCallback(function() {
assertTrue(emptyDir.deleted);
assertFalse(fs.getRoot().hasChild('empty'));
}).
addBoth(continueTesting);
waitForAsync('waiting for file removal');
}
function testRemoveRootRecursively() {
var root = fs.getRoot();
root.removeRecursively().addCallback(function() {
assertTrue(dir.deleted);
assertFalse(fs.getRoot().deleted);
})
.addBoth(continueTesting);
waitForAsync('waiting for testRemoveRoot');
}
function testGetFile() {
// Advance the clock by an arbitrary but known amount.
mockClock.tick(41);
dir.getFile('file').
addCallback(function(file) {
assertEquals(dir.getFileSync('file'), file);
assertEquals('file', file.getName());
assertEquals('/foo/file', file.getFullPath());
assertTrue(file.isFile());
return dir.getLastModified();
}).
addCallback(function(date) {
assertEquals('Reading a file should not update the modification date.',
0, date.getTime());
return dir.getMetadata();
}).
addCallback(function(metadata) {
assertEquals('Reading a file should not update the metadata.',
0, metadata.modificationTime.getTime());
}).
addBoth(continueTesting);
waitForAsync('waiting for file');
}
function testGetFileFromSubdir() {
dir.getFile('subdir/subfile').addCallback(function(file) {
assertEquals(dir.getDirectorySync('subdir').getFileSync('subfile'), file);
assertEquals('subfile', file.getName());
assertEquals('/foo/subdir/subfile', file.getFullPath());
assertTrue(file.isFile());
}).
addBoth(continueTesting);
waitForAsync('waiting for file');
}
function testGetAbsolutePaths() {
fs.getRoot().getFile('/foo/subdir/subfile').
addCallback(function(subfile) {
assertEquals('/foo/subdir/subfile', subfile.getFullPath());
return fs.getRoot().getDirectory('//foo////');
}).
addCallback(function(foo) {
assertEquals('/foo', foo.getFullPath());
return foo.getDirectory('/');
}).
addCallback(function(root) {
assertEquals('/', root.getFullPath());
return root.getDirectory('/////');
}).
addCallback(function(root) {
assertEquals('/', root.getFullPath());
}).
addBoth(continueTesting);
waitForAsync('testGetAbsolutePaths');
}
function testCreateFile() {
mockClock.tick(43);
dir.getLastModified().
addCallback(function(date) { assertEquals(0, date.getTime()); }).
addCallback(function() {
return dir.getFile('bar', goog.fs.DirectoryEntry.Behavior.CREATE);
}).
addCallback(function(file) {
mockClock.tick();
assertEquals('bar', file.getName());
assertEquals('/foo/bar', file.getFullPath());
assertEquals(dir, file.parent);
assertTrue(file.isFile());
return dir.getLastModified();
}).
addCallback(function(date) {
assertEquals(43, date.getTime());
return dir.getMetadata();
}).
addCallback(function(metadata) {
assertEquals(43, metadata.modificationTime.getTime());
}).
addBoth(continueTesting);
waitForAsync('waiting for file creation');
}
function testCreateFileThatAlreadyExists() {
mockClock.tick(47);
var existingFile = dir.getFileSync('file');
dir.getFile('file', goog.fs.DirectoryEntry.Behavior.CREATE).
addCallback(function(file) {
mockClock.tick();
assertEquals('file', file.getName());
assertEquals('/foo/file', file.getFullPath());
assertEquals(dir, file.parent);
assertEquals(existingFile, file);
assertTrue(file.isFile());
return dir.getLastModified();
}).
addCallback(function(date) {
assertEquals(47, date.getTime());
return dir.getMetadata();
}).
addCallback(function(metadata) {
assertEquals(47, metadata.modificationTime.getTime());
}).
addBoth(continueTesting);
waitForAsync('waiting for file creation');
}
function testCreateFileInSubdir() {
dir.getFile('subdir/bar', goog.fs.DirectoryEntry.Behavior.CREATE).
addCallback(function(file) {
assertEquals('bar', file.getName());
assertEquals('/foo/subdir/bar', file.getFullPath());
assertEquals(dir.getDirectorySync('subdir'), file.parent);
assertTrue(file.isFile());
}).
addBoth(continueTesting);
waitForAsync('waiting for file creation');
}
function testCreateFileExclusive() {
dir.getFile('bar', goog.fs.DirectoryEntry.Behavior.CREATE_EXCLUSIVE).
addCallback(function(file) {
assertEquals('bar', file.getName());
assertEquals('/foo/bar', file.getFullPath());
assertEquals(dir, file.parent);
assertTrue(file.isFile());
}).
addBoth(continueTesting);
waitForAsync('waiting for file creation');
}
function testGetNonExistentFile() {
expectError(dir.getFile('bar'), goog.fs.Error.ErrorCode.NOT_FOUND);
}
function testGetNonExistentFileInSubdir() {
expectError(dir.getFile('subdir/bar'), goog.fs.Error.ErrorCode.NOT_FOUND);
}
function testGetFileInNonExistentSubdir() {
expectError(dir.getFile('bar/subfile'), goog.fs.Error.ErrorCode.NOT_FOUND);
}
function testGetFileThatsActuallyADirectory() {
expectError(dir.getFile('subdir'), goog.fs.Error.ErrorCode.TYPE_MISMATCH);
}
function testCreateFileInNonExistentSubdir() {
expectError(
dir.getFile('bar/newfile', goog.fs.DirectoryEntry.Behavior.CREATE),
goog.fs.Error.ErrorCode.NOT_FOUND);
}
function testCreateFileThatsActuallyADirectory() {
expectError(
dir.getFile('subdir', goog.fs.DirectoryEntry.Behavior.CREATE),
goog.fs.Error.ErrorCode.TYPE_MISMATCH);
}
function testCreateExclusiveExistingFile() {
expectError(
dir.getFile('file', goog.fs.DirectoryEntry.Behavior.CREATE_EXCLUSIVE),
goog.fs.Error.ErrorCode.INVALID_MODIFICATION);
}
function testListEmptyDirectory() {
var emptyDir = fs.getRoot().
getDirectorySync('empty', goog.fs.DirectoryEntry.Behavior.CREATE);
emptyDir.listDirectory().
addCallback(function(entryList) {
assertSameElements([], entryList);
}).
addBoth(continueTesting);
waitForAsync('testListEmptyDirectory');
}
function testListDirectory() {
var root = fs.getRoot();
root.getDirectorySync('dir1', goog.fs.DirectoryEntry.Behavior.CREATE);
root.getDirectorySync('dir2', goog.fs.DirectoryEntry.Behavior.CREATE);
root.getFileSync('file1', goog.fs.DirectoryEntry.Behavior.CREATE);
root.getFileSync('file2', goog.fs.DirectoryEntry.Behavior.CREATE);
fs.getRoot().listDirectory().
addCallback(function(entryList) {
assertSameElements([
'dir1',
'dir2',
'file1',
'file2',
'foo'
],
goog.array.map(entryList, function(entry) {
return entry.getName();
}));
}).
addBoth(continueTesting);
waitForAsync('testListDirectory');
}
function testCreatePath() {
dir.createPath('baz/bat').
addCallback(function(batDir) {
assertEquals('/foo/baz/bat', batDir.getFullPath());
return batDir.createPath('../zazzle');
}).
addCallback(function(zazzleDir) {
assertEquals('/foo/baz/zazzle', zazzleDir.getFullPath());
return zazzleDir.createPath('/elements/actinides/neptunium/');
}).
addCallback(function(elDir) {
assertEquals('/elements/actinides/neptunium', elDir.getFullPath());
}).
addBoth(continueTesting);
waitForAsync('testCreatePath');
}
function continueTesting(result) {
asyncTestCase.continueTesting();
if (result instanceof Error) {
throw result;
}
mockClock.tick();
}
function expectError(deferred, code) {
deferred.
addCallback(function() { fail('Expected an error'); }).
addErrback(function(err) {
assertEquals(code, err.code);
asyncTestCase.continueTesting();
});
waitForAsync('waiting for error');
}
function waitForAsync(msg) {
asyncTestCase.waitForAsync(msg);
mockClock.tick();
}
@@ -0,0 +1,637 @@
// Copyright 2011 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 Mock filesystem objects. These are all in the same file to
* avoid circular dependency issues.
*
*/
goog.provide('goog.testing.fs.DirectoryEntry');
goog.provide('goog.testing.fs.Entry');
goog.provide('goog.testing.fs.FileEntry');
goog.require('goog.Timer');
goog.require('goog.array');
goog.require('goog.asserts');
goog.require('goog.async.Deferred');
goog.require('goog.fs.DirectoryEntry');
goog.require('goog.fs.DirectoryEntryImpl');
goog.require('goog.fs.Entry');
goog.require('goog.fs.Error');
goog.require('goog.fs.FileEntry');
goog.require('goog.functions');
goog.require('goog.object');
goog.require('goog.string');
goog.require('goog.testing.fs.File');
goog.require('goog.testing.fs.FileWriter');
/**
* A mock filesystem entry object.
*
* @param {!goog.testing.fs.FileSystem} fs The filesystem containing this entry.
* @param {!goog.testing.fs.DirectoryEntry} parent The directory entry directly
* containing this entry.
* @param {string} name The name of this entry.
* @constructor
* @implements {goog.fs.Entry}
*/
goog.testing.fs.Entry = function(fs, parent, name) {
/**
* This entry's filesystem.
* @type {!goog.testing.fs.FileSystem}
* @private
*/
this.fs_ = fs;
/**
* The name of this entry.
* @type {string}
* @private
*/
this.name_ = name;
/**
* The parent of this entry.
* @type {!goog.testing.fs.DirectoryEntry}
*/
this.parent = parent;
};
/**
* Whether or not this entry has been deleted.
* @type {boolean}
*/
goog.testing.fs.Entry.prototype.deleted = false;
/** @override */
goog.testing.fs.Entry.prototype.isFile = goog.abstractMethod;
/** @override */
goog.testing.fs.Entry.prototype.isDirectory = goog.abstractMethod;
/** @override */
goog.testing.fs.Entry.prototype.getName = function() {
return this.name_;
};
/** @override */
goog.testing.fs.Entry.prototype.getFullPath = function() {
if (this.getName() == '' || this.parent.getName() == '') {
// The root directory has an empty name
return '/' + this.name_;
} else {
return this.parent.getFullPath() + '/' + this.name_;
}
};
/**
* @return {!goog.testing.fs.FileSystem}
* @override
*/
goog.testing.fs.Entry.prototype.getFileSystem = function() {
return this.fs_;
};
/** @override */
goog.testing.fs.Entry.prototype.getLastModified = goog.abstractMethod;
/** @override */
goog.testing.fs.Entry.prototype.getMetadata = goog.abstractMethod;
/** @override */
goog.testing.fs.Entry.prototype.moveTo = function(parent, opt_newName) {
var msg = 'moving ' + this.getFullPath() + ' into ' + parent.getFullPath() +
(opt_newName ? ', renaming to ' + opt_newName : '');
var newFile;
return this.checkNotDeleted(msg).
addCallback(function() { return this.copyTo(parent, opt_newName); }).
addCallback(function(file) {
newFile = file;
return this.remove();
}).addCallback(function() { return newFile; });
};
/** @override */
goog.testing.fs.Entry.prototype.copyTo = function(parent, opt_newName) {
goog.asserts.assert(parent instanceof goog.testing.fs.DirectoryEntry);
var msg = 'copying ' + this.getFullPath() + ' into ' + parent.getFullPath() +
(opt_newName ? ', renaming to ' + opt_newName : '');
var self = this;
return this.checkNotDeleted(msg).addCallback(function() {
var name = opt_newName || self.getName();
var entry = self.clone();
parent.children[name] = entry;
parent.lastModifiedTimestamp_ = goog.now();
entry.name_ = name;
entry.parent = /** @type {!goog.testing.fs.DirectoryEntry} */ (parent);
return entry;
});
};
/**
* @return {!goog.testing.fs.Entry} A shallow copy of this entry object.
*/
goog.testing.fs.Entry.prototype.clone = goog.abstractMethod;
/** @override */
goog.testing.fs.Entry.prototype.toUrl = function(opt_mimetype) {
return 'fakefilesystem:' + this.getFullPath();
};
/** @override */
goog.testing.fs.Entry.prototype.toUri = goog.testing.fs.Entry.prototype.toUrl;
/** @override */
goog.testing.fs.Entry.prototype.wrapEntry = goog.abstractMethod;
/** @override */
goog.testing.fs.Entry.prototype.remove = function() {
var msg = 'removing ' + this.getFullPath();
var self = this;
return this.checkNotDeleted(msg).addCallback(function() {
delete this.parent.children[self.getName()];
self.parent.lastModifiedTimestamp_ = goog.now();
self.deleted = true;
return;
});
};
/** @override */
goog.testing.fs.Entry.prototype.getParent = function() {
var msg = 'getting parent of ' + this.getFullPath();
return this.checkNotDeleted(msg).
addCallback(function() { return this.parent; });
};
/**
* Return a deferred that will call its errback if this entry has been deleted.
* In addition, the deferred will only run after a timeout of 0, and all its
* callbacks will run with the entry as "this".
*
* @param {string} action The name of the action being performed. For error
* reporting.
* @return {!goog.async.Deferred} The deferred that will be called after a
* timeout of 0.
* @protected
*/
goog.testing.fs.Entry.prototype.checkNotDeleted = function(action) {
var d = new goog.async.Deferred(undefined, this);
goog.Timer.callOnce(function() {
if (this.deleted) {
var err = new goog.fs.Error(
/** @type {!FileError} */ ({'name': 'NotFoundError'}),
action);
d.errback(err);
} else {
d.callback();
}
}, 0, this);
return d;
};
/**
* A mock directory entry object.
*
* @param {!goog.testing.fs.FileSystem} fs The filesystem containing this entry.
* @param {goog.testing.fs.DirectoryEntry} parent The directory entry directly
* containing this entry. If this is null, that means this is the root
* directory and so is its own parent.
* @param {string} name The name of this entry.
* @param {!Object<!goog.testing.fs.Entry>} children The map of child names to
* entry objects.
* @constructor
* @extends {goog.testing.fs.Entry}
* @implements {goog.fs.DirectoryEntry}
* @final
*/
goog.testing.fs.DirectoryEntry = function(fs, parent, name, children) {
goog.testing.fs.DirectoryEntry.base(
this, 'constructor', fs, parent || this, name);
/**
* The map of child names to entry objects.
* @type {!Object<!goog.testing.fs.Entry>}
*/
this.children = children;
/**
* The modification time of the directory. Measured using goog.now, which may
* be overridden with mock time providers.
* @type {number}
* @private
*/
this.lastModifiedTimestamp_ = goog.now();
};
goog.inherits(goog.testing.fs.DirectoryEntry, goog.testing.fs.Entry);
/**
* Constructs and returns the metadata object for this entry.
* @return {{modificationTime: Date}} The metadata object.
* @private
*/
goog.testing.fs.DirectoryEntry.prototype.getMetadata_ = function() {
return {
'modificationTime': new Date(this.lastModifiedTimestamp_)
};
};
/** @override */
goog.testing.fs.DirectoryEntry.prototype.isFile = function() {
return false;
};
/** @override */
goog.testing.fs.DirectoryEntry.prototype.isDirectory = function() {
return true;
};
/** @override */
goog.testing.fs.DirectoryEntry.prototype.getLastModified = function() {
var msg = 'reading last modified date for ' + this.getFullPath();
return this.checkNotDeleted(msg).
addCallback(function() {return new Date(this.lastModifiedTimestamp_)});
};
/** @override */
goog.testing.fs.DirectoryEntry.prototype.getMetadata = function() {
var msg = 'reading metadata for ' + this.getFullPath();
return this.checkNotDeleted(msg).
addCallback(function() {return this.getMetadata_()});
};
/** @override */
goog.testing.fs.DirectoryEntry.prototype.clone = function() {
return new goog.testing.fs.DirectoryEntry(
this.getFileSystem(), this.parent, this.getName(), this.children);
};
/** @override */
goog.testing.fs.DirectoryEntry.prototype.remove = function() {
if (!goog.object.isEmpty(this.children)) {
var d = new goog.async.Deferred();
goog.Timer.callOnce(function() {
d.errback(new goog.fs.Error(
/** @type {!FileError} */ ({'name': 'InvalidModificationError'}),
'removing ' + this.getFullPath()));
}, 0, this);
return d;
} else if (this != this.getFileSystem().getRoot()) {
return goog.testing.fs.DirectoryEntry.base(this, 'remove');
} else {
// Root directory, do nothing.
return goog.async.Deferred.succeed();
}
};
/** @override */
goog.testing.fs.DirectoryEntry.prototype.getFile = function(
path, opt_behavior) {
var msg = 'loading file ' + path + ' from ' + this.getFullPath();
opt_behavior = opt_behavior || goog.fs.DirectoryEntry.Behavior.DEFAULT;
return this.checkNotDeleted(msg).addCallback(function() {
try {
return goog.async.Deferred.succeed(this.getFileSync(path, opt_behavior));
} catch (e) {
return goog.async.Deferred.fail(e);
}
});
};
/** @override */
goog.testing.fs.DirectoryEntry.prototype.getDirectory = function(
path, opt_behavior) {
var msg = 'loading directory ' + path + ' from ' + this.getFullPath();
opt_behavior = opt_behavior || goog.fs.DirectoryEntry.Behavior.DEFAULT;
return this.checkNotDeleted(msg).addCallback(function() {
try {
return goog.async.Deferred.succeed(
this.getDirectorySync(path, opt_behavior));
} catch (e) {
return goog.async.Deferred.fail(e);
}
});
};
/**
* Get a file entry synchronously, without waiting for a Deferred to resolve.
*
* @param {string} path The path to the file, relative to this directory.
* @param {goog.fs.DirectoryEntry.Behavior=} opt_behavior The behavior for
* loading the file.
* @param {string=} opt_data The string data encapsulated by the blob.
* @param {string=} opt_type The mime type of the blob.
* @return {!goog.testing.fs.FileEntry} The loaded file.
*/
goog.testing.fs.DirectoryEntry.prototype.getFileSync = function(
path, opt_behavior, opt_data, opt_type) {
opt_behavior = opt_behavior || goog.fs.DirectoryEntry.Behavior.DEFAULT;
return (/** @type {!goog.testing.fs.FileEntry} */ (this.getEntry_(
path, opt_behavior, true /* isFile */,
goog.bind(function(parent, name) {
return new goog.testing.fs.FileEntry(
this.getFileSystem(), parent, name,
goog.isDef(opt_data) ? opt_data : '', opt_type);
}, this))));
};
/**
* Creates a file synchronously. This is a shorthand for getFileSync, useful for
* setting up tests.
*
* @param {string} path The path to the file, relative to this directory.
* @return {!goog.testing.fs.FileEntry} The created file.
*/
goog.testing.fs.DirectoryEntry.prototype.createFileSync = function(path) {
return this.getFileSync(path, goog.fs.DirectoryEntry.Behavior.CREATE);
};
/**
* Get a directory synchronously, without waiting for a Deferred to resolve.
*
* @param {string} path The path to the directory, relative to this one.
* @param {goog.fs.DirectoryEntry.Behavior=} opt_behavior The behavior for
* loading the directory.
* @return {!goog.testing.fs.DirectoryEntry} The loaded directory.
*/
goog.testing.fs.DirectoryEntry.prototype.getDirectorySync = function(
path, opt_behavior) {
opt_behavior = opt_behavior || goog.fs.DirectoryEntry.Behavior.DEFAULT;
return (/** @type {!goog.testing.fs.DirectoryEntry} */ (this.getEntry_(
path, opt_behavior, false /* isFile */,
goog.bind(function(parent, name) {
return new goog.testing.fs.DirectoryEntry(
this.getFileSystem(), parent, name, {});
}, this))));
};
/**
* Creates a directory synchronously. This is a shorthand for getFileSync,
* useful for setting up tests.
*
* @param {string} path The path to the directory, relative to this directory.
* @return {!goog.testing.fs.DirectoryEntry} The created directory.
*/
goog.testing.fs.DirectoryEntry.prototype.createDirectorySync = function(path) {
return this.getDirectorySync(path, goog.fs.DirectoryEntry.Behavior.CREATE);
};
/**
* Get a file or directory entry from a path. This handles parsing the path for
* subdirectories and throwing appropriate errors should something go wrong.
*
* @param {string} path The path to the entry, relative to this directory.
* @param {goog.fs.DirectoryEntry.Behavior} behavior The behavior for loading
* the entry.
* @param {boolean} isFile Whether a file or directory is being loaded.
* @param {function(!goog.testing.fs.DirectoryEntry, string) :
* !goog.testing.fs.Entry} createFn
* The function for creating the entry if it doesn't yet exist. This is
* passed the parent entry and the name of the new entry.
* @return {!goog.testing.fs.Entry} The loaded entry.
* @private
*/
goog.testing.fs.DirectoryEntry.prototype.getEntry_ = function(
path, behavior, isFile, createFn) {
// Filter out leading, trailing, and duplicate slashes.
var components = goog.array.filter(path.split('/'), goog.functions.identity);
var basename = /** @type {string} */ (goog.array.peek(components)) || '';
var dir = goog.string.startsWith(path, '/') ?
this.getFileSystem().getRoot() : this;
goog.array.forEach(components.slice(0, -1), function(p) {
var subdir = dir.children[p];
if (!subdir) {
throw new goog.fs.Error(
/** @type {!FileError} */ ({'name': 'NotFoundError'}),
'loading ' + path + ' from ' + this.getFullPath() + ' (directory ' +
dir.getFullPath() + '/' + p + ')');
}
dir = subdir;
}, this);
// If there is no basename, the path must resolve to the root directory.
var entry = basename ? dir.children[basename] : dir;
if (!entry) {
if (behavior == goog.fs.DirectoryEntry.Behavior.DEFAULT) {
throw new goog.fs.Error(
/** @type {!FileError} */ ({'name': 'NotFoundError'}),
'loading ' + path + ' from ' + this.getFullPath());
} else {
goog.asserts.assert(
behavior == goog.fs.DirectoryEntry.Behavior.CREATE ||
behavior == goog.fs.DirectoryEntry.Behavior.CREATE_EXCLUSIVE);
entry = createFn(dir, basename);
dir.children[basename] = entry;
this.lastModifiedTimestamp_ = goog.now();
return entry;
}
} else if (behavior == goog.fs.DirectoryEntry.Behavior.CREATE_EXCLUSIVE) {
throw new goog.fs.Error(
/** @type {!FileError} */ ({'name': 'InvalidModificationError'}),
'loading ' + path + ' from ' + this.getFullPath());
} else if (entry.isFile() != isFile) {
throw new goog.fs.Error(
/** @type {!FileError} */ ({'name': 'TypeMismatchError'}),
'loading ' + path + ' from ' + this.getFullPath());
} else {
if (behavior == goog.fs.DirectoryEntry.Behavior.CREATE) {
this.lastModifiedTimestamp_ = goog.now();
}
return entry;
}
};
/**
* Returns whether this directory has a child with the given name.
*
* @param {string} name The name of the entry to check for.
* @return {boolean} Whether or not this has a child with the given name.
*/
goog.testing.fs.DirectoryEntry.prototype.hasChild = function(name) {
return name in this.children;
};
/** @override */
goog.testing.fs.DirectoryEntry.prototype.removeRecursively = function() {
var msg = 'removing ' + this.getFullPath() + ' recursively';
return this.checkNotDeleted(msg).addCallback(function() {
var d = goog.async.Deferred.succeed(null);
goog.object.forEach(this.children, function(child) {
d.awaitDeferred(
child.isDirectory() ? child.removeRecursively() : child.remove());
});
d.addCallback(function() { return this.remove(); }, this);
return d;
});
};
/** @override */
goog.testing.fs.DirectoryEntry.prototype.listDirectory = function() {
var msg = 'listing ' + this.getFullPath();
return this.checkNotDeleted(msg).addCallback(function() {
return goog.object.getValues(this.children);
});
};
/** @override */
goog.testing.fs.DirectoryEntry.prototype.createPath =
// This isn't really type-safe.
/** @type {!Function} */ (goog.fs.DirectoryEntryImpl.prototype.createPath);
/**
* A mock file entry object.
*
* @param {!goog.testing.fs.FileSystem} fs The filesystem containing this entry.
* @param {!goog.testing.fs.DirectoryEntry} parent The directory entry directly
* containing this entry.
* @param {string} name The name of this entry.
* @param {string} data The data initially contained in the file.
* @param {string=} opt_type The mime type of the blob.
* @constructor
* @extends {goog.testing.fs.Entry}
* @implements {goog.fs.FileEntry}
* @final
*/
goog.testing.fs.FileEntry = function(fs, parent, name, data, opt_type) {
goog.testing.fs.FileEntry.base(this, 'constructor', fs, parent, name);
/**
* The internal file blob referenced by this file entry.
* @type {!goog.testing.fs.File}
* @private
*/
this.file_ =
new goog.testing.fs.File(name, new Date(goog.now()), data, opt_type);
/**
* The metadata for file.
* @type {{modificationTime: Date}}
* @private
*/
this.metadata_ = {
'modificationTime': this.file_.lastModifiedDate
};
};
goog.inherits(goog.testing.fs.FileEntry, goog.testing.fs.Entry);
/** @override */
goog.testing.fs.FileEntry.prototype.isFile = function() {
return true;
};
/** @override */
goog.testing.fs.FileEntry.prototype.isDirectory = function() {
return false;
};
/** @override */
goog.testing.fs.FileEntry.prototype.clone = function() {
return new goog.testing.fs.FileEntry(
this.getFileSystem(), this.parent,
this.getName(), this.fileSync().toString());
};
/** @override */
goog.testing.fs.FileEntry.prototype.getLastModified = function() {
return this.file().addCallback(function(file) {
return file.lastModifiedDate;
});
};
/** @override */
goog.testing.fs.FileEntry.prototype.getMetadata = function() {
var msg = 'getting metadata for ' + this.getFullPath();
return this.checkNotDeleted(msg).addCallback(function() {
return this.metadata_;
});
};
/** @override */
goog.testing.fs.FileEntry.prototype.createWriter = function() {
var d = new goog.async.Deferred();
goog.Timer.callOnce(
goog.bind(d.callback, d, new goog.testing.fs.FileWriter(this)));
return d;
};
/** @override */
goog.testing.fs.FileEntry.prototype.file = function() {
var msg = 'getting file for ' + this.getFullPath();
return this.checkNotDeleted(msg).addCallback(function() {
return this.fileSync();
});
};
/**
* Get the internal file representation synchronously, without waiting for a
* Deferred to resolve.
*
* @return {!goog.testing.fs.File} The internal file blob referenced by this
* FileEntry.
*/
goog.testing.fs.FileEntry.prototype.fileSync = function() {
return this.file_;
};
@@ -0,0 +1,23 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2011 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<!--
-->
<head>
<title>
Closure Unit Tests - goog.testing.fs.Entry
</title>
<script src="../../base.js">
</script>
<script>
goog.require('goog.testing.fs.EntryTest');
</script>
</head>
<body>
</body>
</html>
@@ -0,0 +1,222 @@
// Copyright 2011 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.
goog.provide('goog.testing.fs.EntryTest');
goog.setTestOnly('goog.testing.fs.EntryTest');
goog.require('goog.fs.DirectoryEntry');
goog.require('goog.fs.Error');
goog.require('goog.testing.AsyncTestCase');
goog.require('goog.testing.MockClock');
goog.require('goog.testing.fs.FileSystem');
goog.require('goog.testing.jsunit');
var asyncTestCase = goog.testing.AsyncTestCase.createAndInstall();
var fs, file, mockClock;
function setUp() {
mockClock = new goog.testing.MockClock(true);
fs = new goog.testing.fs.FileSystem();
file = fs.getRoot().
getDirectorySync('foo', goog.fs.DirectoryEntry.Behavior.CREATE).
getFileSync('bar', goog.fs.DirectoryEntry.Behavior.CREATE);
}
function tearDown() {
mockClock.uninstall();
}
function testGetName() {
assertEquals('bar', file.getName());
}
function testGetFullPath() {
assertEquals('/foo/bar', file.getFullPath());
assertEquals('/', fs.getRoot().getFullPath());
}
function testGetFileSystem() {
assertEquals(fs, file.getFileSystem());
}
function testMoveTo() {
file.moveTo(fs.getRoot()).addCallback(function(newFile) {
assertTrue(file.deleted);
assertFalse(newFile.deleted);
assertEquals('/bar', newFile.getFullPath());
assertEquals(fs.getRoot(), newFile.parent);
assertEquals(newFile, fs.getRoot().getFileSync('bar'));
assertFalse(fs.getRoot().getDirectorySync('foo').hasChild('bar'));
asyncTestCase.continueTesting();
});
waitForAsync('waiting for file move');
}
function testMoveToNewName() {
// Advance the clock to an arbitrary, known time.
mockClock.tick(71);
file.moveTo(fs.getRoot(), 'baz').
addCallback(function(newFile) {
mockClock.tick();
assertTrue(file.deleted);
assertFalse(newFile.deleted);
assertEquals('/baz', newFile.getFullPath());
assertEquals(fs.getRoot(), newFile.parent);
assertEquals(newFile, fs.getRoot().getFileSync('baz'));
var oldParentDir = fs.getRoot().getDirectorySync('foo');
assertFalse(oldParentDir.hasChild('bar'));
assertFalse(oldParentDir.hasChild('baz'));
return oldParentDir.getLastModified();
}).
addCallback(function(lastModifiedDate) {
assertEquals(71, lastModifiedDate.getTime());
var oldParentDir = fs.getRoot().getDirectorySync('foo');
return oldParentDir.getMetadata();
}).
addCallback(function(metadata) {
assertEquals(71, metadata.modificationTime.getTime());
return fs.getRoot().getLastModified();
}).
addCallback(function(rootLastModifiedDate) {
assertEquals(71, rootLastModifiedDate.getTime());
return fs.getRoot().getMetadata();
}).
addCallback(function(rootMetadata) {
assertEquals(71, rootMetadata.modificationTime.getTime());
asyncTestCase.continueTesting();
});
waitForAsync('waiting for file move');
}
function testMoveDeletedFile() {
assertFailsWhenDeleted(function() { return file.moveTo(fs.getRoot()); });
}
function testCopyTo() {
mockClock.tick(61);
file.copyTo(fs.getRoot()).
addCallback(function(newFile) {
assertFalse(file.deleted);
assertFalse(newFile.deleted);
assertEquals('/bar', newFile.getFullPath());
assertEquals(fs.getRoot(), newFile.parent);
assertEquals(newFile, fs.getRoot().getFileSync('bar'));
var oldParentDir = fs.getRoot().getDirectorySync('foo');
assertEquals(file, oldParentDir.getFileSync('bar'));
return oldParentDir.getLastModified();
}).
addCallback(function(lastModifiedDate) {
assertEquals('The original parent directory was not modified.',
0, lastModifiedDate.getTime());
var oldParentDir = fs.getRoot().getDirectorySync('foo');
return oldParentDir.getMetadata();
}).
addCallback(function(metadata) {
assertEquals('The original parent directory was not modified.',
0, metadata.modificationTime.getTime());
return fs.getRoot().getLastModified();
}).
addCallback(function(rootLastModifiedDate) {
assertEquals(61, rootLastModifiedDate.getTime());
return fs.getRoot().getMetadata();
}).
addCallback(function(rootMetadata) {
assertEquals(61, rootMetadata.modificationTime.getTime());
asyncTestCase.continueTesting();
});
waitForAsync('waiting for file copy');
}
function testCopyToNewName() {
file.copyTo(fs.getRoot(), 'baz').addCallback(function(newFile) {
assertFalse(file.deleted);
assertFalse(newFile.deleted);
assertEquals('/baz', newFile.getFullPath());
assertEquals(fs.getRoot(), newFile.parent);
assertEquals(newFile, fs.getRoot().getFileSync('baz'));
assertEquals(file, fs.getRoot().getDirectorySync('foo').getFileSync('bar'));
assertFalse(fs.getRoot().getDirectorySync('foo').hasChild('baz'));
asyncTestCase.continueTesting();
});
waitForAsync('waiting for file copy');
}
function testCopyDeletedFile() {
assertFailsWhenDeleted(function() { return file.copyTo(fs.getRoot()); });
}
function testRemove() {
mockClock.tick(57);
file.remove().
addCallback(function() {
mockClock.tick();
var parentDir = fs.getRoot().getDirectorySync('foo');
assertTrue(file.deleted);
assertFalse(parentDir.hasChild('bar'));
return parentDir.getLastModified();
}).
addCallback(function(date) {
assertEquals(57, date.getTime());
var parentDir = fs.getRoot().getDirectorySync('foo');
return parentDir.getMetadata();
}).
addCallback(function(metadata) {
assertEquals(57, metadata.modificationTime.getTime());
asyncTestCase.continueTesting();
});
waitForAsync('waiting for file removal');
}
function testRemoveDeletedFile() {
assertFailsWhenDeleted(function() { return file.remove(); });
}
function testGetParent() {
file.getParent().addCallback(function(p) {
assertEquals(file.parent, p);
assertEquals(fs.getRoot().getDirectorySync('foo'), p);
assertEquals('/foo', p.getFullPath());
asyncTestCase.continueTesting();
});
waitForAsync('waiting for file parent');
}
function testGetDeletedFileParent() {
assertFailsWhenDeleted(function() { return file.getParent(); });
}
function assertFailsWhenDeleted(fn) {
file.remove().addCallback(fn).
addCallback(function() { fail('Expected an error'); }).
addErrback(function(err) {
assertEquals(goog.fs.Error.ErrorCode.NOT_FOUND, err.code);
asyncTestCase.continueTesting();
});
waitForAsync('waiting for file operation');
}
function waitForAsync(msg) {
asyncTestCase.waitForAsync(msg);
mockClock.tick();
}
@@ -0,0 +1,53 @@
// Copyright 2011 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 Mock file object.
*
*/
goog.provide('goog.testing.fs.File');
goog.require('goog.testing.fs.Blob');
/**
* A mock file object.
*
* @param {string} name The name of the file.
* @param {Date=} opt_lastModified The last modified date for this file. May be
* null if file modification dates are not supported.
* @param {string=} opt_data The string data encapsulated by the blob.
* @param {string=} opt_type The mime type of the blob.
* @constructor
* @extends {goog.testing.fs.Blob}
* @final
*/
goog.testing.fs.File = function(name, opt_lastModified, opt_data, opt_type) {
goog.testing.fs.File.base(this, 'constructor', opt_data, opt_type);
/**
* @see http://www.w3.org/TR/FileAPI/#dfn-name
* @type {string}
*/
this.name = name;
/**
* @see http://www.w3.org/TR/FileAPI/#dfn-lastModifiedDate
* @type {Date}
*/
this.lastModifiedDate = opt_lastModified || null;
};
goog.inherits(goog.testing.fs.File, goog.testing.fs.Blob);
@@ -0,0 +1,23 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2011 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<!--
-->
<head>
<title>
Closure Unit Tests - goog.testing.fs.FileEntry
</title>
<script src="../../base.js">
</script>
<script>
goog.require('goog.testing.fs.FileEntryTest');
</script>
</head>
<body>
</body>
</html>
@@ -0,0 +1,88 @@
// Copyright 2011 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.
goog.provide('goog.testing.fs.FileEntryTest');
goog.setTestOnly('goog.testing.fs.FileEntryTest');
goog.require('goog.testing.AsyncTestCase');
goog.require('goog.testing.MockClock');
goog.require('goog.testing.fs.FileEntry');
goog.require('goog.testing.fs.FileSystem');
goog.require('goog.testing.jsunit');
var asyncTestCase = goog.testing.AsyncTestCase.createAndInstall();
var fs, file, fileEntry, mockClock, currentTime;
function setUp() {
mockClock = new goog.testing.MockClock(true);
fs = new goog.testing.fs.FileSystem();
fileEntry = fs.getRoot().createDirectorySync('foo').createFileSync('bar');
}
function tearDown() {
mockClock.uninstall();
}
function testIsFile() {
assertTrue(fileEntry.isFile());
}
function testIsDirectory() {
assertFalse(fileEntry.isDirectory());
}
function testFile() {
var testFile = new goog.testing.fs.FileEntry(fs, fs.getRoot(),
'test', 'hello world');
testFile.file().addCallback(function(f) {
assertEquals('test', f.name);
assertEquals('hello world', f.toString());
asyncTestCase.continueTesting();
});
waitForAsync('testFile');
}
function testGetLastModified() {
// Advance the clock to a known time.
mockClock.tick(53);
var testFile = new goog.testing.fs.FileEntry(fs, fs.getRoot(),
'timeTest', 'hello world');
mockClock.tick();
testFile.getLastModified().addCallback(function(date) {
assertEquals(53, date.getTime());
asyncTestCase.continueTesting();
});
waitForAsync('testGetLastModified');
}
function testGetMetadata() {
// Advance the clock to a known time.
mockClock.tick(54);
var testFile = new goog.testing.fs.FileEntry(fs, fs.getRoot(),
'timeTest', 'hello world');
mockClock.tick();
testFile.getMetadata().addCallback(function(metadata) {
assertEquals(54, metadata.modificationTime.getTime());
asyncTestCase.continueTesting();
});
waitForAsync('testGetMetadata');
}
function waitForAsync(msg) {
asyncTestCase.waitForAsync(msg);
mockClock.tick();
}
@@ -0,0 +1,275 @@
// Copyright 2011 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 Mock FileReader object.
*
*/
goog.provide('goog.testing.fs.FileReader');
goog.require('goog.Timer');
goog.require('goog.events.EventTarget');
goog.require('goog.fs.Error');
goog.require('goog.fs.FileReader');
goog.require('goog.testing.fs.ProgressEvent');
/**
* A mock FileReader object. This emits the same events as
* {@link goog.fs.FileReader}.
*
* @constructor
* @extends {goog.events.EventTarget}
*/
goog.testing.fs.FileReader = function() {
goog.testing.fs.FileReader.base(this, 'constructor');
/**
* The current state of the reader.
* @type {goog.fs.FileReader.ReadyState}
* @private
*/
this.readyState_ = goog.fs.FileReader.ReadyState.INIT;
};
goog.inherits(goog.testing.fs.FileReader, goog.events.EventTarget);
/**
* The most recent error experienced by this reader.
* @type {goog.fs.Error}
* @private
*/
goog.testing.fs.FileReader.prototype.error_;
/**
* Whether the current operation has been aborted.
* @type {boolean}
* @private
*/
goog.testing.fs.FileReader.prototype.aborted_ = false;
/**
* The blob this reader is reading from.
* @type {goog.testing.fs.Blob}
* @private
*/
goog.testing.fs.FileReader.prototype.blob_;
/**
* The possible return types.
* @enum {number}
*/
goog.testing.fs.FileReader.ReturnType = {
/**
* Used when reading as text.
*/
TEXT: 1,
/**
* Used when reading as binary string.
*/
BINARY_STRING: 2,
/**
* Used when reading as array buffer.
*/
ARRAY_BUFFER: 3,
/**
* Used when reading as data URL.
*/
DATA_URL: 4
};
/**
* The return type we're reading.
* @type {goog.testing.fs.FileReader.ReturnType}
* @private
*/
goog.testing.fs.FileReader.returnType_;
/**
* @see {goog.fs.FileReader#getReadyState}
* @return {goog.fs.FileReader.ReadyState} The current ready state.
*/
goog.testing.fs.FileReader.prototype.getReadyState = function() {
return this.readyState_;
};
/**
* @see {goog.fs.FileReader#getError}
* @return {goog.fs.Error} The current error.
*/
goog.testing.fs.FileReader.prototype.getError = function() {
return this.error_;
};
/**
* @see {goog.fs.FileReader#abort}
*/
goog.testing.fs.FileReader.prototype.abort = function() {
if (this.readyState_ != goog.fs.FileReader.ReadyState.LOADING) {
var msg = 'aborting read';
throw new goog.fs.Error(
/** @type {!FileError} */ ({'name': 'InvalidStateError'}),
msg);
}
this.aborted_ = true;
};
/**
* @see {goog.fs.FileReader#getResult}
* @return {*} The result of the file read.
*/
goog.testing.fs.FileReader.prototype.getResult = function() {
if (this.readyState_ != goog.fs.FileReader.ReadyState.DONE) {
return undefined;
}
if (this.error_) {
return undefined;
}
if (this.returnType_ == goog.testing.fs.FileReader.ReturnType.TEXT) {
return this.blob_.toString();
} else if (this.returnType_ ==
goog.testing.fs.FileReader.ReturnType.ARRAY_BUFFER) {
return this.blob_.toArrayBuffer();
} else if (this.returnType_ ==
goog.testing.fs.FileReader.ReturnType.BINARY_STRING) {
return this.blob_.toString();
} else if (this.returnType_ ==
goog.testing.fs.FileReader.ReturnType.DATA_URL) {
return this.blob_.toDataUrl();
} else {
return undefined;
}
};
/**
* Fires the read events.
* @param {!goog.testing.fs.Blob} blob The blob to read from.
* @private
*/
goog.testing.fs.FileReader.prototype.read_ = function(blob) {
this.blob_ = blob;
if (this.readyState_ == goog.fs.FileReader.ReadyState.LOADING) {
var msg = 'reading file';
throw new goog.fs.Error(
/** @type {!FileError} */ ({'name': 'InvalidStateError'}),
msg);
}
this.readyState_ = goog.fs.FileReader.ReadyState.LOADING;
goog.Timer.callOnce(function() {
if (this.aborted_) {
this.abort_(blob.size);
return;
}
this.progressEvent_(goog.fs.FileReader.EventType.LOAD_START, 0, blob.size);
this.progressEvent_(goog.fs.FileReader.EventType.LOAD, blob.size / 2,
blob.size);
this.progressEvent_(goog.fs.FileReader.EventType.LOAD, blob.size,
blob.size);
this.readyState_ = goog.fs.FileReader.ReadyState.DONE;
this.progressEvent_(goog.fs.FileReader.EventType.LOAD, blob.size,
blob.size);
this.progressEvent_(goog.fs.FileReader.EventType.LOAD_END, blob.size,
blob.size);
}, 0, this);
};
/**
* @see {goog.fs.FileReader#readAsBinaryString}
* @param {!goog.testing.fs.Blob} blob The blob to read.
*/
goog.testing.fs.FileReader.prototype.readAsBinaryString = function(blob) {
this.returnType_ = goog.testing.fs.FileReader.ReturnType.BINARY_STRING;
this.read_(blob);
};
/**
* @see {goog.fs.FileReader#readAsArrayBuffer}
* @param {!goog.testing.fs.Blob} blob The blob to read.
*/
goog.testing.fs.FileReader.prototype.readAsArrayBuffer = function(blob) {
this.returnType_ = goog.testing.fs.FileReader.ReturnType.ARRAY_BUFFER;
this.read_(blob);
};
/**
* @see {goog.fs.FileReader#readAsText}
* @param {!goog.testing.fs.Blob} blob The blob to read.
* @param {string=} opt_encoding The name of the encoding to use.
*/
goog.testing.fs.FileReader.prototype.readAsText = function(blob, opt_encoding) {
this.returnType_ = goog.testing.fs.FileReader.ReturnType.TEXT;
this.read_(blob);
};
/**
* @see {goog.fs.FileReader#readAsDataUrl}
* @param {!goog.testing.fs.Blob} blob The blob to read.
*/
goog.testing.fs.FileReader.prototype.readAsDataUrl = function(blob) {
this.returnType_ = goog.testing.fs.FileReader.ReturnType.DATA_URL;
this.read_(blob);
};
/**
* Abort the current action and emit appropriate events.
*
* @param {number} total The total data that was to be processed, in bytes.
* @private
*/
goog.testing.fs.FileReader.prototype.abort_ = function(total) {
this.error_ = new goog.fs.Error(
/** @type {!FileError} */ ({'name': 'AbortError'}),
'reading file');
this.progressEvent_(goog.fs.FileReader.EventType.ERROR, 0, total);
this.progressEvent_(goog.fs.FileReader.EventType.ABORT, 0, total);
this.readyState_ = goog.fs.FileReader.ReadyState.DONE;
this.progressEvent_(goog.fs.FileReader.EventType.LOAD_END, 0, total);
this.aborted_ = false;
};
/**
* Dispatch a progress event.
*
* @param {goog.fs.FileReader.EventType} type The event type.
* @param {number} loaded The number of bytes processed.
* @param {number} total The total data that was to be processed, in bytes.
* @private
*/
goog.testing.fs.FileReader.prototype.progressEvent_ = function(type, loaded,
total) {
this.dispatchEvent(new goog.testing.fs.ProgressEvent(type, loaded, total));
};
@@ -0,0 +1,23 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2011 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<!--
-->
<head>
<title>
Closure Unit Tests - goog.testing.fs.FileReader
</title>
<script type="text/javascript" src="../../base.js">
</script>
<script type="text/javascript">
goog.require('goog.testing.fs.FileReaderTest');
</script>
</head>
<body>
</body>
</html>
@@ -0,0 +1,234 @@
// Copyright 2011 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.
goog.provide('goog.testing.fs.FileReaderTest');
goog.setTestOnly('goog.testing.fs.FileReaderTest');
goog.require('goog.Timer');
goog.require('goog.async.Deferred');
goog.require('goog.events');
goog.require('goog.fs.Error');
goog.require('goog.fs.FileReader');
goog.require('goog.fs.FileSaver');
goog.require('goog.testing.AsyncTestCase');
goog.require('goog.testing.fs.FileReader');
goog.require('goog.testing.fs.FileSystem');
goog.require('goog.testing.jsunit');
var asyncTestCase = goog.testing.AsyncTestCase.createAndInstall();
var file, deferredReader;
var hasArrayBuffer = goog.isDef(goog.global.ArrayBuffer);
function setUp() {
var fs = new goog.testing.fs.FileSystem();
var fileEntry = fs.getRoot().createDirectorySync('foo').createFileSync('bar');
file = fileEntry.fileSync();
file.setDataInternal('test content');
deferredReader = new goog.async.Deferred();
goog.Timer.callOnce(
goog.bind(deferredReader.callback, deferredReader,
new goog.testing.fs.FileReader()));
}
function testRead() {
deferredReader.
addCallback(goog.partial(checkReadyState,
goog.fs.FileReader.ReadyState.INIT)).
addCallback(goog.partial(checkResult, undefined)).
addCallback(goog.partial(readAsText)).
addCallback(goog.partial(checkReadyState,
goog.fs.FileReader.ReadyState.LOADING)).
addCallback(goog.partial(checkResult, undefined)).
addCallback(goog.partial(waitForEvent,
goog.fs.FileReader.EventType.LOAD_START)).
addCallback(goog.partial(checkResult, undefined)).
addCallback(goog.partial(waitForEvent,
goog.fs.FileReader.EventType.LOAD)).
addCallback(goog.partial(checkResult, undefined)).
addCallback(goog.partial(waitForEvent,
goog.fs.FileReader.EventType.LOAD_END)).
addCallback(goog.partial(checkResult, file.toString())).
addCallback(goog.partial(checkReadyState,
goog.fs.FileReader.ReadyState.DONE)).
addBoth(continueTesting);
waitForAsync('testRead');
}
function testReadAsArrayBuffer() {
if (!hasArrayBuffer) {
// Skip if array buffer is not supported
return;
}
deferredReader.
addCallback(goog.partial(checkReadyState,
goog.fs.FileReader.ReadyState.INIT)).
addCallback(goog.partial(checkResult, undefined)).
addCallback(goog.partial(readAsArrayBuffer)).
addCallback(goog.partial(checkReadyState,
goog.fs.FileReader.ReadyState.LOADING)).
addCallback(goog.partial(checkResult, undefined)).
addCallback(goog.partial(waitForEvent,
goog.fs.FileReader.EventType.LOAD_START)).
addCallback(goog.partial(checkResult, undefined)).
addCallback(goog.partial(waitForEvent,
goog.fs.FileReader.EventType.LOAD)).
addCallback(goog.partial(checkResult, undefined)).
addCallback(goog.partial(waitForEvent,
goog.fs.FileReader.EventType.LOAD_END)).
addCallback(goog.partial(checkResult, file.toArrayBuffer())).
addCallback(goog.partial(checkReadyState,
goog.fs.FileReader.ReadyState.DONE)).
addBoth(continueTesting);
waitForAsync('testReadAsArrayBuffer');
}
function testReadAsDataUrl() {
deferredReader.
addCallback(goog.partial(checkReadyState,
goog.fs.FileReader.ReadyState.INIT)).
addCallback(goog.partial(checkResult, undefined)).
addCallback(goog.partial(readAsDataUrl)).
addCallback(goog.partial(checkReadyState,
goog.fs.FileReader.ReadyState.LOADING)).
addCallback(goog.partial(checkResult, undefined)).
addCallback(goog.partial(waitForEvent,
goog.fs.FileReader.EventType.LOAD_START)).
addCallback(goog.partial(checkResult, undefined)).
addCallback(goog.partial(waitForEvent,
goog.fs.FileReader.EventType.LOAD)).
addCallback(goog.partial(checkResult, undefined)).
addCallback(goog.partial(waitForEvent,
goog.fs.FileReader.EventType.LOAD_END)).
addCallback(goog.partial(checkResult, file.toDataUrl())).
addCallback(goog.partial(checkReadyState,
goog.fs.FileReader.ReadyState.DONE)).
addBoth(continueTesting);
waitForAsync('testReadAsDataUrl');
}
function testAbort() {
deferredReader.
addCallback(goog.partial(readAsText)).
addCallback(function(reader) { reader.abort(); }).
addCallback(goog.partial(checkReadyState,
goog.fs.FileReader.ReadyState.LOADING)).
addCallback(goog.partial(waitForError, goog.fs.Error.ErrorCode.ABORT)).
addCallback(goog.partial(checkReadyState,
goog.fs.FileReader.ReadyState.LOADING)).
addCallback(goog.partial(waitForEvent,
goog.fs.FileReader.EventType.ABORT)).
addCallback(goog.partial(checkReadyState,
goog.fs.FileReader.ReadyState.LOADING)).
addCallback(goog.partial(waitForEvent,
goog.fs.FileReader.EventType.LOAD_END)).
addCallback(goog.partial(checkReadyState,
goog.fs.FileReader.ReadyState.DONE)).
addCallback(goog.partial(checkResult, undefined)).
addBoth(continueTesting);
waitForAsync('testAbort');
}
function testAbortBeforeRead() {
deferredReader.
addCallback(function(reader) { reader.abort(); }).
addErrback(function(err) {
assertEquals(goog.fs.Error.ErrorCode.INVALID_STATE, err.code);
return true;
}).
addCallback(function(calledErrback) {
assertTrue(calledErrback);
}).
addBoth(continueTesting);
waitForAsync('testAbortBeforeRead');
}
function testReadDuringRead() {
deferredReader.
addCallback(goog.partial(readAsText)).
addCallback(goog.partial(readAsText)).
addErrback(function(err) {
assertEquals(goog.fs.Error.ErrorCode.INVALID_STATE, err.code);
return true;
}).
addCallback(assertTrue).
addBoth(continueTesting);
waitForAsync('testReadDuringRead');
}
function continueTesting(result) {
asyncTestCase.continueTesting();
if (result instanceof Error) {
throw result;
}
}
function waitForAsync(msg) {
asyncTestCase.waitForAsync(msg);
}
function waitForEvent(type, target) {
var d = new goog.async.Deferred();
goog.events.listenOnce(target, type, goog.bind(d.callback, d, target));
return d;
}
function waitForError(type, target) {
var d = new goog.async.Deferred();
goog.events.listenOnce(
target, goog.fs.FileReader.EventType.ERROR, function(e) {
assertEquals(type, target.getError().code);
d.callback(target);
});
return d;
}
function readAsText(reader) {
reader.readAsText(file);
}
function readAsArrayBuffer(reader) {
reader.readAsArrayBuffer(file);
}
function readAsDataUrl(reader) {
reader.readAsDataUrl(file);
}
function readAndWait(reader) {
readAsText(reader);
return waitForEvent(goog.fs.FileSaver.EventType.LOAD_END, reader);
}
function checkResult(expectedResult, reader) {
checkEquals(expectedResult, reader.getResult());
}
function checkEquals(a, b) {
if (hasArrayBuffer &&
a instanceof ArrayBuffer && b instanceof ArrayBuffer) {
assertEquals(a.byteLength, b.byteLength);
var viewA = new Uint8Array(a);
var viewB = new Uint8Array(b);
for (var i = 0; i < a.byteLength; i++) {
assertEquals(viewA[i], viewB[i]);
}
} else {
assertEquals(a, b);
}
}
function checkReadyState(expectedState, reader) {
assertEquals(expectedState, reader.getReadyState());
}
@@ -0,0 +1,64 @@
// Copyright 2011 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 Mock filesystem object.
*
*/
goog.provide('goog.testing.fs.FileSystem');
goog.require('goog.fs.FileSystem');
goog.require('goog.testing.fs.DirectoryEntry');
/**
* A mock filesystem object.
*
* @param {string=} opt_name The name of the filesystem.
* @constructor
* @implements {goog.fs.FileSystem}
* @final
*/
goog.testing.fs.FileSystem = function(opt_name) {
/**
* The name of the filesystem.
* @type {string}
* @private
*/
this.name_ = opt_name || 'goog.testing.fs.FileSystem';
/**
* The root entry of the filesystem.
* @type {!goog.testing.fs.DirectoryEntry}
* @private
*/
this.root_ = new goog.testing.fs.DirectoryEntry(this, null, '', {});
};
/** @override */
goog.testing.fs.FileSystem.prototype.getName = function() {
return this.name_;
};
/**
* @override
* @return {!goog.testing.fs.DirectoryEntry}
*/
goog.testing.fs.FileSystem.prototype.getRoot = function() {
return this.root_;
};
@@ -0,0 +1,268 @@
// Copyright 2011 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 Mock FileWriter object.
*
*/
goog.provide('goog.testing.fs.FileWriter');
goog.require('goog.Timer');
goog.require('goog.events.EventTarget');
goog.require('goog.fs.Error');
goog.require('goog.fs.FileSaver');
goog.require('goog.string');
goog.require('goog.testing.fs.ProgressEvent');
/**
* A mock FileWriter object. This emits the same events as
* {@link goog.fs.FileSaver} and {@link goog.fs.FileWriter}.
*
* @param {!goog.testing.fs.FileEntry} fileEntry The file entry to write to.
* @constructor
* @extends {goog.events.EventTarget}
* @final
*/
goog.testing.fs.FileWriter = function(fileEntry) {
goog.testing.fs.FileWriter.base(this, 'constructor');
/**
* The file entry to which to write.
* @type {!goog.testing.fs.FileEntry}
* @private
*/
this.fileEntry_ = fileEntry;
/**
* The file blob to write to.
* @type {!goog.testing.fs.File}
* @private
*/
this.file_ = fileEntry.fileSync();
/**
* The current state of the writer.
* @type {goog.fs.FileSaver.ReadyState}
* @private
*/
this.readyState_ = goog.fs.FileSaver.ReadyState.INIT;
};
goog.inherits(goog.testing.fs.FileWriter, goog.events.EventTarget);
/**
* The most recent error experienced by this writer.
* @type {goog.fs.Error}
* @private
*/
goog.testing.fs.FileWriter.prototype.error_;
/**
* Whether the current operation has been aborted.
* @type {boolean}
* @private
*/
goog.testing.fs.FileWriter.prototype.aborted_ = false;
/**
* The current position in the file.
* @type {number}
* @private
*/
goog.testing.fs.FileWriter.prototype.position_ = 0;
/**
* @see {goog.fs.FileSaver#getReadyState}
* @return {goog.fs.FileSaver.ReadyState} The ready state.
*/
goog.testing.fs.FileWriter.prototype.getReadyState = function() {
return this.readyState_;
};
/**
* @see {goog.fs.FileSaver#getError}
* @return {goog.fs.Error} The error.
*/
goog.testing.fs.FileWriter.prototype.getError = function() {
return this.error_;
};
/**
* @see {goog.fs.FileWriter#getPosition}
* @return {number} The position.
*/
goog.testing.fs.FileWriter.prototype.getPosition = function() {
return this.position_;
};
/**
* @see {goog.fs.FileWriter#getLength}
* @return {number} The length.
*/
goog.testing.fs.FileWriter.prototype.getLength = function() {
return this.file_.size;
};
/**
* @see {goog.fs.FileSaver#abort}
*/
goog.testing.fs.FileWriter.prototype.abort = function() {
if (this.readyState_ != goog.fs.FileSaver.ReadyState.WRITING) {
var msg = 'aborting save of ' + this.fileEntry_.getFullPath();
throw new goog.fs.Error(
/** @type {!FileError} */ ({'name': 'InvalidStateError'}),
msg);
}
this.aborted_ = true;
};
/**
* @see {goog.fs.FileWriter#write}
* @param {!goog.testing.fs.Blob} blob The blob to write.
*/
goog.testing.fs.FileWriter.prototype.write = function(blob) {
if (this.readyState_ == goog.fs.FileSaver.ReadyState.WRITING) {
var msg = 'writing to ' + this.fileEntry_.getFullPath();
throw new goog.fs.Error(
/** @type {!FileError} */ ({'name': 'InvalidStateError'}),
msg);
}
this.readyState_ = goog.fs.FileSaver.ReadyState.WRITING;
goog.Timer.callOnce(function() {
if (this.aborted_) {
this.abort_(blob.size);
return;
}
this.progressEvent_(goog.fs.FileSaver.EventType.WRITE_START, 0, blob.size);
var fileString = this.file_.toString();
this.file_.setDataInternal(
fileString.substring(0, this.position_) + blob.toString() +
fileString.substring(this.position_ + blob.size, fileString.length));
this.position_ += blob.size;
this.progressEvent_(
goog.fs.FileSaver.EventType.WRITE, blob.size, blob.size);
this.readyState_ = goog.fs.FileSaver.ReadyState.DONE;
this.progressEvent_(
goog.fs.FileSaver.EventType.WRITE_END, blob.size, blob.size);
}, 0, this);
};
/**
* @see {goog.fs.FileWriter#truncate}
* @param {number} size The size to truncate to.
*/
goog.testing.fs.FileWriter.prototype.truncate = function(size) {
if (this.readyState_ == goog.fs.FileSaver.ReadyState.WRITING) {
var msg = 'truncating ' + this.fileEntry_.getFullPath();
throw new goog.fs.Error(
/** @type {!FileError} */ ({'name': 'InvalidStateError'}),
msg);
}
this.readyState_ = goog.fs.FileSaver.ReadyState.WRITING;
goog.Timer.callOnce(function() {
if (this.aborted_) {
this.abort_(size);
return;
}
this.progressEvent_(goog.fs.FileSaver.EventType.WRITE_START, 0, size);
var fileString = this.file_.toString();
if (size > fileString.length) {
this.file_.setDataInternal(
fileString + goog.string.repeat('\0', size - fileString.length));
} else {
this.file_.setDataInternal(fileString.substring(0, size));
}
this.position_ = Math.min(this.position_, size);
this.progressEvent_(goog.fs.FileSaver.EventType.WRITE, size, size);
this.readyState_ = goog.fs.FileSaver.ReadyState.DONE;
this.progressEvent_(goog.fs.FileSaver.EventType.WRITE_END, size, size);
}, 0, this);
};
/**
* @see {goog.fs.FileWriter#seek}
* @param {number} offset The offset to seek to.
*/
goog.testing.fs.FileWriter.prototype.seek = function(offset) {
if (this.readyState_ == goog.fs.FileSaver.ReadyState.WRITING) {
var msg = 'truncating ' + this.fileEntry_.getFullPath();
throw new goog.fs.Error(
/** @type {!FileError} */ ({name: 'InvalidStateError'}),
msg);
}
if (offset < 0) {
this.position_ = Math.max(0, this.file_.size + offset);
} else {
this.position_ = Math.min(offset, this.file_.size);
}
};
/**
* Abort the current action and emit appropriate events.
*
* @param {number} total The total data that was to be processed, in bytes.
* @private
*/
goog.testing.fs.FileWriter.prototype.abort_ = function(total) {
this.error_ = new goog.fs.Error(
/** @type {!FileError} */ ({'name': 'AbortError'}),
'saving ' + this.fileEntry_.getFullPath());
this.progressEvent_(goog.fs.FileSaver.EventType.ERROR, 0, total);
this.progressEvent_(goog.fs.FileSaver.EventType.ABORT, 0, total);
this.readyState_ = goog.fs.FileSaver.ReadyState.DONE;
this.progressEvent_(goog.fs.FileSaver.EventType.WRITE_END, 0, total);
this.aborted_ = false;
};
/**
* Dispatch a progress event.
*
* @param {goog.fs.FileSaver.EventType} type The type of the event.
* @param {number} loaded The number of bytes processed.
* @param {number} total The total data that was to be processed, in bytes.
* @private
*/
goog.testing.fs.FileWriter.prototype.progressEvent_ = function(
type, loaded, total) {
// On write, update the last modified date to the current (real or mock) time.
if (type == goog.fs.FileSaver.EventType.WRITE) {
this.file_.lastModifiedDate = new Date(goog.now());
}
this.dispatchEvent(new goog.testing.fs.ProgressEvent(type, loaded, total));
};
@@ -0,0 +1,23 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2011 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<!--
-->
<head>
<title>
Closure Unit Tests - goog.testing.fs.FileWriter
</title>
<script src="../../base.js">
</script>
<script>
goog.require('goog.testing.fs.FileWriterTest');
</script>
</head>
<body>
</body>
</html>
@@ -0,0 +1,322 @@
// Copyright 2011 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.
goog.provide('goog.testing.fs.FileWriterTest');
goog.setTestOnly('goog.testing.fs.FileWriterTest');
goog.require('goog.async.Deferred');
goog.require('goog.events');
goog.require('goog.fs.Error');
goog.require('goog.fs.FileSaver');
goog.require('goog.testing.AsyncTestCase');
goog.require('goog.testing.MockClock');
goog.require('goog.testing.fs.Blob');
goog.require('goog.testing.fs.FileSystem');
goog.require('goog.testing.jsunit');
var asyncTestCase = goog.testing.AsyncTestCase.createAndInstall();
var file, deferredWriter, mockClock;
function setUp() {
mockClock = new goog.testing.MockClock(true);
var fs = new goog.testing.fs.FileSystem();
var fileEntry = fs.getRoot().createDirectorySync('foo').createFileSync('bar');
deferredWriter = fileEntry.createWriter();
file = fileEntry.fileSync();
file.setDataInternal('');
}
function tearDown() {
mockClock.uninstall();
}
function testWrite() {
deferredWriter.
addCallback(goog.partial(checkReadyState,
goog.fs.FileSaver.ReadyState.INIT)).
addCallback(goog.partial(checkPositionAndLength, 0, 0)).
addCallback(goog.partial(checkLastModified, 0)).
addCallback(goog.partial(tick, 3)).
addCallback(goog.partial(writeString, 'hello')).
addCallback(goog.partial(checkPositionAndLength, 0, 0)).
addCallback(goog.partial(checkReadyState,
goog.fs.FileSaver.ReadyState.WRITING)).
addCallback(goog.partial(checkLastModified, 0)).
addCallback(goog.partial(waitForEvent,
goog.fs.FileSaver.EventType.WRITE_START)).
addCallback(goog.partial(checkLastModified, 0)).
addCallback(goog.partial(checkPositionAndLength, 0, 0)).
addCallback(goog.partial(waitForEvent,
goog.fs.FileSaver.EventType.WRITE)).
addCallback(function() { assertEquals('hello', file.toString()); }).
addCallback(goog.partial(checkPositionAndLength, 5, 5)).
addCallback(goog.partial(checkLastModified, 3)).
addCallback(tick).
addCallback(goog.partial(checkReadyState,
goog.fs.FileSaver.ReadyState.WRITING)).
addCallback(goog.partial(checkLastModified, 3)).
addCallback(goog.partial(waitForEvent,
goog.fs.FileSaver.EventType.WRITE_END)).
addCallback(goog.partial(checkLastModified, 3)).
addCallback(goog.partial(checkReadyState,
goog.fs.FileSaver.ReadyState.DONE)).
addCallback(goog.partial(checkLastModified, 3)).
addCallback(goog.partial(writeString, ' world')).
addCallback(goog.partial(checkPositionAndLength, 5, 5)).
addCallback(goog.partial(waitForEvent,
goog.fs.FileSaver.EventType.WRITE)).
addCallback(function() { assertEquals('hello world', file.toString()); }).
addCallback(goog.partial(checkPositionAndLength, 11, 11)).
addCallback(goog.partial(checkLastModified, 4)).
addBoth(continueTesting);
waitForAsync('testWrite');
}
function testSeek() {
deferredWriter.
addCallback(goog.partial(tick, 17)).
addCallback(goog.partial(writeAndWait, 'hello world')).
addCallback(tick).
addCallback(goog.partial(checkPositionAndLength, 11, 11)).
addCallback(function(writer) { writer.seek(6); }).
addCallback(goog.partial(checkPositionAndLength, 6, 11)).
addCallback(goog.partial(checkLastModified, 17)).
addCallback(goog.partial(writeAndWait, 'universe')).
addCallback(tick).
addCallback(function() {
assertEquals('hello universe', file.toString());
}).
addCallback(goog.partial(checkPositionAndLength, 14, 14)).
addCallback(function(writer) { writer.seek(500); }).
addCallback(goog.partial(checkPositionAndLength, 14, 14)).
addCallback(goog.partial(writeAndWait, '!')).
addCallback(tick).
addCallback(function() {
assertEquals('hello universe!', file.toString());
}).
addCallback(goog.partial(checkPositionAndLength, 15, 15)).
addCallback(function(writer) { writer.seek(-9); }).
addCallback(goog.partial(checkPositionAndLength, 6, 15)).
addCallback(goog.partial(writeAndWait, 'foo')).
addCallback(tick).
addCallback(function() {
assertEquals('hello fooverse!', file.toString());
}).
addCallback(goog.partial(checkPositionAndLength, 9, 15)).
addCallback(function(writer) { writer.seek(-500); }).
addCallback(goog.partial(checkPositionAndLength, 0, 15)).
addCallback(goog.partial(writeAndWait, 'bye-o')).
addCallback(tick).
addCallback(function() {
assertEquals('bye-o fooverse!', file.toString());
}).
addCallback(goog.partial(checkPositionAndLength, 5, 15)).
addCallback(goog.partial(checkLastModified, 21)).
addBoth(continueTesting);
waitForAsync('testSeek');
}
function testAbort() {
deferredWriter.
addCallback(goog.partial(tick, 13)).
addCallback(goog.partial(writeString, 'hello world')).
addCallback(function(writer) { writer.abort(); }).
addCallback(goog.partial(checkReadyState,
goog.fs.FileSaver.ReadyState.WRITING)).
addCallback(goog.partial(waitForError, goog.fs.Error.ErrorCode.ABORT)).
addCallback(goog.partial(checkReadyState,
goog.fs.FileSaver.ReadyState.WRITING)).
addCallback(goog.partial(waitForEvent,
goog.fs.FileSaver.EventType.ABORT)).
addCallback(goog.partial(checkReadyState,
goog.fs.FileSaver.ReadyState.WRITING)).
addCallback(goog.partial(waitForEvent,
goog.fs.FileSaver.EventType.WRITE_END)).
addCallback(goog.partial(checkReadyState,
goog.fs.FileSaver.ReadyState.DONE)).
addCallback(goog.partial(checkPositionAndLength, 0, 0)).
addCallback(goog.partial(checkLastModified, 0)).
addCallback(function() { assertEquals('', file.toString()); }).
addBoth(continueTesting);
waitForAsync('testAbort');
}
function testTruncate() {
deferredWriter.
addCallback(goog.partial(writeAndWait, 'hello world')).
addCallback(goog.partial(checkPositionAndLength, 11, 11)).
addCallback(function(writer) { writer.truncate(5); }).
addCallback(goog.partial(checkPositionAndLength, 11, 11)).
addCallback(goog.partial(checkReadyState,
goog.fs.FileSaver.ReadyState.WRITING)).
addCallback(goog.partial(waitForEvent,
goog.fs.FileSaver.EventType.WRITE_START)).
addCallback(goog.partial(tick, 7)).
addCallback(goog.partial(checkPositionAndLength, 11, 11)).
addCallback(goog.partial(checkLastModified, 0)).
addCallback(goog.partial(waitForEvent,
goog.fs.FileSaver.EventType.WRITE)).
addCallback(goog.partial(checkLastModified, 7)).
addCallback(tick).
addCallback(goog.partial(checkReadyState,
goog.fs.FileSaver.ReadyState.WRITING)).
addCallback(goog.partial(checkPositionAndLength, 5, 5)).
addCallback(function() { assertEquals('hello', file.toString()); }).
addCallback(goog.partial(waitForEvent,
goog.fs.FileSaver.EventType.WRITE_END)).
addCallback(goog.partial(checkReadyState,
goog.fs.FileSaver.ReadyState.DONE)).
addCallback(function(writer) { writer.truncate(10); }).
addCallback(goog.partial(waitForEvent,
goog.fs.FileSaver.EventType.WRITE_END)).
addCallback(goog.partial(checkPositionAndLength, 5, 10)).
addCallback(goog.partial(checkLastModified, 8)).
addCallback(function() {
assertEquals('hello\0\0\0\0\0', file.toString());
}).
addBoth(continueTesting);
waitForAsync('testTruncate');
}
function testAbortBeforeWrite() {
deferredWriter.
addCallback(function(writer) { writer.abort(); }).
addErrback(function(err) {
assertEquals(goog.fs.Error.ErrorCode.INVALID_STATE, err.code);
return true;
}).
addCallback(function(calledErrback) {
assertTrue(calledErrback);
}).
addBoth(continueTesting);
waitForAsync('testAbortBeforeWrite');
}
function testAbortAfterWrite() {
deferredWriter.
addCallback(goog.partial(writeAndWait, 'hello world')).
addCallback(function(writer) { writer.abort(); }).
addErrback(function(err) {
assertEquals(goog.fs.Error.ErrorCode.INVALID_STATE, err.code);
return true;
}).
addCallback(assertTrue).
addBoth(continueTesting);
waitForAsync('testAbortAfterWrite');
}
function testWriteDuringWrite() {
deferredWriter.
addCallback(goog.partial(writeString, 'hello world')).
addCallback(goog.partial(writeString, 'hello world')).
addErrback(function(err) {
assertEquals(goog.fs.Error.ErrorCode.INVALID_STATE, err.code);
return true;
}).
addCallback(assertTrue).
addBoth(continueTesting);
waitForAsync('testWriteDuringWrite');
}
function testSeekDuringWrite() {
deferredWriter.
addCallback(goog.partial(writeString, 'hello world')).
addCallback(function(writer) { writer.seek(5); }).
addErrback(function(err) {
assertEquals(goog.fs.Error.ErrorCode.INVALID_STATE, err.code);
return true;
}).
addCallback(assertTrue).
addBoth(continueTesting);
waitForAsync('testSeekDuringWrite');
}
function testTruncateDuringWrite() {
deferredWriter.
addCallback(goog.partial(writeString, 'hello world')).
addCallback(function(writer) { writer.truncate(5); }).
addErrback(function(err) {
assertEquals(goog.fs.Error.ErrorCode.INVALID_STATE, err.code);
return true;
}).
addCallback(assertTrue).
addBoth(continueTesting);
waitForAsync('testTruncateDuringWrite');
}
function tick(opt_tickCount) {
mockClock.tick(opt_tickCount);
}
function continueTesting(result) {
asyncTestCase.continueTesting();
if (result instanceof Error) {
throw result;
}
mockClock.tick();
}
function waitForAsync(msg) {
asyncTestCase.waitForAsync(msg);
// The mock clock must be advanced far enough that all timeouts added during
// callbacks will be triggered. 1000ms is much more than enough.
mockClock.tick(1000);
}
function waitForEvent(type, target) {
var d = new goog.async.Deferred();
goog.events.listenOnce(target, type, goog.bind(d.callback, d, target));
return d;
}
function waitForError(type, target) {
var d = new goog.async.Deferred();
goog.events.listenOnce(
target, goog.fs.FileSaver.EventType.ERROR, function(e) {
assertEquals(type, target.getError().code);
d.callback(target);
});
return d;
}
function checkReadyState(expectedState, writer) {
assertEquals(expectedState, writer.getReadyState());
}
function checkPositionAndLength(expectedPosition, expectedLength, writer) {
assertEquals(expectedPosition, writer.getPosition());
assertEquals(expectedLength, writer.getLength());
}
function checkLastModified(expectedTime) {
assertEquals(expectedTime, file.lastModifiedDate.getTime());
}
function writeString(str, writer) {
writer.write(new goog.testing.fs.Blob(str));
}
function writeAndWait(str, writer) {
writeString(str, writer);
return waitForEvent(goog.fs.FileSaver.EventType.WRITE_END, writer);
}
@@ -0,0 +1,169 @@
// Copyright 2011 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 Mock implementations of the Closure HTML5 FileSystem wrapper
* classes. These implementations are designed to be usable in any browser, so
* they use none of the native FileSystem-related objects.
*
*/
goog.provide('goog.testing.fs');
goog.require('goog.Timer');
goog.require('goog.array');
goog.require('goog.async.Deferred');
/** @suppress {extraRequire} */
goog.require('goog.fs');
goog.require('goog.testing.fs.Blob');
goog.require('goog.testing.fs.FileSystem');
/**
* Get a filesystem object. Since these are mocks, there's no difference between
* temporary and persistent filesystems.
*
* @param {number} size Ignored.
* @return {!goog.async.Deferred} The deferred
* {@link goog.testing.fs.FileSystem}.
*/
goog.testing.fs.getTemporary = function(size) {
var d = new goog.async.Deferred();
goog.Timer.callOnce(
goog.bind(d.callback, d, new goog.testing.fs.FileSystem()));
return d;
};
/**
* Get a filesystem object. Since these are mocks, there's no difference between
* temporary and persistent filesystems.
*
* @param {number} size Ignored.
* @return {!goog.async.Deferred} The deferred
* {@link goog.testing.fs.FileSystem}.
*/
goog.testing.fs.getPersistent = function(size) {
return goog.testing.fs.getTemporary(size);
};
/**
* Which object URLs have been granted for fake blobs.
* @type {!Object<boolean>}
* @private
*/
goog.testing.fs.objectUrls_ = {};
/**
* Create a fake object URL for a given fake blob. This can be used as a real
* URL, and it can be created and revoked normally.
*
* @param {!goog.testing.fs.Blob} blob The blob for which to create the URL.
* @return {string} The URL.
*/
goog.testing.fs.createObjectUrl = function(blob) {
var url = blob.toDataUrl();
goog.testing.fs.objectUrls_[url] = true;
return url;
};
/**
* Remove a URL that was created for a fake blob.
*
* @param {string} url The URL to revoke.
*/
goog.testing.fs.revokeObjectUrl = function(url) {
delete goog.testing.fs.objectUrls_[url];
};
/**
* Return whether or not a URL has been granted for the given blob.
*
* @param {!goog.testing.fs.Blob} blob The blob to check.
* @return {boolean} Whether a URL has been granted.
*/
goog.testing.fs.isObjectUrlGranted = function(blob) {
return (blob.toDataUrl()) in goog.testing.fs.objectUrls_;
};
/**
* Concatenates one or more values together and converts them to a fake blob.
*
* @param {...(string|!goog.testing.fs.Blob)} var_args The values that will make
* up the resulting blob.
* @return {!goog.testing.fs.Blob} The blob.
*/
goog.testing.fs.getBlob = function(var_args) {
return new goog.testing.fs.Blob(goog.array.map(arguments, String).join(''));
};
/**
* Creates a blob with the given properties.
* See https://developer.mozilla.org/en-US/docs/Web/API/Blob for more details.
*
* @param {Array<string|!goog.testing.fs.Blob>} parts
* The values that will make up the resulting blob.
* @param {string=} opt_type The MIME type of the Blob.
* @param {string=} opt_endings Specifies how strings containing newlines are to
* be written out.
* @return {!goog.testing.fs.Blob} The blob.
*/
goog.testing.fs.getBlobWithProperties = function(parts, opt_type, opt_endings) {
return new goog.testing.fs.Blob(goog.array.map(parts, String).join(''),
opt_type);
};
/**
* Returns the string value of a fake blob.
*
* @param {!goog.testing.fs.Blob} blob The blob to convert to a string.
* @param {string=} opt_encoding Ignored.
* @return {!goog.async.Deferred} The deferred string value of the blob.
*/
goog.testing.fs.blobToString = function(blob, opt_encoding) {
var d = new goog.async.Deferred();
goog.Timer.callOnce(goog.bind(d.callback, d, blob.toString()));
return d;
};
/**
* Installs goog.testing.fs in place of the standard goog.fs. After calling
* this, code that uses goog.fs should work without issue using goog.testing.fs.
*
* @param {!goog.testing.PropertyReplacer} stubs The property replacer for
* stubbing out the original goog.fs functions.
*/
goog.testing.fs.install = function(stubs) {
// Prevent warnings that goog.fs may get optimized away. It's true this is
// unsafe in compiled code, but it's only meant for tests.
var fs = goog.getObjectByName('goog.fs');
stubs.replace(fs, 'getTemporary', goog.testing.fs.getTemporary);
stubs.replace(fs, 'getPersistent', goog.testing.fs.getPersistent);
stubs.replace(fs, 'createObjectUrl', goog.testing.fs.createObjectUrl);
stubs.replace(fs, 'revokeObjectUrl', goog.testing.fs.revokeObjectUrl);
stubs.replace(fs, 'getBlob', goog.testing.fs.getBlob);
stubs.replace(fs, 'getBlobWithProperties',
goog.testing.fs.getBlobWithProperties);
stubs.replace(fs, 'blobToString', goog.testing.fs.blobToString);
stubs.replace(fs, 'browserSupportsObjectUrls',
function() { return true; });
};
@@ -0,0 +1,23 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2011 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<!--
-->
<head>
<title>
Closure Unit Tests - goog.testing.fs
</title>
<script src="../../base.js">
</script>
<script>
goog.require('goog.testing.fsTest');
</script>
</head>
<body>
</body>
</html>
@@ -0,0 +1,55 @@
// Copyright 2011 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.
goog.provide('goog.testing.fsTest');
goog.setTestOnly('goog.testing.fsTest');
goog.require('goog.testing.AsyncTestCase');
goog.require('goog.testing.fs');
goog.require('goog.testing.fs.Blob');
goog.require('goog.testing.jsunit');
var asyncTestCase = goog.testing.AsyncTestCase.createAndInstall();
function testObjectUrls() {
var blob = goog.testing.fs.getBlob('foo');
var url = goog.testing.fs.createObjectUrl(blob);
assertTrue(goog.testing.fs.isObjectUrlGranted(blob));
goog.testing.fs.revokeObjectUrl(url);
assertFalse(goog.testing.fs.isObjectUrlGranted(blob));
}
function testGetBlob() {
assertEquals(
new goog.testing.fs.Blob('foobarbaz').toString(),
goog.testing.fs.getBlob('foo', 'bar', 'baz').toString());
assertEquals(
new goog.testing.fs.Blob('foobarbaz').toString(),
goog.testing.fs.getBlob('foo', new goog.testing.fs.Blob('bar'), 'baz').
toString());
}
function testBlobToString() {
goog.testing.fs.blobToString(new goog.testing.fs.Blob('foobarbaz')).
addCallback(goog.partial(assertEquals, 'foobarbaz')).
addCallback(goog.bind(asyncTestCase.continueTesting, asyncTestCase));
asyncTestCase.waitForAsync('testBlobToString');
}
function testGetBlobWithProperties() {
assertEquals(
'data:spam/eggs;base64,Zm9vYmFy',
new goog.testing.fs.getBlobWithProperties(
['foo', new goog.testing.fs.Blob('bar')], 'spam/eggs').toDataUrl());
}
@@ -0,0 +1,25 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2011 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<!--
-->
<head>
<title>
Closure Integration Tests - goog.testing.fs
</title>
<script src="../../base.js">
</script>
<script>
goog.require('goog.testing.fs.integrationTest');
</script>
</head>
<body>
<div id="closureTestRunnerLog">
</div>
</body>
</html>
@@ -0,0 +1,221 @@
// Copyright 2011 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.
goog.provide('goog.testing.fs.integrationTest');
goog.setTestOnly('goog.testing.fs.integrationTest');
goog.require('goog.async.Deferred');
goog.require('goog.async.DeferredList');
goog.require('goog.events');
goog.require('goog.fs');
goog.require('goog.fs.DirectoryEntry');
goog.require('goog.fs.Error');
goog.require('goog.fs.FileSaver');
goog.require('goog.testing.AsyncTestCase');
goog.require('goog.testing.PropertyReplacer');
goog.require('goog.testing.fs');
goog.require('goog.testing.jsunit');
var TEST_DIR = 'goog-fs-test-dir';
var deferredFs = goog.testing.fs.getTemporary();
var asyncTestCase = goog.testing.AsyncTestCase.createAndInstall();
function setUpPage() {
goog.testing.fs.install(new goog.testing.PropertyReplacer());
}
function tearDown() {
loadTestDir().
addCallback(function(dir) { return dir.removeRecursively(); }).
addBoth(continueTesting);
asyncTestCase.waitForAsync('removing filesystem');
}
function testWriteFile() {
loadFile('test', goog.fs.DirectoryEntry.Behavior.CREATE).
addCallback(goog.partial(writeToFile, 'test content')).
addCallback(goog.partial(checkFileContent, 'test content')).
addBoth(continueTesting);
asyncTestCase.waitForAsync('testWriteFile');
}
function testRemoveFile() {
loadFile('test', goog.fs.DirectoryEntry.Behavior.CREATE).
addCallback(goog.partial(writeToFile, 'test content')).
addCallback(function(fileEntry) { return fileEntry.remove(); }).
addCallback(goog.partial(checkFileRemoved, 'test')).
addBoth(continueTesting);
asyncTestCase.waitForAsync('testRemoveFile');
}
function testMoveFile() {
var deferredSubdir = loadDirectory(
'subdir', goog.fs.DirectoryEntry.Behavior.CREATE);
var deferredWrittenFile =
loadFile('test', goog.fs.DirectoryEntry.Behavior.CREATE).
addCallback(goog.partial(writeToFile, 'test content'));
goog.async.DeferredList.gatherResults([deferredSubdir, deferredWrittenFile]).
addCallback(splitArgs(function(dir, fileEntry) {
return fileEntry.moveTo(dir);
})).
addCallback(goog.partial(checkFileContent, 'test content')).
addCallback(goog.partial(checkFileRemoved, 'test')).
addBoth(continueTesting);
asyncTestCase.waitForAsync('testMoveFile');
}
function testCopyFile() {
var deferredFile = loadFile('test', goog.fs.DirectoryEntry.Behavior.CREATE);
var deferredSubdir = loadDirectory(
'subdir', goog.fs.DirectoryEntry.Behavior.CREATE);
var deferredWrittenFile = deferredFile.branch().
addCallback(goog.partial(writeToFile, 'test content'));
goog.async.DeferredList.gatherResults([deferredSubdir, deferredWrittenFile]).
addCallback(splitArgs(function(dir, fileEntry) {
return fileEntry.copyTo(dir);
})).
addCallback(goog.partial(checkFileContent, 'test content')).
awaitDeferred(deferredFile).
addCallback(goog.partial(checkFileContent, 'test content')).
addBoth(continueTesting);
asyncTestCase.waitForAsync('testCopyFile');
}
function testAbortWrite() {
var deferredFile = loadFile('test', goog.fs.DirectoryEntry.Behavior.CREATE);
deferredFile.branch().
addCallback(goog.partial(startWrite, 'test content')).
addCallback(function(writer) { writer.abort(); }).
addCallback(
goog.partial(waitForEvent, goog.fs.FileSaver.EventType.ABORT)).
awaitDeferred(deferredFile).
addCallback(goog.partial(checkFileContent, '')).
addBoth(continueTesting);
asyncTestCase.waitForAsync('testAbortWrite');
}
function testSeek() {
var deferredFile = loadFile('test', goog.fs.DirectoryEntry.Behavior.CREATE);
deferredFile.branch().
addCallback(goog.partial(writeToFile, 'test content')).
addCallback(function(fileEntry) { return fileEntry.createWriter(); }).
addCallback(
goog.partial(checkReadyState, goog.fs.FileSaver.ReadyState.INIT)).
addCallback(function(writer) {
writer.seek(5);
writer.write(goog.fs.getBlob('stuff and things'));
}).
addCallback(
goog.partial(checkReadyState, goog.fs.FileSaver.ReadyState.WRITING)).
addCallback(
goog.partial(waitForEvent, goog.fs.FileSaver.EventType.WRITE)).
awaitDeferred(deferredFile).
addCallback(goog.partial(checkFileContent, 'test stuff and things')).
addBoth(continueTesting);
asyncTestCase.waitForAsync('testSeek');
}
function testTruncate() {
var deferredFile = loadFile('test', goog.fs.DirectoryEntry.Behavior.CREATE);
deferredFile.branch().
addCallback(goog.partial(writeToFile, 'test content')).
addCallback(function(fileEntry) { return fileEntry.createWriter(); }).
addCallback(
goog.partial(checkReadyState, goog.fs.FileSaver.ReadyState.INIT)).
addCallback(function(writer) { writer.truncate(4); }).
addCallback(
goog.partial(checkReadyState, goog.fs.FileSaver.ReadyState.WRITING)).
addCallback(
goog.partial(waitForEvent, goog.fs.FileSaver.EventType.WRITE)).
awaitDeferred(deferredFile).
addCallback(goog.partial(checkFileContent, 'test')).
addBoth(continueTesting);
asyncTestCase.waitForAsync('testTruncate');
}
function continueTesting(result) {
asyncTestCase.continueTesting();
if (result instanceof Error) {
throw result;
}
}
function loadTestDir() {
return deferredFs.branch().addCallback(function(fs) {
return fs.getRoot().getDirectory(
TEST_DIR, goog.fs.DirectoryEntry.Behavior.CREATE);
});
}
function loadFile(filename, behavior) {
return loadTestDir().addCallback(function(dir) {
return dir.getFile(filename, behavior);
});
}
function loadDirectory(filename, behavior) {
return loadTestDir().addCallback(function(dir) {
return dir.getDirectory(filename, behavior);
});
}
function startWrite(content, fileEntry) {
return fileEntry.createWriter().
addCallback(
goog.partial(checkReadyState, goog.fs.FileSaver.ReadyState.INIT)).
addCallback(function(writer) {
writer.write(goog.fs.getBlob(content));
return writer;
}).
addCallback(
goog.partial(checkReadyState, goog.fs.FileSaver.ReadyState.WRITING));
}
function waitForEvent(type, target) {
var d = new goog.async.Deferred();
goog.events.listenOnce(target, type, d.callback, false, d);
return d;
}
function writeToFile(content, fileEntry) {
return startWrite(content, fileEntry).
addCallback(
goog.partial(waitForEvent, goog.fs.FileSaver.EventType.WRITE)).
addCallback(function() { return fileEntry; });
}
function checkFileContent(content, fileEntry) {
return fileEntry.file().
addCallback(function(blob) { return goog.fs.blobToString(blob); }).
addCallback(goog.partial(assertEquals, content));
}
function checkFileRemoved(filename) {
return loadFile(filename).
addCallback(goog.partial(fail, 'expected file to be removed')).
addErrback(function(err) {
assertEquals(err.code, goog.fs.Error.ErrorCode.NOT_FOUND);
return true; // Go back to callback path
});
}
function checkReadyState(expectedState, writer) {
assertEquals(expectedState, writer.getReadyState());
}
function splitArgs(fn) {
return function(args) { return fn(args[0], args[1]); };
}
@@ -0,0 +1,82 @@
// Copyright 2011 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 Mock ProgressEvent object.
*
*/
goog.provide('goog.testing.fs.ProgressEvent');
goog.require('goog.events.Event');
/**
* A mock progress event.
*
* @param {!goog.fs.FileSaver.EventType|!goog.fs.FileReader.EventType} type
* Event type.
* @param {number} loaded The number of bytes processed.
* @param {number} total The total data that was to be processed, in bytes.
* @constructor
* @extends {goog.events.Event}
* @final
*/
goog.testing.fs.ProgressEvent = function(type, loaded, total) {
goog.testing.fs.ProgressEvent.base(this, 'constructor', type);
/**
* The number of bytes processed.
* @type {number}
* @private
*/
this.loaded_ = loaded;
/**
* The total data that was to be procesed, in bytes.
* @type {number}
* @private
*/
this.total_ = total;
};
goog.inherits(goog.testing.fs.ProgressEvent, goog.events.Event);
/**
* @see {goog.fs.ProgressEvent#isLengthComputable}
* @return {boolean} True if the length is known.
*/
goog.testing.fs.ProgressEvent.prototype.isLengthComputable = function() {
return true;
};
/**
* @see {goog.fs.ProgressEvent#getLoaded}
* @return {number} The number of bytes loaded or written.
*/
goog.testing.fs.ProgressEvent.prototype.getLoaded = function() {
return this.loaded_;
};
/**
* @see {goog.fs.ProgressEvent#getTotal}
* @return {number} The total bytes to load or write.
*/
goog.testing.fs.ProgressEvent.prototype.getTotal = function() {
return this.total_;
};
@@ -0,0 +1,176 @@
// 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 Enable mocking of functions not attached to objects
* whether they be global / top-level or anonymous methods / closures.
*
* See the unit tests for usage.
*
*/
goog.provide('goog.testing');
goog.provide('goog.testing.FunctionMock');
goog.provide('goog.testing.GlobalFunctionMock');
goog.provide('goog.testing.MethodMock');
goog.require('goog.object');
goog.require('goog.testing.LooseMock');
goog.require('goog.testing.Mock');
goog.require('goog.testing.PropertyReplacer');
goog.require('goog.testing.StrictMock');
/**
* Class used to mock a function. Useful for mocking closures and anonymous
* callbacks etc. Creates a function object that extends goog.testing.Mock.
* @param {string=} opt_functionName The optional name of the function to mock.
* Set to '[anonymous mocked function]' if not passed in.
* @param {number=} opt_strictness One of goog.testing.Mock.LOOSE or
* goog.testing.Mock.STRICT. The default is STRICT.
* @return {!goog.testing.MockInterface} The mocked function.
* @suppress {missingProperties} Mocks do not fit in the type system well.
*/
goog.testing.FunctionMock = function(opt_functionName, opt_strictness) {
var fn = function() {
var args = Array.prototype.slice.call(arguments);
args.splice(0, 0, opt_functionName || '[anonymous mocked function]');
return fn.$mockMethod.apply(fn, args);
};
var base = opt_strictness === goog.testing.Mock.LOOSE ?
goog.testing.LooseMock : goog.testing.StrictMock;
goog.object.extend(fn, new base({}));
return /** @type {!goog.testing.MockInterface} */ (fn);
};
/**
* Mocks an existing function. Creates a goog.testing.FunctionMock
* and registers it in the given scope with the name specified by functionName.
* @param {Object} scope The scope of the method to be mocked out.
* @param {string} functionName The name of the function we're going to mock.
* @param {number=} opt_strictness One of goog.testing.Mock.LOOSE or
* goog.testing.Mock.STRICT. The default is STRICT.
* @return {!goog.testing.MockInterface} The mocked method.
*/
goog.testing.MethodMock = function(scope, functionName, opt_strictness) {
if (!(functionName in scope)) {
throw Error(functionName + ' is not a property of the given scope.');
}
var fn = goog.testing.FunctionMock(functionName, opt_strictness);
fn.$propertyReplacer_ = new goog.testing.PropertyReplacer();
fn.$propertyReplacer_.set(scope, functionName, fn);
fn.$tearDown = goog.testing.MethodMock.$tearDown;
return fn;
};
/**
* Resets the global function that we mocked back to its original state.
* @this {goog.testing.MockInterface}
*/
goog.testing.MethodMock.$tearDown = function() {
this.$propertyReplacer_.reset();
};
/**
* Mocks a global / top-level function. Creates a goog.testing.MethodMock
* in the global scope with the name specified by functionName.
* @param {string} functionName The name of the function we're going to mock.
* @param {number=} opt_strictness One of goog.testing.Mock.LOOSE or
* goog.testing.Mock.STRICT. The default is STRICT.
* @return {!goog.testing.MockInterface} The mocked global function.
*/
goog.testing.GlobalFunctionMock = function(functionName, opt_strictness) {
return goog.testing.MethodMock(goog.global, functionName, opt_strictness);
};
/**
* Convenience method for creating a mock for a function.
* @param {string=} opt_functionName The optional name of the function to mock
* set to '[anonymous mocked function]' if not passed in.
* @param {number=} opt_strictness One of goog.testing.Mock.LOOSE or
* goog.testing.Mock.STRICT. The default is STRICT.
* @return {goog.testing.MockInterface} The mocked function.
*/
goog.testing.createFunctionMock = function(opt_functionName, opt_strictness) {
return goog.testing.FunctionMock(opt_functionName, opt_strictness);
};
/**
* Convenience method for creating a mock for a method.
* @param {Object} scope The scope of the method to be mocked out.
* @param {string} functionName The name of the function we're going to mock.
* @param {number=} opt_strictness One of goog.testing.Mock.LOOSE or
* goog.testing.Mock.STRICT. The default is STRICT.
* @return {!goog.testing.MockInterface} The mocked global function.
*/
goog.testing.createMethodMock = function(scope, functionName, opt_strictness) {
return goog.testing.MethodMock(scope, functionName, opt_strictness);
};
/**
* Convenience method for creating a mock for a constructor. Copies class
* members to the mock.
*
* <p>When mocking a constructor to return a mocked instance, remember to create
* the instance mock before mocking the constructor. If you mock the constructor
* first, then the mock framework will be unable to examine the prototype chain
* when creating the mock instance.
* @param {Object} scope The scope of the constructor to be mocked out.
* @param {string} constructorName The name of the constructor we're going to
* mock.
* @param {number=} opt_strictness One of goog.testing.Mock.LOOSE or
* goog.testing.Mock.STRICT. The default is STRICT.
* @return {!goog.testing.MockInterface} The mocked constructor.
*/
goog.testing.createConstructorMock = function(scope, constructorName,
opt_strictness) {
var realConstructor = scope[constructorName];
var constructorMock = goog.testing.MethodMock(scope, constructorName,
opt_strictness);
// Copy class members from the real constructor to the mock. Do not copy
// the closure superClass_ property (see goog.inherits), the built-in
// prototype property, or properties added to Function.prototype
// (see goog.MODIFY_FUNCTION_PROTOTYPES in closure/base.js).
for (var property in realConstructor) {
if (property != 'superClass_' &&
property != 'prototype' &&
realConstructor.hasOwnProperty(property)) {
constructorMock[property] = realConstructor[property];
}
}
return constructorMock;
};
/**
* Convenience method for creating a mocks for a global / top-level function.
* @param {string} functionName The name of the function we're going to mock.
* @param {number=} opt_strictness One of goog.testing.Mock.LOOSE or
* goog.testing.Mock.STRICT. The default is STRICT.
* @return {!goog.testing.MockInterface} The mocked global function.
*/
goog.testing.createGlobalFunctionMock = function(functionName, opt_strictness) {
return goog.testing.GlobalFunctionMock(functionName, opt_strictness);
};
@@ -0,0 +1,27 @@
<!DOCTYPE html>
<!--
Test mocking global / top-level functions
-->
<html>
<!--
Copyright 2008 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>
Global Mock Unit Test
</title>
<script src="../base.js">
</script>
<script>
goog.require('goog.testing.FunctionMockTest');
</script>
</head>
<body>
</body>
</html>
@@ -0,0 +1,503 @@
// 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.
goog.provide('goog.testing.FunctionMockTest');
goog.setTestOnly('goog.testing.FunctionMockTest');
goog.require('goog.array');
goog.require('goog.string');
goog.require('goog.testing');
goog.require('goog.testing.FunctionMock');
goog.require('goog.testing.Mock');
goog.require('goog.testing.StrictMock');
goog.require('goog.testing.asserts');
goog.require('goog.testing.jsunit');
goog.require('goog.testing.mockmatchers');
// Global scope so we can tear it down safely
var mockGlobal;
function tearDown() {
if (mockGlobal) {
mockGlobal.$tearDown();
}
}
//----- Tests for goog.testing.FunctionMock
function testMockFunctionCallOrdering() {
var doOneTest = function(mockFunction, success, expected_args, actual_args) {
goog.array.forEach(expected_args, function(arg) { mockFunction(arg); });
mockFunction.$replay();
var callFunction = function() {
goog.array.forEach(actual_args, function(arg) { mockFunction(arg); });
mockFunction.$verify();
};
if (success) {
callFunction();
} else {
assertThrows(callFunction);
}
};
var doTest = function(strict_ok, loose_ok, expected_args, actual_args) {
doOneTest(goog.testing.createFunctionMock(), strict_ok,
expected_args, actual_args);
doOneTest(goog.testing.createFunctionMock('name'), strict_ok,
expected_args, actual_args);
doOneTest(goog.testing.createFunctionMock('name', goog.testing.Mock.STRICT),
strict_ok, expected_args, actual_args);
doOneTest(goog.testing.createFunctionMock('name', goog.testing.Mock.LOOSE),
loose_ok, expected_args, actual_args);
};
doTest(true, true, [1, 2], [1, 2]);
doTest(false, true, [1, 2], [2, 1]);
doTest(false, false, [1, 2], [2, 2]);
doTest(false, false, [1, 2], [1]);
doTest(false, false, [1, 2], [1, 1]);
doTest(false, false, [1, 2], [1]);
}
function testMocksFunctionWithNoArgs() {
var mockFoo = goog.testing.createFunctionMock();
mockFoo();
mockFoo.$replay();
mockFoo();
mockFoo.$verify();
}
function testMocksFunctionWithOneArg() {
var mockFoo = goog.testing.createFunctionMock();
mockFoo('x');
mockFoo.$replay();
mockFoo('x');
mockFoo.$verify();
}
function testMocksFunctionWithMultipleArgs() {
var mockFoo = goog.testing.createFunctionMock();
mockFoo('x', 'y');
mockFoo.$replay();
mockFoo('x', 'y');
mockFoo.$verify();
}
function testFailsIfCalledWithIncorrectArgs() {
var mockFoo = goog.testing.createFunctionMock();
mockFoo();
mockFoo.$replay();
assertThrows(function() {mockFoo('x');});
mockFoo.$reset();
mockFoo('x');
mockFoo.$replay();
assertThrows(function() {mockFoo();});
mockFoo.$reset();
mockFoo('x');
mockFoo.$replay();
assertThrows(function() {mockFoo('x', 'y');});
mockFoo.$reset();
mockFoo('x', 'y');
mockFoo.$replay();
assertThrows(function() {mockFoo('x');});
mockFoo.$reset();
mockFoo('correct');
mockFoo.$replay();
assertThrows(function() {mockFoo('wrong');});
mockFoo.$reset();
mockFoo('correct', 'args');
mockFoo.$replay();
assertThrows(function() {mockFoo('wrong', 'args');});
mockFoo.$reset();
}
function testMocksFunctionWithReturnValue() {
var mockFoo = goog.testing.createFunctionMock();
mockFoo().$returns('bar');
mockFoo.$replay();
assertEquals('bar', mockFoo());
mockFoo.$verify();
}
function testFunctionMockWorksWhenPassedAsACallback() {
var invoker = {
register: function(callback) {
this.callback = callback;
},
invoke: function(args) {
return this.callback(args);
}
};
var mockFunction = goog.testing.createFunctionMock();
mockFunction('bar').$returns('baz');
mockFunction.$replay();
invoker.register(mockFunction);
assertEquals('baz', invoker.invoke('bar'));
mockFunction.$verify();
}
function testFunctionMockQuacksLikeAStrictMock() {
var mockFunction = goog.testing.createFunctionMock();
assertQuacksLike(mockFunction, goog.testing.StrictMock);
}
//----- Global functions for goog.testing.GlobalFunctionMock to mock
function globalFoo() {
return 'I am Spartacus!';
}
function globalBar(who, what) {
return [who, 'is', what].join(' ');
}
//----- Tests for goog.testing.createGlobalFunctionMock
function testMocksGlobalFunctionWithNoArgs() {
mockGlobal = goog.testing.createGlobalFunctionMock('globalFoo');
mockGlobal().$returns('No, I am Spartacus!');
mockGlobal.$replay();
assertEquals('No, I am Spartacus!', globalFoo());
mockGlobal.$verify();
}
function testMocksGlobalFunctionUsingGlobalName() {
goog.testing.createGlobalFunctionMock('globalFoo');
globalFoo().$returns('No, I am Spartacus!');
globalFoo.$replay();
assertEquals('No, I am Spartacus!', globalFoo());
globalFoo.$verify();
globalFoo.$tearDown();
}
function testMocksGlobalFunctionWithArgs() {
var mockReturnValue = 'Noam is Chomsky!';
mockGlobal = goog.testing.createGlobalFunctionMock('globalBar');
mockGlobal('Noam', 'Spartacus').$returns(mockReturnValue);
mockGlobal.$replay();
assertEquals(mockReturnValue, globalBar('Noam', 'Spartacus'));
mockGlobal.$verify();
}
function testGlobalFunctionMockFailsWithIncorrectArgs() {
mockGlobal = goog.testing.createGlobalFunctionMock('globalBar');
mockGlobal('a', 'b');
mockGlobal.$replay();
assertThrows('Mock should have failed because of incorrect arguments',
function() {globalBar('b', 'a')});
}
function testGlobalFunctionMockQuacksLikeAFunctionMock() {
mockGlobal = goog.testing.createGlobalFunctionMock('globalFoo');
assertQuacksLike(mockGlobal, goog.testing.FunctionMock);
}
function testMockedFunctionsAvailableInGlobalAndGoogGlobalAndWindowScope() {
mockGlobal = goog.testing.createGlobalFunctionMock('globalFoo');
// we expect this call 3 times through global, goog.global and window scope
mockGlobal().$times(3);
mockGlobal.$replay();
goog.global.globalFoo();
window.globalFoo();
globalFoo();
mockGlobal.$verify();
}
function testTearDownRestoresOriginalGlobalFunction() {
mockGlobal = goog.testing.createGlobalFunctionMock('globalFoo');
mockGlobal().$returns('No, I am Spartacus!');
mockGlobal.$replay();
assertEquals('No, I am Spartacus!', globalFoo());
mockGlobal.$tearDown();
assertEquals('I am Spartacus!', globalFoo());
mockGlobal.$verify();
}
function testTearDownHandlesMultipleMocking() {
var mock1 = goog.testing.createGlobalFunctionMock('globalFoo');
var mock2 = goog.testing.createGlobalFunctionMock('globalFoo');
var mock3 = goog.testing.createGlobalFunctionMock('globalFoo');
mock1().$returns('No, I am Spartacus 1!');
mock2().$returns('No, I am Spartacus 2!');
mock3().$returns('No, I am Spartacus 3!');
mock1.$replay();
mock2.$replay();
mock3.$replay();
assertEquals('No, I am Spartacus 3!', globalFoo());
mock3.$tearDown();
assertEquals('No, I am Spartacus 2!', globalFoo());
mock2.$tearDown();
assertEquals('No, I am Spartacus 1!', globalFoo());
mock1.$tearDown();
assertEquals('I am Spartacus!', globalFoo());
}
function testGlobalFunctionMockCallOrdering() {
var mock = goog.testing.createGlobalFunctionMock('globalFoo');
mock(1);
mock(2);
mock.$replay();
assertThrows(function() {globalFoo(2);});
mock.$tearDown();
mock = goog.testing.createGlobalFunctionMock('globalFoo',
goog.testing.Mock.STRICT);
mock(1);
mock(2);
mock.$replay();
globalFoo(1);
globalFoo(2);
mock.$verify();
mock.$tearDown();
mock = goog.testing.createGlobalFunctionMock('globalFoo',
goog.testing.Mock.STRICT);
mock(1);
mock(2);
mock.$replay();
assertThrows(function() {globalFoo(2);});
mock.$tearDown();
mock = goog.testing.createGlobalFunctionMock('globalFoo',
goog.testing.Mock.LOOSE);
mock(1);
mock(2);
mock.$replay();
globalFoo(2);
globalFoo(1);
mock.$verify();
mock.$tearDown();
}
//----- Functions for goog.testing.MethodMock to mock
var mynamespace = {};
mynamespace.myMethod = function() {
return 'I should be mocked.';
};
function testMocksMethod() {
mockMethod = goog.testing.createMethodMock(mynamespace, 'myMethod');
mockMethod().$returns('I have been mocked!');
mockMethod.$replay();
assertEquals('I have been mocked!', mockMethod());
mockMethod.$verify();
}
function testMocksMethodInNamespace() {
goog.testing.createMethodMock(mynamespace, 'myMethod');
mynamespace.myMethod().$returns('I have been mocked!');
mynamespace.myMethod.$replay();
assertEquals('I have been mocked!', mynamespace.myMethod());
mynamespace.myMethod.$verify();
mynamespace.myMethod.$tearDown();
}
function testMethodMockCanOnlyMockExistingMethods() {
assertThrows(function() {
goog.testing.createMethodMock(mynamespace, 'doesNotExist');
});
}
function testMethodMockCallOrdering() {
goog.testing.createMethodMock(mynamespace, 'myMethod');
mynamespace.myMethod(1);
mynamespace.myMethod(2);
mynamespace.myMethod.$replay();
assertThrows(function() {mynamespace.myMethod(2);});
mynamespace.myMethod.$tearDown();
goog.testing.createMethodMock(mynamespace, 'myMethod',
goog.testing.Mock.STRICT);
mynamespace.myMethod(1);
mynamespace.myMethod(2);
mynamespace.myMethod.$replay();
mynamespace.myMethod(1);
mynamespace.myMethod(2);
mynamespace.myMethod.$verify();
mynamespace.myMethod.$tearDown();
goog.testing.createMethodMock(mynamespace, 'myMethod',
goog.testing.Mock.STRICT);
mynamespace.myMethod(1);
mynamespace.myMethod(2);
mynamespace.myMethod.$replay();
assertThrows(function() {mynamespace.myMethod(2);});
mynamespace.myMethod.$tearDown();
goog.testing.createMethodMock(mynamespace, 'myMethod',
goog.testing.Mock.LOOSE);
mynamespace.myMethod(1);
mynamespace.myMethod(2);
mynamespace.myMethod.$replay();
mynamespace.myMethod(2);
mynamespace.myMethod(1);
mynamespace.myMethod.$verify();
mynamespace.myMethod.$tearDown();
}
//----- Functions for goog.testing.createConstructorMock to mock
var constructornamespace = {};
constructornamespace.MyConstructor = function() {
};
constructornamespace.MyConstructor.prototype.myMethod = function() {
return 'I should be mocked.';
};
constructornamespace.MyConstructorWithArgument = function(argument) {
this.argument_ = argument;
};
constructornamespace.MyConstructorWithArgument.prototype.myMethod = function() {
return this.argument_;
};
constructornamespace.MyConstructorWithClassMembers = function() {
};
constructornamespace.MyConstructorWithClassMembers.CONSTANT = 42;
constructornamespace.MyConstructorWithClassMembers.classMethod = function() {
return 'class method return value';
};
function testConstructorMock() {
var mockObject =
new goog.testing.StrictMock(constructornamespace.MyConstructor);
var mockConstructor = goog.testing.createConstructorMock(
constructornamespace, 'MyConstructor');
mockConstructor().$returns(mockObject);
mockObject.myMethod().$returns('I have been mocked!');
mockConstructor.$replay();
mockObject.$replay();
assertEquals('I have been mocked!',
new constructornamespace.MyConstructor().myMethod());
mockConstructor.$verify();
mockObject.$verify();
mockConstructor.$tearDown();
}
function testConstructorMockWithArgument() {
var mockObject = new goog.testing.StrictMock(
constructornamespace.MyConstructorWithArgument);
var mockConstructor = goog.testing.createConstructorMock(
constructornamespace, 'MyConstructorWithArgument');
mockConstructor(goog.testing.mockmatchers.isString).$returns(mockObject);
mockObject.myMethod().$returns('I have been mocked!');
mockConstructor.$replay();
mockObject.$replay();
assertEquals('I have been mocked!',
new constructornamespace.MyConstructorWithArgument('I should be mocked.')
.myMethod());
mockConstructor.$verify();
mockObject.$verify();
mockConstructor.$tearDown();
}
/**
* Test that class members are copied to the mock constructor.
*/
function testConstructorMockWithClassMembers() {
var mockConstructor = goog.testing.createConstructorMock(
constructornamespace, 'MyConstructorWithClassMembers');
assertEquals(42, constructornamespace.MyConstructorWithClassMembers.CONSTANT);
assertEquals('class method return value',
constructornamespace.MyConstructorWithClassMembers.classMethod());
mockConstructor.$tearDown();
}
function testConstructorMockCallOrdering() {
var instance = {};
goog.testing.createConstructorMock(constructornamespace,
'MyConstructorWithArgument');
constructornamespace.MyConstructorWithArgument(1).$returns(instance);
constructornamespace.MyConstructorWithArgument(2).$returns(instance);
constructornamespace.MyConstructorWithArgument.$replay();
assertThrows(
function() {new constructornamespace.MyConstructorWithArgument(2);});
constructornamespace.MyConstructorWithArgument.$tearDown();
goog.testing.createConstructorMock(constructornamespace,
'MyConstructorWithArgument',
goog.testing.Mock.STRICT);
constructornamespace.MyConstructorWithArgument(1).$returns(instance);
constructornamespace.MyConstructorWithArgument(2).$returns(instance);
constructornamespace.MyConstructorWithArgument.$replay();
new constructornamespace.MyConstructorWithArgument(1);
new constructornamespace.MyConstructorWithArgument(2);
constructornamespace.MyConstructorWithArgument.$verify();
constructornamespace.MyConstructorWithArgument.$tearDown();
goog.testing.createConstructorMock(constructornamespace,
'MyConstructorWithArgument',
goog.testing.Mock.STRICT);
constructornamespace.MyConstructorWithArgument(1).$returns(instance);
constructornamespace.MyConstructorWithArgument(2).$returns(instance);
constructornamespace.MyConstructorWithArgument.$replay();
assertThrows(
function() {new constructornamespace.MyConstructorWithArgument(2);});
constructornamespace.MyConstructorWithArgument.$tearDown();
goog.testing.createConstructorMock(constructornamespace,
'MyConstructorWithArgument',
goog.testing.Mock.LOOSE);
constructornamespace.MyConstructorWithArgument(1).$returns(instance);
constructornamespace.MyConstructorWithArgument(2).$returns(instance);
constructornamespace.MyConstructorWithArgument.$replay();
new constructornamespace.MyConstructorWithArgument(2);
new constructornamespace.MyConstructorWithArgument(1);
constructornamespace.MyConstructorWithArgument.$verify();
constructornamespace.MyConstructorWithArgument.$tearDown();
}
//----- Helper assertions
function assertQuacksLike(obj, target) {
for (meth in target.prototype) {
if (!goog.string.endsWith(meth, '_')) {
assertNotUndefined('Should have implemented ' + meth + '()', obj[meth]);
}
}
}
@@ -0,0 +1,64 @@
// 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 Testing utilities for DOM related tests.
*
* @author robbyw@google.com (Robby Walker)
*/
goog.provide('goog.testing.graphics');
goog.require('goog.graphics.Path');
goog.require('goog.testing.asserts');
/**
* Array mapping numeric segment constant to a descriptive character.
* @type {Array<string>}
* @private
*/
goog.testing.graphics.SEGMENT_NAMES_ = function() {
var arr = [];
arr[goog.graphics.Path.Segment.MOVETO] = 'M';
arr[goog.graphics.Path.Segment.LINETO] = 'L';
arr[goog.graphics.Path.Segment.CURVETO] = 'C';
arr[goog.graphics.Path.Segment.ARCTO] = 'A';
arr[goog.graphics.Path.Segment.CLOSE] = 'X';
return arr;
}();
/**
* Test if the given path matches the expected array of commands and parameters.
* @param {Array<string|number>} expected The expected array of commands and
* parameters.
* @param {goog.graphics.Path} path The path to test against.
*/
goog.testing.graphics.assertPathEquals = function(expected, path) {
var actual = [];
path.forEachSegment(function(seg, args) {
actual.push(goog.testing.graphics.SEGMENT_NAMES_[seg]);
Array.prototype.push.apply(actual, args);
});
assertEquals(expected.length, actual.length);
for (var i = 0; i < expected.length; i++) {
if (goog.isNumber(expected[i])) {
assertTrue(goog.isNumber(actual[i]));
assertRoughlyEquals(expected[i], actual[i], 0.01);
} else {
assertEquals(expected[i], actual[i]);
}
}
};
@@ -0,0 +1,77 @@
// Copyright 2013 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 Assert functions that account for locale data changes.
*
* The locale data gets updated from CLDR (http://cldr.unicode.org/),
* and CLDR gets an update about twice per year.
* So the locale data are expected to change.
* This can make unit tests quite fragile:
* assertEquals("Dec 31, 2013, 1:23pm", format);
* Now imagine that the decision is made to add a dot after abbreviations,
* and a comma between date and time.
* The previous assert will fail, because the string is now
* "Dec. 31 2013, 1:23pm"
*
* One option is to not unit test the results of the formatters client side,
* and just trust that CLDR and closure/i18n takes care of that.
* The other option is to be a more flexible when testing.
* This is the role of assertI18nEquals, to centralize all the small
* differences between hard-coded values in unit tests and the current result.
* It allows some decupling, so that the closure/i18n can be updated without
* breaking all the clients using it.
* For the example above, this will succeed:
* assertI18nEquals("Dec 31, 2013, 1:23pm", "Dec. 31, 2013 1:23pm");
* It does this by white-listing, no "guessing" involved.
*
* But I would say that the best practice is the first option: trust the
* library, stop unit-testing it.
*/
goog.provide('goog.testing.i18n.asserts');
goog.setTestOnly('goog.testing.i18n.asserts');
goog.require('goog.testing.jsunit');
/**
* A map of known tests where locale data changed, but the old values are
* still tested for by various clients.
* @const {!Object<string, string>}
* @private
*/
goog.testing.i18n.asserts.EXPECTED_VALUE_MAP_ = {
// Data to test the assert itself, old string as key, new string as value
};
/**
* Asserts that the two values are "almost equal" from i18n perspective
* (based on a manually maintained and validated whitelist).
* @param {string} expected The expected value.
* @param {string} actual The actual value.
*/
goog.testing.i18n.asserts.assertI18nEquals = function(expected, actual) {
if (expected == actual) {
return;
}
var newExpected = goog.testing.i18n.asserts.EXPECTED_VALUE_MAP_[expected];
if (newExpected == actual) {
return;
}
assertEquals(expected, actual);
};
@@ -0,0 +1,19 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2013 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Closure Unit Tests - goog.testing.i18n.asserts</title>
<script src="../../base.js"></script>
</head>
<body>
<script>
goog.require('goog.testing.i18n.assertsTest');
</script>
</body>
</html>
@@ -0,0 +1,67 @@
// Copyright 2013 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.testing.i18n.asserts.
*/
goog.provide('goog.testing.i18n.assertsTest');
goog.setTestOnly('goog.testing.i18n.assertsTest');
goog.require('goog.testing.ExpectedFailures');
goog.require('goog.testing.i18n.asserts');
// Add this mapping for testing only
goog.testing.i18n.asserts.EXPECTED_VALUE_MAP_['mappedValue'] = 'newValue';
var expectedFailures = new goog.testing.ExpectedFailures();
function tearDown() {
expectedFailures.handleTearDown();
}
function testEdgeCases() {
// Pass
goog.testing.i18n.asserts.assertI18nEquals(null, null);
goog.testing.i18n.asserts.assertI18nEquals('', '');
// Fail
expectedFailures.expectFailureFor(true);
try {
goog.testing.i18n.asserts.assertI18nEquals(null, '');
goog.testing.i18n.asserts.assertI18nEquals(null, 'test');
goog.testing.i18n.asserts.assertI18nEquals('', null);
goog.testing.i18n.asserts.assertI18nEquals('', 'test');
goog.testing.i18n.asserts.assertI18nEquals('test', null);
goog.testing.i18n.asserts.assertI18nEquals('test', '');
} catch (e) {
expectedFailures.handleException(e);
}
}
function testMappingWorks() {
// Real equality
goog.testing.i18n.asserts.assertI18nEquals('test', 'test');
// i18n mapped equality
goog.testing.i18n.asserts.assertI18nEquals('mappedValue', 'newValue');
// Negative testing
expectedFailures.expectFailureFor(true);
try {
goog.testing.i18n.asserts.assertI18nEquals('unmappedValue', 'newValue');
} catch (e) {
expectedFailures.handleException(e);
}
}
@@ -0,0 +1,157 @@
// Copyright 2007 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 Utilities for working with JsUnit. Writes out the JsUnit file
* that needs to be included in every unit test.
*
* Testing code should not have dependencies outside of goog.testing so as to
* reduce the chance of masking missing dependencies.
*
*/
goog.provide('goog.testing.jsunit');
goog.require('goog.testing.TestCase');
goog.require('goog.testing.TestRunner');
/**
* Base path for JsUnit app files, relative to Closure's base path.
* @type {string}
*/
goog.testing.jsunit.BASE_PATH =
'../../third_party/java/jsunit/core/app/';
/**
* Filename for the core JS Unit script.
* @type {string}
*/
goog.testing.jsunit.CORE_SCRIPT =
goog.testing.jsunit.BASE_PATH + 'jsUnitCore.js';
/**
* @define {boolean} If this code is being parsed by JsTestC, we let it disable
* the onload handler to avoid running the test in JsTestC.
*/
goog.define('goog.testing.jsunit.AUTO_RUN_ONLOAD', true);
/**
* @define {number} Sets a delay in milliseconds after the window onload event
* and running the tests. Used to prevent interference with Selenium and give
* tests with asynchronous operations time to finish loading.
*/
goog.define('goog.testing.jsunit.AUTO_RUN_DELAY_IN_MS', 500);
(function() {
// Increases the maximum number of stack frames in Google Chrome from the
// default 10 to 50 to get more useful stack traces.
Error.stackTraceLimit = 50;
// Store a reference to the window's timeout so that it can't be overridden
// by tests.
/** @type {!Function} */
var realTimeout = window.setTimeout;
// Check for JsUnit's test runner (need to check for >2.2 and <=2.2)
if (top['JsUnitTestManager'] || top['jsUnitTestManager']) {
// Running inside JsUnit so add support code.
var path = goog.basePath + goog.testing.jsunit.CORE_SCRIPT;
document.write('<script type="text/javascript" src="' +
path + '"></' + 'script>');
} else {
// Create a test runner.
var tr = new goog.testing.TestRunner();
// Export it so that it can be queried by Selenium and tests that use a
// compiled test runner.
goog.exportSymbol('G_testRunner', tr);
goog.exportSymbol('G_testRunner.initialize', tr.initialize);
goog.exportSymbol('G_testRunner.isInitialized', tr.isInitialized);
goog.exportSymbol('G_testRunner.isFinished', tr.isFinished);
goog.exportSymbol('G_testRunner.isSuccess', tr.isSuccess);
goog.exportSymbol('G_testRunner.getReport', tr.getReport);
goog.exportSymbol('G_testRunner.getRunTime', tr.getRunTime);
goog.exportSymbol('G_testRunner.getNumFilesLoaded', tr.getNumFilesLoaded);
goog.exportSymbol('G_testRunner.setStrict', tr.setStrict);
goog.exportSymbol('G_testRunner.logTestFailure', tr.logTestFailure);
goog.exportSymbol('G_testRunner.getTestResults', tr.getTestResults);
// Export debug as a global function for JSUnit compatibility. This just
// calls log on the current test case.
if (!goog.global['debug']) {
goog.exportSymbol('debug', goog.bind(tr.log, tr));
}
// If the application has defined a global error filter, set it now. This
// allows users who use a base test include to set the error filter before
// the testing code is loaded.
if (goog.global['G_errorFilter']) {
tr.setErrorFilter(goog.global['G_errorFilter']);
}
// Add an error handler to report errors that may occur during
// initialization of the page.
var onerror = window.onerror;
window.onerror = function(error, url, line) {
// Call any existing onerror handlers.
if (onerror) {
onerror(error, url, line);
}
if (typeof error == 'object') {
// Webkit started passing an event object as the only argument to
// window.onerror. It doesn't contain an error message, url or line
// number. We therefore log as much info as we can.
if (error.target && error.target.tagName == 'SCRIPT') {
tr.logError('UNKNOWN ERROR: Script ' + error.target.src);
} else {
tr.logError('UNKNOWN ERROR: No error information available.');
}
} else {
tr.logError('JS ERROR: ' + error + '\nURL: ' + url + '\nLine: ' + line);
}
};
// Create an onload handler, if the test runner hasn't been initialized then
// no test has been registered with the test runner by the test file. We
// then create a new test case and auto discover any tests in the global
// scope. If this code is being parsed by JsTestC, we let it disable the
// onload handler to avoid running the test in JsTestC.
if (goog.testing.jsunit.AUTO_RUN_ONLOAD) {
var onload = window.onload;
window.onload = function(e) {
// Call any existing onload handlers.
if (onload) {
onload(e);
}
// Wait so that we don't interfere with WebDriver.
realTimeout(function() {
if (!tr.initialized) {
var test = new goog.testing.TestCase(document.title);
test.autoDiscoverTests();
tr.initialize(test);
}
tr.execute();
}, goog.testing.jsunit.AUTO_RUN_DELAY_IN_MS);
window.onload = null;
};
}
}
})();
@@ -0,0 +1,242 @@
// 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 This file defines a loose mock implementation.
*/
goog.provide('goog.testing.LooseExpectationCollection');
goog.provide('goog.testing.LooseMock');
goog.require('goog.array');
goog.require('goog.structs.Map');
goog.require('goog.testing.Mock');
/**
* This class is an ordered collection of expectations for one method. Since
* the loose mock does most of its verification at the time of $verify, this
* class is necessary to manage the return/throw behavior when the mock is
* being called.
* @constructor
* @final
*/
goog.testing.LooseExpectationCollection = function() {
/**
* The list of expectations. All of these should have the same name.
* @type {Array<goog.testing.MockExpectation>}
* @private
*/
this.expectations_ = [];
};
/**
* Adds an expectation to this collection.
* @param {goog.testing.MockExpectation} expectation The expectation to add.
*/
goog.testing.LooseExpectationCollection.prototype.addExpectation =
function(expectation) {
this.expectations_.push(expectation);
};
/**
* Gets the list of expectations in this collection.
* @return {Array<goog.testing.MockExpectation>} The array of expectations.
*/
goog.testing.LooseExpectationCollection.prototype.getExpectations = function() {
return this.expectations_;
};
/**
* This is a mock that does not care about the order of method calls. As a
* result, it won't throw exceptions until verify() is called. The only
* exception is that if a method is called that has no expectations, then an
* exception will be thrown.
* @param {Object|Function} objectToMock The object that should be mocked, or
* the constructor of an object to mock.
* @param {boolean=} opt_ignoreUnexpectedCalls Whether to ignore unexpected
* calls.
* @param {boolean=} opt_mockStaticMethods An optional argument denoting that
* a mock should be constructed from the static functions of a class.
* @param {boolean=} opt_createProxy An optional argument denoting that
* a proxy for the target mock should be created.
* @constructor
* @extends {goog.testing.Mock}
*/
goog.testing.LooseMock = function(objectToMock, opt_ignoreUnexpectedCalls,
opt_mockStaticMethods, opt_createProxy) {
goog.testing.Mock.call(this, objectToMock, opt_mockStaticMethods,
opt_createProxy);
/**
* A map of method names to a LooseExpectationCollection for that method.
* @type {goog.structs.Map}
* @private
*/
this.$expectations_ = new goog.structs.Map();
/**
* The calls that have been made; we cache them to verify at the end. Each
* element is an array where the first element is the name, and the second
* element is the arguments.
* @type {Array<Array<*>>}
* @private
*/
this.$calls_ = [];
/**
* Whether to ignore unexpected calls.
* @type {boolean}
* @private
*/
this.$ignoreUnexpectedCalls_ = !!opt_ignoreUnexpectedCalls;
};
goog.inherits(goog.testing.LooseMock, goog.testing.Mock);
/**
* A setter for the ignoreUnexpectedCalls field.
* @param {boolean} ignoreUnexpectedCalls Whether to ignore unexpected calls.
* @return {!goog.testing.LooseMock} This mock object.
*/
goog.testing.LooseMock.prototype.$setIgnoreUnexpectedCalls = function(
ignoreUnexpectedCalls) {
this.$ignoreUnexpectedCalls_ = ignoreUnexpectedCalls;
return this;
};
/** @override */
goog.testing.LooseMock.prototype.$recordExpectation = function() {
if (!this.$expectations_.containsKey(this.$pendingExpectation.name)) {
this.$expectations_.set(this.$pendingExpectation.name,
new goog.testing.LooseExpectationCollection());
}
var collection = this.$expectations_.get(this.$pendingExpectation.name);
collection.addExpectation(this.$pendingExpectation);
};
/** @override */
goog.testing.LooseMock.prototype.$recordCall = function(name, args) {
if (!this.$expectations_.containsKey(name)) {
if (this.$ignoreUnexpectedCalls_) {
return;
}
this.$throwCallException(name, args);
}
// Start from the beginning of the expectations for this name,
// and iterate over them until we find an expectation that matches
// and also has calls remaining.
var collection = this.$expectations_.get(name);
var matchingExpectation = null;
var expectations = collection.getExpectations();
for (var i = 0; i < expectations.length; i++) {
var expectation = expectations[i];
if (this.$verifyCall(expectation, name, args)) {
matchingExpectation = expectation;
if (expectation.actualCalls < expectation.maxCalls) {
break;
} // else continue and see if we can find something that does match
}
}
if (matchingExpectation == null) {
this.$throwCallException(name, args, expectation);
}
matchingExpectation.actualCalls++;
if (matchingExpectation.actualCalls > matchingExpectation.maxCalls) {
this.$throwException('Too many calls to ' + matchingExpectation.name +
'\nExpected: ' + matchingExpectation.maxCalls + ' but was: ' +
matchingExpectation.actualCalls);
}
this.$calls_.push([name, args]);
return this.$do(matchingExpectation, args);
};
/** @override */
goog.testing.LooseMock.prototype.$reset = function() {
goog.testing.LooseMock.superClass_.$reset.call(this);
this.$expectations_ = new goog.structs.Map();
this.$calls_ = [];
};
/** @override */
goog.testing.LooseMock.prototype.$replay = function() {
goog.testing.LooseMock.superClass_.$replay.call(this);
// Verify that there are no expectations that can never be reached.
// This can't catch every situation, but it is a decent sanity check
// and it's similar to the behavior of EasyMock in java.
var collections = this.$expectations_.getValues();
for (var i = 0; i < collections.length; i++) {
var expectations = collections[i].getExpectations();
for (var j = 0; j < expectations.length; j++) {
var expectation = expectations[j];
// If this expectation can be called infinite times, then
// check if any subsequent expectation has the exact same
// argument list.
if (!isFinite(expectation.maxCalls)) {
for (var k = j + 1; k < expectations.length; k++) {
var laterExpectation = expectations[k];
if (laterExpectation.minCalls > 0 &&
goog.array.equals(expectation.argumentList,
laterExpectation.argumentList)) {
var name = expectation.name;
var argsString = this.$argumentsAsString(expectation.argumentList);
this.$throwException([
'Expected call to ', name, ' with arguments ', argsString,
' has an infinite max number of calls; can\'t expect an',
' identical call later with a positive min number of calls'
].join(''));
}
}
}
}
}
};
/** @override */
goog.testing.LooseMock.prototype.$verify = function() {
goog.testing.LooseMock.superClass_.$verify.call(this);
var collections = this.$expectations_.getValues();
for (var i = 0; i < collections.length; i++) {
var expectations = collections[i].getExpectations();
for (var j = 0; j < expectations.length; j++) {
var expectation = expectations[j];
if (expectation.actualCalls > expectation.maxCalls) {
this.$throwException('Too many calls to ' + expectation.name +
'\nExpected: ' + expectation.maxCalls + ' but was: ' +
expectation.actualCalls);
} else if (expectation.actualCalls < expectation.minCalls) {
this.$throwException('Not enough calls to ' + expectation.name +
'\nExpected: ' + expectation.minCalls + ' but was: ' +
expectation.actualCalls);
}
}
}
};
@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2008 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>
Closure Unit Tests - goog.testing.LooseMock
</title>
<script src="../base.js">
</script>
<script>
goog.require('goog.testing.LooseMockTest');
</script>
</head>
<body>
</body>
</html>
@@ -0,0 +1,342 @@
// 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.
goog.provide('goog.testing.LooseMockTest');
goog.setTestOnly('goog.testing.LooseMockTest');
goog.require('goog.testing.LooseMock');
goog.require('goog.testing.PropertyReplacer');
goog.require('goog.testing.jsunit');
goog.require('goog.testing.mockmatchers');
// The object that we will be mocking
var RealObject = function() {
};
RealObject.prototype.a = function() {
fail('real object should never be called');
};
RealObject.prototype.b = function() {
fail('real object should never be called');
};
var mock;
var stubs;
function setUp() {
var obj = new RealObject();
mock = new goog.testing.LooseMock(obj);
stubs = new goog.testing.PropertyReplacer();
}
function tearDown() {
stubs.reset();
}
/*
* Calling this method evades the logTestFailure in
* goog.testing.Mock.prototype.$recordAndThrow, so it doesn't look like the
* test failed.
*/
function silenceFailureLogging() {
if (goog.global['G_testRunner']) {
stubs.set(goog.global['G_testRunner'],
'logTestFailure', goog.nullFunction);
}
}
function unsilenceFailureLogging() {
stubs.reset();
}
/**
* Version of assertThrows that doesn't log the exception generated by the
* mocks under test.
* TODO: would be nice to check that a particular substring is in the thrown
* message so we know it's not something dumb like a syntax error
*/
function assertThrowsQuiet(var_args) {
silenceFailureLogging();
assertThrows.apply(null, arguments);
unsilenceFailureLogging();
}
// Most of the basic functionality is tested in strictmock_test; these tests
// cover the cases where loose mocks are different from strict mocks
function testSimpleExpectations() {
mock.a(5);
mock.b();
mock.$replay();
mock.a(5);
mock.b();
mock.$verify();
mock.$reset();
mock.a();
mock.b();
mock.$replay();
mock.b();
mock.a();
mock.$verify();
mock.$reset();
mock.a(5).$times(2);
mock.a(5);
mock.a(2);
mock.$replay();
mock.a(5);
mock.a(5);
mock.a(5);
mock.a(2);
mock.$verify();
}
function testMultipleExpectations() {
mock.a().$returns(1);
mock.a().$returns(2);
mock.$replay();
assertEquals(1, mock.a());
assertEquals(2, mock.a());
mock.$verify();
}
function testMultipleExpectationArgs() {
mock.a('asdf').$anyTimes();
mock.a('qwer').$anyTimes();
mock.b().$times(3);
mock.$replay();
mock.a('asdf');
mock.b();
mock.a('asdf');
mock.a('qwer');
mock.b();
mock.a('qwer');
mock.b();
mock.$verify();
mock.$reset();
mock.a('asdf').$anyTimes();
mock.a('qwer').$anyTimes();
mock.$replay();
mock.a('asdf');
mock.a('qwer');
goog.bind(mock.a, mock, 'asdf');
goog.bind(mock.$verify, mock);
}
function testSameMethodOutOfOrder() {
mock.a('foo').$returns(1);
mock.a('bar').$returns(2);
mock.$replay();
assertEquals(2, mock.a('bar'));
assertEquals(1, mock.a('foo'));
}
function testSameMethodDifferentReturnValues() {
mock.a('foo').$returns(1).$times(2);
mock.a('foo').$returns(3);
mock.a('bar').$returns(2);
mock.$replay();
assertEquals(1, mock.a('foo'));
assertEquals(2, mock.a('bar'));
assertEquals(1, mock.a('foo'));
assertEquals(3, mock.a('foo'));
assertThrowsQuiet(function() {
mock.a('foo');
mock.$verify();
});
}
function testSameMethodBrokenExpectations() {
// This is a weird corner case.
// No way to ever make this verify no matter what you call after replaying,
// because the second expectation of mock.a('foo') will be masked by
// the first expectation that can be called any number of times, and so we
// can never satisfy that second expectation.
mock.a('foo').$returns(1).$anyTimes();
mock.a('bar').$returns(2);
mock.a('foo').$returns(3);
// LooseMock can detect this case and fail on $replay.
assertThrowsQuiet(goog.bind(mock.$replay, mock));
mock.$reset();
// This is a variant of the corner case above, but it's harder to determine
// that the expectation to mock.a('bar') can never be satisfied. So we don't
// fail on $replay, but we do fail on $verify.
mock.a(goog.testing.mockmatchers.isString).$returns(1).$anyTimes();
mock.a('bar').$returns(2);
mock.$replay();
assertEquals(1, mock.a('foo'));
assertEquals(1, mock.a('bar'));
assertThrowsQuiet(goog.bind(mock.$verify, mock));
}
function testSameMethodMultipleAnyTimes() {
mock.a('foo').$returns(1).$anyTimes();
mock.a('foo').$returns(2).$anyTimes();
mock.$replay();
assertEquals(1, mock.a('foo'));
assertEquals(1, mock.a('foo'));
assertEquals(1, mock.a('foo'));
// Note we'll never return 2 but that's ok.
mock.$verify();
}
function testFailingFast() {
mock.a().$anyTimes();
mock.$replay();
mock.a();
mock.a();
assertThrowsQuiet(goog.bind(mock.b, mock));
mock.$reset();
// too many
mock.a();
mock.b();
mock.$replay();
mock.a();
mock.b();
var message;
silenceFailureLogging();
try {
mock.a();
} catch (e) {
message = e.message;
}
unsilenceFailureLogging();
assertTrue('No exception thrown on unexpected call', goog.isDef(message));
assertContains('Too many calls to a', message);
}
function testTimes() {
mock.a().$times(3);
mock.b().$times(2);
mock.$replay();
mock.a();
mock.b();
mock.b();
mock.a();
mock.a();
mock.$verify();
}
function testFailingSlow() {
// not enough
mock.a().$times(3);
mock.$replay();
mock.a();
mock.a();
assertThrowsQuiet(goog.bind(mock.$verify, mock));
mock.$reset();
// not enough, interleaved order
mock.a().$times(3);
mock.b().$times(3);
mock.$replay();
mock.a();
mock.b();
mock.a();
mock.b();
assertThrowsQuiet(goog.bind(mock.$verify, mock));
mock.$reset();
// bad args
mock.a('asdf').$anyTimes();
mock.$replay();
mock.a('asdf');
assertThrowsQuiet(goog.bind(mock.a, mock, 'qwert'));
assertThrowsQuiet(goog.bind(mock.$verify, mock));
}
function testArgsAndReturns() {
mock.a('asdf').$atLeastOnce().$returns(5);
mock.b('qwer').$times(2).$returns(3);
mock.$replay();
assertEquals(5, mock.a('asdf'));
assertEquals(3, mock.b('qwer'));
assertEquals(5, mock.a('asdf'));
assertEquals(5, mock.a('asdf'));
assertEquals(3, mock.b('qwer'));
mock.$verify();
}
function testThrows() {
mock.a().$throws('exception!');
mock.$replay();
assertThrowsQuiet(goog.bind(mock.a, mock));
mock.$verify();
}
function testDoes() {
mock.a(1, 2).$does(function(a, b) {return a + b;});
mock.$replay();
assertEquals('Mock should call the function', 3, mock.a(1, 2));
mock.$verify();
}
function testIgnoresExtraCalls() {
mock = new goog.testing.LooseMock(RealObject, true);
mock.a();
mock.$replay();
mock.a();
mock.b(); // doesn't throw
mock.$verify();
}
function testSkipAnyTimes() {
mock = new goog.testing.LooseMock(RealObject);
mock.a(1).$anyTimes();
mock.a(2).$anyTimes();
mock.a(3).$anyTimes();
mock.$replay();
mock.a(1);
mock.a(3);
mock.$verify();
}
function testErrorMessageForBadArgs() {
mock.a();
mock.$anyTimes();
mock.$replay();
var message;
silenceFailureLogging();
try {
mock.a('a');
} catch (e) {
message = e.message;
}
unsilenceFailureLogging();
assertTrue('No exception thrown on verify', goog.isDef(message));
assertContains('Bad arguments to a()', message);
}
@@ -0,0 +1,80 @@
// Copyright 2010 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 Mock MessageChannel implementation that can receive fake
* messages and test that the right messages are sent.
*
*/
goog.provide('goog.testing.messaging.MockMessageChannel');
goog.require('goog.messaging.AbstractChannel');
goog.require('goog.testing.asserts');
/**
* Class for unit-testing code that communicates over a MessageChannel.
* @param {goog.testing.MockControl} mockControl The mock control used to create
* the method mock for #send.
* @extends {goog.messaging.AbstractChannel}
* @constructor
* @final
*/
goog.testing.messaging.MockMessageChannel = function(mockControl) {
goog.testing.messaging.MockMessageChannel.base(this, 'constructor');
/**
* Whether the channel has been disposed.
* @type {boolean}
*/
this.disposed = false;
mockControl.createMethodMock(this, 'send');
};
goog.inherits(goog.testing.messaging.MockMessageChannel,
goog.messaging.AbstractChannel);
/**
* A mock send function. Actually an instance of
* {@link goog.testing.FunctionMock}.
* @param {string} serviceName The name of the remote service to run.
* @param {string|!Object} payload The payload to send to the remote page.
* @override
*/
goog.testing.messaging.MockMessageChannel.prototype.send = function(
serviceName, payload) {};
/**
* Sets a flag indicating that this is disposed.
* @override
*/
goog.testing.messaging.MockMessageChannel.prototype.dispose = function() {
this.disposed = true;
};
/**
* Mocks the receipt of a message. Passes the payload the appropriate service.
* @param {string} serviceName The service to run.
* @param {string|!Object} payload The argument to pass to the service.
*/
goog.testing.messaging.MockMessageChannel.prototype.receive = function(
serviceName, payload) {
this.deliver(serviceName, payload);
};
@@ -0,0 +1,102 @@
// Copyright 2010 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 A simple mock class for imitating HTML5 MessageEvents.
*
*/
goog.provide('goog.testing.messaging.MockMessageEvent');
goog.require('goog.events.BrowserEvent');
goog.require('goog.events.EventType');
goog.require('goog.testing.events.Event');
/**
* Creates a new fake MessageEvent.
*
* @param {*} data The data of the message.
* @param {string=} opt_origin The origin of the message, for server-sent and
* cross-document events.
* @param {string=} opt_lastEventId The last event ID, for server-sent events.
* @param {Window=} opt_source The proxy for the source window, for
* cross-document events.
* @param {Array<MessagePort>=} opt_ports The Array of ports sent with the
* message, for cross-document and channel events.
* @extends {goog.testing.events.Event}
* @constructor
* @final
*/
goog.testing.messaging.MockMessageEvent = function(
data, opt_origin, opt_lastEventId, opt_source, opt_ports) {
goog.testing.messaging.MockMessageEvent.base(
this, 'constructor', goog.events.EventType.MESSAGE);
/**
* The data of the message.
* @type {*}
*/
this.data = data;
/**
* The origin of the message, for server-sent and cross-document events.
* @type {?string}
*/
this.origin = opt_origin || null;
/**
* The last event ID, for server-sent events.
* @type {?string}
*/
this.lastEventId = opt_lastEventId || null;
/**
* The proxy for the source window, for cross-document events.
* @type {Window}
*/
this.source = opt_source || null;
/**
* The Array of ports sent with the message, for cross-document and channel
* events.
* @type {Array<!MessagePort>}
*/
this.ports = opt_ports || null;
};
goog.inherits(
goog.testing.messaging.MockMessageEvent, goog.testing.events.Event);
/**
* Wraps a new fake MessageEvent in a BrowserEvent, like how a real MessageEvent
* would be wrapped.
*
* @param {*} data The data of the message.
* @param {string=} opt_origin The origin of the message, for server-sent and
* cross-document events.
* @param {string=} opt_lastEventId The last event ID, for server-sent events.
* @param {Window=} opt_source The proxy for the source window, for
* cross-document events.
* @param {Array<MessagePort>=} opt_ports The Array of ports sent with the
* message, for cross-document and channel events.
* @return {!goog.events.BrowserEvent} The wrapping event.
*/
goog.testing.messaging.MockMessageEvent.wrap = function(
data, opt_origin, opt_lastEventId, opt_source, opt_ports) {
return new goog.events.BrowserEvent(
new goog.testing.messaging.MockMessageEvent(
data, opt_origin, opt_lastEventId, opt_source, opt_ports));
};
@@ -0,0 +1,86 @@
// Copyright 2011 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 A simple dummy class for representing message ports in tests.
*
*/
goog.provide('goog.testing.messaging.MockMessagePort');
goog.require('goog.events.EventTarget');
/**
* Class for unit-testing code that uses MessagePorts.
* @param {*} id An opaque identifier, used because message ports otherwise have
* no distinguishing characteristics.
* @param {goog.testing.MockControl} mockControl The mock control used to create
* the method mock for #postMessage.
* @constructor
* @extends {goog.events.EventTarget}
* @final
*/
goog.testing.messaging.MockMessagePort = function(id, mockControl) {
goog.testing.messaging.MockMessagePort.base(this, 'constructor');
/**
* An opaque identifier, used because message ports otherwise have no
* distinguishing characteristics.
* @type {*}
*/
this.id = id;
/**
* Whether or not the port has been started.
* @type {boolean}
*/
this.started = false;
/**
* Whether or not the port has been closed.
* @type {boolean}
*/
this.closed = false;
mockControl.createMethodMock(this, 'postMessage');
};
goog.inherits(goog.testing.messaging.MockMessagePort, goog.events.EventTarget);
/**
* A mock postMessage funciton. Actually an instance of
* {@link goog.testing.FunctionMock}.
* @param {*} message The message to send.
* @param {Array<MessagePort>=} opt_ports Ports to send with the message.
*/
goog.testing.messaging.MockMessagePort.prototype.postMessage = function(
message, opt_ports) {};
/**
* Starts the port.
*/
goog.testing.messaging.MockMessagePort.prototype.start = function() {
this.started = true;
};
/**
* Closes the port.
*/
goog.testing.messaging.MockMessagePort.prototype.close = function() {
this.closed = true;
};
@@ -0,0 +1,66 @@
// Copyright 2011 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 A fake PortNetwork implementation that simply produces
* MockMessageChannels for all ports.
*
*/
goog.provide('goog.testing.messaging.MockPortNetwork');
goog.require('goog.messaging.PortNetwork'); // interface
goog.require('goog.testing.messaging.MockMessageChannel');
/**
* The fake PortNetwork.
*
* @param {!goog.testing.MockControl} mockControl The mock control for creating
* the mock message channels.
* @constructor
* @implements {goog.messaging.PortNetwork}
* @final
*/
goog.testing.messaging.MockPortNetwork = function(mockControl) {
/**
* The mock control for creating mock message channels.
* @type {!goog.testing.MockControl}
* @private
*/
this.mockControl_ = mockControl;
/**
* The mock ports that have been created.
* @type {!Object<!goog.testing.messaging.MockMessageChannel>}
* @private
*/
this.ports_ = {};
};
/**
* Get the mock port with the given name.
* @param {string} name The name of the port to get.
* @return {!goog.testing.messaging.MockMessageChannel} The mock port.
* @override
*/
goog.testing.messaging.MockPortNetwork.prototype.dial = function(name) {
if (!(name in this.ports_)) {
this.ports_[name] =
new goog.testing.messaging.MockMessageChannel(this.mockControl_);
}
return this.ports_[name];
};
@@ -0,0 +1,645 @@
// 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 This file defines base classes used for creating mocks in
* JavaScript. The API was inspired by EasyMock.
*
* The basic API is:
* <ul>
* <li>Create an object to be mocked
* <li>Create a mock object, passing in the above object to the constructor
* <li>Set expectations by calling methods on the mock object
* <li>Call $replay() on the mock object
* <li>Pass the mock to code that will make real calls on it
* <li>Call $verify() to make sure that expectations were met
* </ul>
*
* For examples, please see the unit tests for LooseMock and StrictMock.
*
* Still TODO
* implement better (and pluggable) argument matching
* Have the exceptions for LooseMock show the number of expected/actual calls
* loose and strict mocks share a lot of code - move it to the base class
*
*/
goog.provide('goog.testing.Mock');
goog.provide('goog.testing.MockExpectation');
goog.require('goog.array');
goog.require('goog.object');
goog.require('goog.testing.JsUnitException');
goog.require('goog.testing.MockInterface');
goog.require('goog.testing.mockmatchers');
/**
* This is a class that represents an expectation.
* @param {string} name The name of the method for this expectation.
* @constructor
* @final
*/
goog.testing.MockExpectation = function(name) {
/**
* The name of the method that is expected to be called.
* @type {string}
*/
this.name = name;
/**
* An array of error messages for expectations not met.
* @type {Array<string>}
*/
this.errorMessages = [];
};
/**
* The minimum number of times this method should be called.
* @type {number}
*/
goog.testing.MockExpectation.prototype.minCalls = 1;
/**
* The maximum number of times this method should be called.
* @type {number}
*/
goog.testing.MockExpectation.prototype.maxCalls = 1;
/**
* The value that this method should return.
* @type {*}
*/
goog.testing.MockExpectation.prototype.returnValue;
/**
* The value that will be thrown when the method is called
* @type {*}
*/
goog.testing.MockExpectation.prototype.exceptionToThrow;
/**
* The arguments that are expected to be passed to this function
* @type {Array<*>}
*/
goog.testing.MockExpectation.prototype.argumentList;
/**
* The number of times this method is called by real code.
* @type {number}
*/
goog.testing.MockExpectation.prototype.actualCalls = 0;
/**
* The number of times this method is called during the verification phase.
* @type {number}
*/
goog.testing.MockExpectation.prototype.verificationCalls = 0;
/**
* The function which will be executed when this method is called.
* Method arguments will be passed to this function, and return value
* of this function will be returned by the method.
* @type {Function}
*/
goog.testing.MockExpectation.prototype.toDo;
/**
* Allow expectation failures to include messages.
* @param {string} message The failure message.
*/
goog.testing.MockExpectation.prototype.addErrorMessage = function(message) {
this.errorMessages.push(message);
};
/**
* Get the error messages seen so far.
* @return {string} Error messages separated by \n.
*/
goog.testing.MockExpectation.prototype.getErrorMessage = function() {
return this.errorMessages.join('\n');
};
/**
* Get how many error messages have been seen so far.
* @return {number} Count of error messages.
*/
goog.testing.MockExpectation.prototype.getErrorMessageCount = function() {
return this.errorMessages.length;
};
/**
* The base class for a mock object.
* @param {Object|Function} objectToMock The object that should be mocked, or
* the constructor of an object to mock.
* @param {boolean=} opt_mockStaticMethods An optional argument denoting that
* a mock should be constructed from the static functions of a class.
* @param {boolean=} opt_createProxy An optional argument denoting that
* a proxy for the target mock should be created.
* @constructor
* @implements {goog.testing.MockInterface}
*/
goog.testing.Mock = function(objectToMock, opt_mockStaticMethods,
opt_createProxy) {
if (!goog.isObject(objectToMock) && !goog.isFunction(objectToMock)) {
throw new Error('objectToMock must be an object or constructor.');
}
if (opt_createProxy && !opt_mockStaticMethods &&
goog.isFunction(objectToMock)) {
/**
* @constructor
* @final
*/
var tempCtor = function() {};
goog.inherits(tempCtor, objectToMock);
this.$proxy = new tempCtor();
} else if (opt_createProxy && opt_mockStaticMethods &&
goog.isFunction(objectToMock)) {
throw Error('Cannot create a proxy when opt_mockStaticMethods is true');
} else if (opt_createProxy && !goog.isFunction(objectToMock)) {
throw Error('Must have a constructor to create a proxy');
}
if (goog.isFunction(objectToMock) && !opt_mockStaticMethods) {
this.$initializeFunctions_(objectToMock.prototype);
} else {
this.$initializeFunctions_(objectToMock);
}
this.$argumentListVerifiers_ = {};
};
/**
* Option that may be passed when constructing function, method, and
* constructor mocks. Indicates that the expected calls should be accepted in
* any order.
* @const
* @type {number}
*/
goog.testing.Mock.LOOSE = 1;
/**
* Option that may be passed when constructing function, method, and
* constructor mocks. Indicates that the expected calls should be accepted in
* the recorded order only.
* @const
* @type {number}
*/
goog.testing.Mock.STRICT = 0;
/**
* This array contains the name of the functions that are part of the base
* Object prototype.
* Basically a copy of goog.object.PROTOTYPE_FIELDS_.
* @const
* @type {!Array<string>}
* @private
*/
goog.testing.Mock.PROTOTYPE_FIELDS_ = [
'constructor',
'hasOwnProperty',
'isPrototypeOf',
'propertyIsEnumerable',
'toLocaleString',
'toString',
'valueOf'
];
/**
* A proxy for the mock. This can be used for dependency injection in lieu of
* the mock if the test requires a strict instanceof check.
* @type {Object}
*/
goog.testing.Mock.prototype.$proxy = null;
/**
* Map of argument name to optional argument list verifier function.
* @type {Object}
*/
goog.testing.Mock.prototype.$argumentListVerifiers_;
/**
* Whether or not we are in recording mode.
* @type {boolean}
* @private
*/
goog.testing.Mock.prototype.$recording_ = true;
/**
* The expectation currently being created. All methods that modify the
* current expectation return the Mock object for easy chaining, so this is
* where we keep track of the expectation that's currently being modified.
* @type {goog.testing.MockExpectation}
* @protected
*/
goog.testing.Mock.prototype.$pendingExpectation;
/**
* First exception thrown by this mock; used in $verify.
* @type {Object}
* @private
*/
goog.testing.Mock.prototype.$threwException_ = null;
/**
* Initializes the functions on the mock object.
* @param {Object} objectToMock The object being mocked.
* @private
*/
goog.testing.Mock.prototype.$initializeFunctions_ = function(objectToMock) {
// Gets the object properties.
var enumerableProperties = goog.object.getKeys(objectToMock);
// The non enumerable properties are added if they override the ones in the
// Object prototype. This is due to the fact that IE8 does not enumerate any
// of the prototype Object functions even when overriden and mocking these is
// sometimes needed.
for (var i = 0; i < goog.testing.Mock.PROTOTYPE_FIELDS_.length; i++) {
var prop = goog.testing.Mock.PROTOTYPE_FIELDS_[i];
// Look at b/6758711 if you're considering adding ALL properties to ALL
// mocks.
if (objectToMock[prop] !== Object.prototype[prop]) {
enumerableProperties.push(prop);
}
}
// Adds the properties to the mock.
for (var i = 0; i < enumerableProperties.length; i++) {
var prop = enumerableProperties[i];
if (typeof objectToMock[prop] == 'function') {
this[prop] = goog.bind(this.$mockMethod, this, prop);
if (this.$proxy) {
this.$proxy[prop] = goog.bind(this.$mockMethod, this, prop);
}
}
}
};
/**
* Registers a verfifier function to use when verifying method argument lists.
* @param {string} methodName The name of the method for which the verifierFn
* should be used.
* @param {Function} fn Argument list verifier function. Should take 2 argument
* arrays as arguments, and return true if they are considered equivalent.
* @return {!goog.testing.Mock} This mock object.
*/
goog.testing.Mock.prototype.$registerArgumentListVerifier = function(methodName,
fn) {
this.$argumentListVerifiers_[methodName] = fn;
return this;
};
/**
* The function that replaces all methods on the mock object.
* @param {string} name The name of the method being mocked.
* @return {*} In record mode, returns the mock object. In replay mode, returns
* whatever the creator of the mock set as the return value.
*/
goog.testing.Mock.prototype.$mockMethod = function(name) {
try {
// Shift off the name argument so that args contains the arguments to
// the mocked method.
var args = goog.array.slice(arguments, 1);
if (this.$recording_) {
this.$pendingExpectation = new goog.testing.MockExpectation(name);
this.$pendingExpectation.argumentList = args;
this.$recordExpectation();
return this;
} else {
return this.$recordCall(name, args);
}
} catch (ex) {
this.$recordAndThrow(ex);
}
};
/**
* Records the currently pending expectation, intended to be overridden by a
* subclass.
* @protected
*/
goog.testing.Mock.prototype.$recordExpectation = function() {};
/**
* Records an actual method call, intended to be overridden by a
* subclass. The subclass must find the pending expectation and return the
* correct value.
* @param {string} name The name of the method being called.
* @param {Array<?>} args The arguments to the method.
* @return {*} The return expected by the mock.
* @protected
*/
goog.testing.Mock.prototype.$recordCall = function(name, args) {
return undefined;
};
/**
* If the expectation expects to throw, this method will throw.
* @param {goog.testing.MockExpectation} expectation The expectation.
*/
goog.testing.Mock.prototype.$maybeThrow = function(expectation) {
if (typeof expectation.exceptionToThrow != 'undefined') {
throw expectation.exceptionToThrow;
}
};
/**
* If this expectation defines a function to be called,
* it will be called and its result will be returned.
* Otherwise, if the expectation expects to throw, it will throw.
* Otherwise, this method will return defined value.
* @param {goog.testing.MockExpectation} expectation The expectation.
* @param {Array<?>} args The arguments to the method.
* @return {*} The return value expected by the mock.
*/
goog.testing.Mock.prototype.$do = function(expectation, args) {
if (typeof expectation.toDo == 'undefined') {
this.$maybeThrow(expectation);
return expectation.returnValue;
} else {
return expectation.toDo.apply(this, args);
}
};
/**
* Specifies a return value for the currently pending expectation.
* @param {*} val The return value.
* @return {!goog.testing.Mock} This mock object.
*/
goog.testing.Mock.prototype.$returns = function(val) {
this.$pendingExpectation.returnValue = val;
return this;
};
/**
* Specifies a value for the currently pending expectation to throw.
* @param {*} val The value to throw.
* @return {!goog.testing.Mock} This mock object.
*/
goog.testing.Mock.prototype.$throws = function(val) {
this.$pendingExpectation.exceptionToThrow = val;
return this;
};
/**
* Specifies a function to call for currently pending expectation.
* Note, that using this method overrides declarations made
* using $returns() and $throws() methods.
* @param {Function} func The function to call.
* @return {!goog.testing.Mock} This mock object.
*/
goog.testing.Mock.prototype.$does = function(func) {
this.$pendingExpectation.toDo = func;
return this;
};
/**
* Allows the expectation to be called 0 or 1 times.
* @return {!goog.testing.Mock} This mock object.
*/
goog.testing.Mock.prototype.$atMostOnce = function() {
this.$pendingExpectation.minCalls = 0;
this.$pendingExpectation.maxCalls = 1;
return this;
};
/**
* Allows the expectation to be called any number of times, as long as it's
* called once.
* @return {!goog.testing.Mock} This mock object.
*/
goog.testing.Mock.prototype.$atLeastOnce = function() {
this.$pendingExpectation.maxCalls = Infinity;
return this;
};
/**
* Allows the expectation to be called exactly once.
* @return {!goog.testing.Mock} This mock object.
*/
goog.testing.Mock.prototype.$once = function() {
this.$pendingExpectation.minCalls = 1;
this.$pendingExpectation.maxCalls = 1;
return this;
};
/**
* Disallows the expectation from being called.
* @return {!goog.testing.Mock} This mock object.
*/
goog.testing.Mock.prototype.$never = function() {
this.$pendingExpectation.minCalls = 0;
this.$pendingExpectation.maxCalls = 0;
return this;
};
/**
* Allows the expectation to be called any number of times.
* @return {!goog.testing.Mock} This mock object.
*/
goog.testing.Mock.prototype.$anyTimes = function() {
this.$pendingExpectation.minCalls = 0;
this.$pendingExpectation.maxCalls = Infinity;
return this;
};
/**
* Specifies the number of times the expectation should be called.
* @param {number} times The number of times this method will be called.
* @return {!goog.testing.Mock} This mock object.
*/
goog.testing.Mock.prototype.$times = function(times) {
this.$pendingExpectation.minCalls = times;
this.$pendingExpectation.maxCalls = times;
return this;
};
/**
* Switches from recording to replay mode.
* @override
*/
goog.testing.Mock.prototype.$replay = function() {
this.$recording_ = false;
};
/**
* Resets the state of this mock object. This clears all pending expectations
* without verifying, and puts the mock in recording mode.
* @override
*/
goog.testing.Mock.prototype.$reset = function() {
this.$recording_ = true;
this.$threwException_ = null;
delete this.$pendingExpectation;
};
/**
* Throws an exception and records that an exception was thrown.
* @param {string} comment A short comment about the exception.
* @param {?string=} opt_message A longer message about the exception.
* @throws {Object} JsUnitException object.
* @protected
*/
goog.testing.Mock.prototype.$throwException = function(comment, opt_message) {
this.$recordAndThrow(new goog.testing.JsUnitException(comment, opt_message));
};
/**
* Throws an exception and records that an exception was thrown.
* @param {Object} ex Exception.
* @throws {Object} #ex.
* @protected
*/
goog.testing.Mock.prototype.$recordAndThrow = function(ex) {
// If it's an assert exception, record it.
if (ex['isJsUnitException']) {
var testRunner = goog.global['G_testRunner'];
if (testRunner) {
var logTestFailureFunction = testRunner['logTestFailure'];
if (logTestFailureFunction) {
logTestFailureFunction.call(testRunner, ex);
}
}
if (!this.$threwException_) {
// Only remember first exception thrown.
this.$threwException_ = ex;
}
}
throw ex;
};
/**
* Verify that all of the expectations were met. Should be overridden by
* subclasses.
* @override
*/
goog.testing.Mock.prototype.$verify = function() {
if (this.$threwException_) {
throw this.$threwException_;
}
};
/**
* Verifies that a method call matches an expectation.
* @param {goog.testing.MockExpectation} expectation The expectation to check.
* @param {string} name The name of the called method.
* @param {Array<*>?} args The arguments passed to the mock.
* @return {boolean} Whether the call matches the expectation.
*/
goog.testing.Mock.prototype.$verifyCall = function(expectation, name, args) {
if (expectation.name != name) {
return false;
}
var verifierFn =
this.$argumentListVerifiers_.hasOwnProperty(expectation.name) ?
this.$argumentListVerifiers_[expectation.name] :
goog.testing.mockmatchers.flexibleArrayMatcher;
return verifierFn(expectation.argumentList, args, expectation);
};
/**
* Render the provided argument array to a string to help
* clients with debugging tests.
* @param {Array<*>?} args The arguments passed to the mock.
* @return {string} Human-readable string.
*/
goog.testing.Mock.prototype.$argumentsAsString = function(args) {
var retVal = [];
for (var i = 0; i < args.length; i++) {
try {
retVal.push(goog.typeOf(args[i]));
} catch (e) {
retVal.push('[unknown]');
}
}
return '(' + retVal.join(', ') + ')';
};
/**
* Throw an exception based on an incorrect method call.
* @param {string} name Name of method called.
* @param {Array<*>?} args Arguments passed to the mock.
* @param {goog.testing.MockExpectation=} opt_expectation Expected next call,
* if any.
*/
goog.testing.Mock.prototype.$throwCallException = function(name, args,
opt_expectation) {
var errorStringBuffer = [];
var actualArgsString = this.$argumentsAsString(args);
var expectedArgsString = opt_expectation ?
this.$argumentsAsString(opt_expectation.argumentList) : '';
if (opt_expectation && opt_expectation.name == name) {
errorStringBuffer.push('Bad arguments to ', name, '().\n',
'Actual: ', actualArgsString, '\n',
'Expected: ', expectedArgsString, '\n',
opt_expectation.getErrorMessage());
} else {
errorStringBuffer.push('Unexpected call to ', name,
actualArgsString, '.');
if (opt_expectation) {
errorStringBuffer.push('\nNext expected call was to ',
opt_expectation.name,
expectedArgsString);
}
}
this.$throwException(errorStringBuffer.join(''));
};
@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2008 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>
Closure Unit Tests - goog.testing.Mock
</title>
<script src="../base.js">
</script>
<script>
goog.require('goog.testing.MockTest');
</script>
</head>
<body>
</body>
</html>
@@ -0,0 +1,260 @@
// 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.
goog.provide('goog.testing.MockTest');
goog.setTestOnly('goog.testing.MockTest');
goog.require('goog.array');
goog.require('goog.testing');
goog.require('goog.testing.Mock');
goog.require('goog.testing.MockControl');
goog.require('goog.testing.MockExpectation');
goog.require('goog.testing.jsunit');
// The object that we will be mocking
var RealObject = function() {
};
RealObject.prototype.a = function() {
fail('real object should never be called');
};
RealObject.prototype.b = function() {
fail('real object should never be called');
};
var matchers = goog.testing.mockmatchers;
var mock;
function setUp() {
var obj = new RealObject();
mock = new goog.testing.Mock(obj);
}
function testMockErrorMessage() {
var expectation = new goog.testing.MockExpectation('a');
assertEquals(0, expectation.getErrorMessageCount());
assertEquals('', expectation.getErrorMessage());
expectation.addErrorMessage('foo failed');
assertEquals(1, expectation.getErrorMessageCount());
assertEquals('foo failed', expectation.getErrorMessage());
expectation.addErrorMessage('bar failed');
assertEquals(2, expectation.getErrorMessageCount());
assertEquals('foo failed\nbar failed', expectation.getErrorMessage());
}
function testVerifyArgumentList() {
var expectation = new goog.testing.MockExpectation('a');
assertEquals('', expectation.getErrorMessage());
// test single string arg
expectation.argumentList = ['foo'];
assertTrue(mock.$verifyCall(expectation, 'a', ['foo']));
// single numeric arg
expectation.argumentList = [2];
assertTrue(mock.$verifyCall(expectation, 'a', [2]));
// single object arg (using standard === comparison)
var obj = {prop1: 'prop1', prop2: 2};
expectation.argumentList = [obj];
assertTrue(mock.$verifyCall(expectation, 'a', [obj]));
// make sure comparison succeeds if args are similar, but not ===
var obj2 = {prop1: 'prop1', prop2: 2};
expectation.argumentList = [obj];
assertTrue(mock.$verifyCall(expectation, 'a', [obj2]));
assertEquals('', expectation.getErrorMessage());
// multiple args
expectation.argumentList = ['foo', 2, obj, obj2];
assertTrue(mock.$verifyCall(expectation, 'a', ['foo', 2, obj, obj2]));
// test flexible arg matching.
expectation.argumentList = ['foo', matchers.isNumber];
assertTrue(mock.$verifyCall(expectation, 'a', ['foo', 1]));
expectation.argumentList = [new matchers.InstanceOf(RealObject)];
assertTrue(mock.$verifyCall(expectation, 'a', [new RealObject()]));
}
function testVerifyArgumentListForObjectMethods() {
var expectation = new goog.testing.MockExpectation('toString');
expectation.argumentList = [];
assertTrue(mock.$verifyCall(expectation, 'toString', []));
}
function testRegisterArgumentListVerifier() {
var expectationA = new goog.testing.MockExpectation('a');
var expectationB = new goog.testing.MockExpectation('b');
// Simple matcher that return true if all args are === equivalent.
mock.$registerArgumentListVerifier('a', function(expectedArgs, args) {
return goog.array.equals(expectedArgs, args, function(a, b) {
return (a === b);
});
});
// test single string arg
expectationA.argumentList = ['foo'];
assertTrue(mock.$verifyCall(expectationA, 'a', ['foo']));
// single numeric arg
expectationA.argumentList = [2];
assertTrue(mock.$verifyCall(expectationA, 'a', [2]));
// single object arg (using standard === comparison)
var obj = {prop1: 'prop1', prop2: 2};
expectationA.argumentList = [obj];
expectationB.argumentList = [obj];
assertTrue(mock.$verifyCall(expectationA, 'a', [obj]));
assertTrue(mock.$verifyCall(expectationB, 'b', [obj]));
// if args are similar, but not ===, then comparison should succeed
// for method with registered object matcher, and fail for method without
var obj2 = {prop1: 'prop1', prop2: 2};
expectationA.argumentList = [obj];
expectationB.argumentList = [obj];
assertFalse(mock.$verifyCall(expectationA, 'a', [obj2]));
assertTrue(mock.$verifyCall(expectationB, 'b', [obj2]));
// multiple args, should fail for method with registered arg matcher,
// and succeed for method without.
expectationA.argumentList = ['foo', 2, obj, obj2];
expectationB.argumentList = ['foo', 2, obj, obj2];
assertFalse(mock.$verifyCall(expectationA, 'a', ['foo', 2, obj2, obj]));
assertTrue(mock.$verifyCall(expectationB, 'b', ['foo', 2, obj2, obj]));
}
function testCreateProxy() {
mock = new goog.testing.Mock(RealObject, false, true);
assertTrue(mock.$proxy instanceof RealObject);
assertThrows(function() {
new goog.testing.Mock(RealObject, true, true);
});
assertThrows(function() {
new goog.testing.Mock(1, false, true);
});
}
function testValidConstructorArgument() {
var someNamespace = { };
assertThrows(function() {
new goog.testing.Mock(someNamespace.RealObjectWithTypo);
});
}
function testArgumentsAsString() {
assertEquals('()', mock.$argumentsAsString([]));
assertEquals('(string, number, object, null)',
mock.$argumentsAsString(['red', 1, {}, null]));
}
function testThrowCallExceptionBadArgs() {
var msg;
mock.$throwException = function(m) {
msg = m;
};
mock.$throwCallException(
'fn1', ['b'],
{ name: 'fn1',
argumentList: ['c'],
getErrorMessage: function() { return ''; } });
assertContains(
'Bad arguments to fn1().\nActual: (string)\nExpected: (string)', msg);
}
function testThrowCallExceptionUnexpected() {
var msg;
mock.$throwException = function(m) {
msg = m;
};
mock.$throwCallException('fn1', ['b']);
assertEquals('Unexpected call to fn1(string).', msg);
}
function testThrowCallExceptionUnexpectedWithNext() {
var msg;
mock.$throwException = function(m) {
msg = m;
};
mock.$throwCallException(
'fn1', ['b'],
{ name: 'fn2',
argumentList: [3],
getErrorMessage: function() { return ''; } });
assertEquals(
'Unexpected call to fn1(string).\n' +
'Next expected call was to fn2(number)', msg);
}
// This tests that base Object functions which are not enumerable in IE can
// be mocked correctly.
function testBindNonEnumerableFunctions() {
// Create Foo and override non enumerable functions.
var Foo = function() {};
Foo.prototype.constructor = function() {
fail('real object should never be called');
};
Foo.prototype.hasOwnProperty = function() {
fail('real object should never be called');
};
Foo.prototype.isPrototypeOf = function() {
fail('real object should never be called');
};
Foo.prototype.propertyIsEnumerable = function() {
fail('real object should never be called');
};
Foo.prototype.toLocaleString = function() {
fail('real object should never be called');
};
Foo.prototype.toString = function() {
fail('real object should never be called');
};
Foo.prototype.valueOf = function() {
fail('real object should never be called');
};
// Create Mock and set $returns for toString.
var mockControl = new goog.testing.MockControl();
var mock = mockControl.createLooseMock(Foo);
mock.constructor().$returns('constructor');
mock.hasOwnProperty().$returns('hasOwnProperty');
mock.isPrototypeOf().$returns('isPrototypeOf');
mock.propertyIsEnumerable().$returns('propertyIsEnumerable');
mock.toLocaleString().$returns('toLocaleString');
mock.toString().$returns('toString');
mock.valueOf().$returns('valueOf');
// Execute and assert that the Mock is working correctly.
mockControl.$replayAll();
assertEquals('constructor', mock.constructor());
assertEquals('hasOwnProperty', mock.hasOwnProperty());
assertEquals('isPrototypeOf', mock.isPrototypeOf());
assertEquals('propertyIsEnumerable', mock.propertyIsEnumerable());
assertEquals('toLocaleString', mock.toLocaleString());
assertEquals('toString', mock.toString());
assertEquals('valueOf', mock.valueOf());
mockControl.$verifyAll();
}
@@ -0,0 +1,585 @@
// 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 This file defines a factory that can be used to mock and
* replace an entire class. This allows for mocks to be used effectively with
* "new" instead of having to inject all instances. Essentially, a given class
* is replaced with a proxy to either a loose or strict mock. Proxies locate
* the appropriate mock based on constructor arguments.
*
* The usage is:
* <ul>
* <li>Create a mock with one of the provided methods with a specifc set of
* constructor arguments
* <li>Set expectations by calling methods on the mock object
* <li>Call $replay() on the mock object
* <li>Instantiate the object as normal
* <li>Call $verify() to make sure that expectations were met
* <li>Call reset on the factory to revert all classes back to their original
* state
* </ul>
*
* For examples, please see the unit test.
*
*/
goog.provide('goog.testing.MockClassFactory');
goog.provide('goog.testing.MockClassRecord');
goog.require('goog.array');
goog.require('goog.object');
goog.require('goog.testing.LooseMock');
goog.require('goog.testing.StrictMock');
goog.require('goog.testing.TestCase');
goog.require('goog.testing.mockmatchers');
/**
* A record that represents all the data associated with a mock replacement of
* a given class.
* @param {Object} namespace The namespace in which the mocked class resides.
* @param {string} className The name of the class within the namespace.
* @param {Function} originalClass The original class implementation before it
* was replaced by a proxy.
* @param {Function} proxy The proxy that replaced the original class.
* @constructor
* @final
*/
goog.testing.MockClassRecord = function(namespace, className, originalClass,
proxy) {
/**
* A standard closure namespace (e.g. goog.foo.bar) that contains the mock
* class referenced by this MockClassRecord.
* @type {Object}
* @private
*/
this.namespace_ = namespace;
/**
* The name of the class within the provided namespace.
* @type {string}
* @private
*/
this.className_ = className;
/**
* The original class implementation.
* @type {Function}
* @private
*/
this.originalClass_ = originalClass;
/**
* The proxy being used as a replacement for the original class.
* @type {Function}
* @private
*/
this.proxy_ = proxy;
/**
* A mocks that will be constructed by their argument list. The entries are
* objects with the format {'args': args, 'mock': mock}.
* @type {Array<Object>}
* @private
*/
this.instancesByArgs_ = [];
};
/**
* A mock associated with the static functions for a given class.
* @type {goog.testing.StrictMock|goog.testing.LooseMock|null}
* @private
*/
goog.testing.MockClassRecord.prototype.staticMock_ = null;
/**
* A getter for this record's namespace.
* @return {Object} The namespace.
*/
goog.testing.MockClassRecord.prototype.getNamespace = function() {
return this.namespace_;
};
/**
* A getter for this record's class name.
* @return {string} The name of the class referenced by this record.
*/
goog.testing.MockClassRecord.prototype.getClassName = function() {
return this.className_;
};
/**
* A getter for the original class.
* @return {Function} The original class implementation before mocking.
*/
goog.testing.MockClassRecord.prototype.getOriginalClass = function() {
return this.originalClass_;
};
/**
* A getter for the proxy being used as a replacement for the original class.
* @return {Function} The proxy.
*/
goog.testing.MockClassRecord.prototype.getProxy = function() {
return this.proxy_;
};
/**
* A getter for the static mock.
* @return {goog.testing.StrictMock|goog.testing.LooseMock|null} The static
* mock associated with this record.
*/
goog.testing.MockClassRecord.prototype.getStaticMock = function() {
return this.staticMock_;
};
/**
* A setter for the static mock.
* @param {goog.testing.StrictMock|goog.testing.LooseMock} staticMock A mock to
* associate with the static functions for the referenced class.
*/
goog.testing.MockClassRecord.prototype.setStaticMock = function(staticMock) {
this.staticMock_ = staticMock;
};
/**
* Adds a new mock instance mapping. The mapping connects a set of function
* arguments to a specific mock instance.
* @param {Array<?>} args An array of function arguments.
* @param {goog.testing.StrictMock|goog.testing.LooseMock} mock A mock
* associated with the supplied arguments.
*/
goog.testing.MockClassRecord.prototype.addMockInstance = function(args, mock) {
this.instancesByArgs_.push({args: args, mock: mock});
};
/**
* Finds the mock corresponding to a given argument set. Throws an error if
* there is no appropriate match found.
* @param {Array<?>} args An array of function arguments.
* @return {goog.testing.StrictMock|goog.testing.LooseMock|null} The mock
* corresponding to a given argument set.
*/
goog.testing.MockClassRecord.prototype.findMockInstance = function(args) {
for (var i = 0; i < this.instancesByArgs_.length; i++) {
var instanceArgs = this.instancesByArgs_[i].args;
if (goog.testing.mockmatchers.flexibleArrayMatcher(instanceArgs, args)) {
return this.instancesByArgs_[i].mock;
}
}
return null;
};
/**
* Resets this record by reverting all the mocked classes back to the original
* implementation and clearing out the mock instance list.
*/
goog.testing.MockClassRecord.prototype.reset = function() {
this.namespace_[this.className_] = this.originalClass_;
this.instancesByArgs_ = [];
};
/**
* A factory used to create new mock class instances. It is able to generate
* both static and loose mocks. The MockClassFactory is a singleton since it
* tracks the classes that have been mocked internally.
* @constructor
* @final
*/
goog.testing.MockClassFactory = function() {
if (goog.testing.MockClassFactory.instance_) {
return goog.testing.MockClassFactory.instance_;
}
/**
* A map from class name -> goog.testing.MockClassRecord.
* @type {Object}
* @private
*/
this.mockClassRecords_ = {};
goog.testing.MockClassFactory.instance_ = this;
};
/**
* A singleton instance of the MockClassFactory.
* @type {goog.testing.MockClassFactory?}
* @private
*/
goog.testing.MockClassFactory.instance_ = null;
/**
* The names of the fields that are defined on Object.prototype.
* @type {Array<string>}
* @private
*/
goog.testing.MockClassFactory.PROTOTYPE_FIELDS_ = [
'constructor',
'hasOwnProperty',
'isPrototypeOf',
'propertyIsEnumerable',
'toLocaleString',
'toString',
'valueOf'
];
/**
* Iterates through a namespace to find the name of a given class. This is done
* solely to support compilation since string identifiers would break down.
* Tests usually aren't compiled, but the functionality is supported.
* @param {Object} namespace A javascript namespace (e.g. goog.testing).
* @param {Function} classToMock The class whose name should be returned.
* @return {string} The name of the class.
* @private
*/
goog.testing.MockClassFactory.prototype.getClassName_ = function(namespace,
classToMock) {
var namespaces;
if (namespace === goog.global) {
namespaces = goog.testing.TestCase.getGlobals();
} else {
namespaces = [namespace];
}
for (var i = 0; i < namespaces.length; i++) {
for (var prop in namespaces[i]) {
if (namespaces[i][prop] === classToMock) {
return prop;
}
}
}
throw Error('Class is not a part of the given namespace');
};
/**
* Returns whether or not a given class has been mocked.
* @param {string} className The name of the class.
* @return {boolean} Whether or not the given class name has a MockClassRecord.
* @private
*/
goog.testing.MockClassFactory.prototype.classHasMock_ = function(className) {
return !!this.mockClassRecords_[className];
};
/**
* Returns a proxy constructor closure. Since this is a constructor, "this"
* refers to the local scope of the constructed object thus bind cannot be
* used.
* @param {string} className The name of the class.
* @param {Function} mockFinder A bound function that returns the mock
* associated with a class given the constructor's argument list.
* @return {!Function} A proxy constructor.
* @private
*/
goog.testing.MockClassFactory.prototype.getProxyCtor_ = function(className,
mockFinder) {
return function() {
this.$mock_ = mockFinder(className, arguments);
if (!this.$mock_) {
// The "arguments" variable is not a proper Array so it must be converted.
var args = Array.prototype.slice.call(arguments, 0);
throw Error('No mock found for ' + className + ' with arguments ' +
args.join(', '));
}
};
};
/**
* Returns a proxy function for a mock class instance. This function cannot
* be used with bind since "this" must refer to the scope of the proxy
* constructor.
* @param {string} fnName The name of the function that should be proxied.
* @return {!Function} A proxy function.
* @private
*/
goog.testing.MockClassFactory.prototype.getProxyFunction_ = function(fnName) {
return function() {
return this.$mock_[fnName].apply(this.$mock_, arguments);
};
};
/**
* Find a mock instance for a given class name and argument list.
* @param {string} className The name of the class.
* @param {Array<?>} args The argument list to match.
* @return {goog.testing.StrictMock|goog.testing.LooseMock} The mock found for
* the given argument list.
* @private
*/
goog.testing.MockClassFactory.prototype.findMockInstance_ = function(className,
args) {
return this.mockClassRecords_[className].findMockInstance(args);
};
/**
* Create a proxy class. A proxy will pass functions to the mock for a class.
* The proxy class only covers prototype methods. A static mock is not build
* simultaneously since it might be strict or loose. The proxy class inherits
* from the target class in order to preserve instanceof checks.
* @param {Object} namespace A javascript namespace (e.g. goog.testing).
* @param {Function} classToMock The class that will be proxied.
* @param {string} className The name of the class.
* @return {!Function} The proxy for provided class.
* @private
*/
goog.testing.MockClassFactory.prototype.createProxy_ = function(namespace,
classToMock, className) {
var proxy = this.getProxyCtor_(className,
goog.bind(this.findMockInstance_, this));
var protoToProxy = classToMock.prototype;
goog.inherits(proxy, classToMock);
for (var prop in protoToProxy) {
if (goog.isFunction(protoToProxy[prop])) {
proxy.prototype[prop] = this.getProxyFunction_(prop);
}
}
// For IE the for-in-loop does not contain any properties that are not
// enumerable on the prototype object (for example isPrototypeOf from
// Object.prototype) and it will also not include 'replace' on objects that
// extend String and change 'replace' (not that it is common for anyone to
// extend anything except Object).
// TODO (arv): Implement goog.object.getIterator and replace this loop.
goog.array.forEach(goog.testing.MockClassFactory.PROTOTYPE_FIELDS_,
function(field) {
if (Object.prototype.hasOwnProperty.call(protoToProxy, field)) {
proxy.prototype[field] = this.getProxyFunction_(field);
}
}, this);
this.mockClassRecords_[className] = new goog.testing.MockClassRecord(
namespace, className, classToMock, proxy);
namespace[className] = proxy;
return proxy;
};
/**
* Gets either a loose or strict mock for a given class based on a set of
* arguments.
* @param {Object} namespace A javascript namespace (e.g. goog.testing).
* @param {Function} classToMock The class that will be mocked.
* @param {boolean} isStrict Whether or not the mock should be strict.
* @param {goog.array.ArrayLike} ctorArgs The arguments associated with this
* instance's constructor.
* @return {!goog.testing.StrictMock|!goog.testing.LooseMock} The mock created
* for the provided class.
* @private
*/
goog.testing.MockClassFactory.prototype.getMockClass_ =
function(namespace, classToMock, isStrict, ctorArgs) {
var className = this.getClassName_(namespace, classToMock);
// The namespace and classToMock variables should be removed from the
// passed in argument stack.
ctorArgs = goog.array.slice(ctorArgs, 2);
if (goog.isFunction(classToMock)) {
var mock = isStrict ? new goog.testing.StrictMock(classToMock) :
new goog.testing.LooseMock(classToMock);
if (!this.classHasMock_(className)) {
this.createProxy_(namespace, classToMock, className);
} else {
var instance = this.findMockInstance_(className, ctorArgs);
if (instance) {
throw Error('Mock instance already created for ' + className +
' with arguments ' + ctorArgs.join(', '));
}
}
this.mockClassRecords_[className].addMockInstance(ctorArgs, mock);
return mock;
} else {
throw Error('Cannot create a mock class for ' + className +
' of type ' + typeof classToMock);
}
};
/**
* Gets a strict mock for a given class.
* @param {Object} namespace A javascript namespace (e.g. goog.testing).
* @param {Function} classToMock The class that will be mocked.
* @param {...*} var_args The arguments associated with this instance's
* constructor.
* @return {!goog.testing.StrictMock} The mock created for the provided class.
*/
goog.testing.MockClassFactory.prototype.getStrictMockClass =
function(namespace, classToMock, var_args) {
return /** @type {!goog.testing.StrictMock} */ (this.getMockClass_(namespace,
classToMock, true, arguments));
};
/**
* Gets a loose mock for a given class.
* @param {Object} namespace A javascript namespace (e.g. goog.testing).
* @param {Function} classToMock The class that will be mocked.
* @param {...*} var_args The arguments associated with this instance's
* constructor.
* @return {goog.testing.LooseMock} The mock created for the provided class.
*/
goog.testing.MockClassFactory.prototype.getLooseMockClass =
function(namespace, classToMock, var_args) {
return /** @type {goog.testing.LooseMock} */ (this.getMockClass_(namespace,
classToMock, false, arguments));
};
/**
* Creates either a loose or strict mock for the static functions of a given
* class.
* @param {Function} classToMock The class whose static functions will be
* mocked. This should be the original class and not the proxy.
* @param {string} className The name of the class.
* @param {Function} proxy The proxy that will replace the original class.
* @param {boolean} isStrict Whether or not the mock should be strict.
* @return {!goog.testing.StrictMock|!goog.testing.LooseMock} The mock created
* for the static functions of the provided class.
* @private
*/
goog.testing.MockClassFactory.prototype.createStaticMock_ =
function(classToMock, className, proxy, isStrict) {
var mock = isStrict ? new goog.testing.StrictMock(classToMock, true) :
new goog.testing.LooseMock(classToMock, false, true);
for (var prop in classToMock) {
if (goog.isFunction(classToMock[prop])) {
proxy[prop] = goog.bind(mock.$mockMethod, mock, prop);
} else if (classToMock[prop] !== classToMock.prototype) {
proxy[prop] = classToMock[prop];
}
}
this.mockClassRecords_[className].setStaticMock(mock);
return mock;
};
/**
* Gets either a loose or strict mock for the static functions of a given class.
* @param {Object} namespace A javascript namespace (e.g. goog.testing).
* @param {Function} classToMock The class whose static functions will be
* mocked. This should be the original class and not the proxy.
* @param {boolean} isStrict Whether or not the mock should be strict.
* @return {goog.testing.StrictMock|goog.testing.LooseMock} The mock created
* for the static functions of the provided class.
* @private
*/
goog.testing.MockClassFactory.prototype.getStaticMock_ = function(namespace,
classToMock, isStrict) {
var className = this.getClassName_(namespace, classToMock);
if (goog.isFunction(classToMock)) {
if (!this.classHasMock_(className)) {
var proxy = this.createProxy_(namespace, classToMock, className);
var mock = this.createStaticMock_(classToMock, className, proxy,
isStrict);
return mock;
}
if (!this.mockClassRecords_[className].getStaticMock()) {
var proxy = this.mockClassRecords_[className].getProxy();
var originalClass = this.mockClassRecords_[className].getOriginalClass();
var mock = this.createStaticMock_(originalClass, className, proxy,
isStrict);
return mock;
} else {
var mock = this.mockClassRecords_[className].getStaticMock();
var mockIsStrict = mock instanceof goog.testing.StrictMock;
if (mockIsStrict != isStrict) {
var mockType = mock instanceof goog.testing.StrictMock ? 'strict' :
'loose';
var requestedType = isStrict ? 'strict' : 'loose';
throw Error('Requested a ' + requestedType + ' static mock, but a ' +
mockType + ' mock already exists.');
}
return mock;
}
} else {
throw Error('Cannot create a mock for the static functions of ' +
className + ' of type ' + typeof classToMock);
}
};
/**
* Gets a strict mock for the static functions of a given class.
* @param {Object} namespace A javascript namespace (e.g. goog.testing).
* @param {Function} classToMock The class whose static functions will be
* mocked. This should be the original class and not the proxy.
* @return {goog.testing.StrictMock} The mock created for the static functions
* of the provided class.
*/
goog.testing.MockClassFactory.prototype.getStrictStaticMock =
function(namespace, classToMock) {
return /** @type {goog.testing.StrictMock} */ (this.getStaticMock_(namespace,
classToMock, true));
};
/**
* Gets a loose mock for the static functions of a given class.
* @param {Object} namespace A javascript namespace (e.g. goog.testing).
* @param {Function} classToMock The class whose static functions will be
* mocked. This should be the original class and not the proxy.
* @return {goog.testing.LooseMock} The mock created for the static functions
* of the provided class.
*/
goog.testing.MockClassFactory.prototype.getLooseStaticMock =
function(namespace, classToMock) {
return /** @type {goog.testing.LooseMock} */ (this.getStaticMock_(namespace,
classToMock, false));
};
/**
* Resests the factory by reverting all mocked classes to their original
* implementations and removing all MockClassRecords.
*/
goog.testing.MockClassFactory.prototype.reset = function() {
goog.object.forEach(this.mockClassRecords_, function(record) {
record.reset();
});
this.mockClassRecords_ = {};
};
@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2008 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>
Closure Unit Tests - goog.testing.MockClassFactory
</title>
<script src="../base.js">
</script>
<script>
goog.require('goog.testing.MockClassFactoryTest');
</script>
</head>
<body>
</body>
</html>
@@ -0,0 +1,238 @@
// 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.
goog.setTestOnly('goog.testing.MockClassFactoryTest');
goog.require('goog.testing');
goog.require('goog.testing.MockClassFactory');
goog.require('goog.testing.jsunit');
goog.provide('fake.BaseClass');
goog.provide('fake.ChildClass');
goog.provide('goog.testing.MockClassFactoryTest');
// Classes that will be mocked. A base class and child class are used to
// test inheritance.
fake.BaseClass = function(a) {
fail('real object should never be called');
};
fake.BaseClass.prototype.foo = function() {
fail('real object should never be called');
};
fake.BaseClass.prototype.toString = function() {return 'foo';};
fake.BaseClass.prototype.toLocaleString = function() {return 'bar';};
fake.ChildClass = function(a) {
fail('real object should never be called');
};
goog.inherits(fake.ChildClass, fake.BaseClass);
fake.ChildClass.staticFoo = function() {
fail('real object should never be called');
};
fake.ChildClass.prototype.bar = function() {
fail('real object should never be called');
};
fake.ChildClass.staticProperty = 'staticPropertyOnClass';
function TopLevelBaseClass() {
}
var mockClassFactory = new goog.testing.MockClassFactory();
var matchers = goog.testing.mockmatchers;
function tearDown() {
mockClassFactory.reset();
}
function testGetStrictMockClass() {
var mock1 = mockClassFactory.getStrictMockClass(fake, fake.BaseClass, 1);
mock1.foo();
mock1.$replay();
var mock2 = mockClassFactory.getStrictMockClass(fake, fake.BaseClass, 2);
mock2.foo();
mock2.$replay();
var mock3 = mockClassFactory.getStrictMockClass(fake, fake.ChildClass, 3);
mock3.foo();
mock3.bar();
mock3.$replay();
var instance1 = new fake.BaseClass(1);
instance1.foo();
mock1.$verify();
var instance2 = new fake.BaseClass(2);
instance2.foo();
mock2.$verify();
var instance3 = new fake.ChildClass(3);
instance3.foo();
instance3.bar();
mock3.$verify();
assertThrows(function() {new fake.BaseClass(-1)});
assertTrue(instance1 instanceof fake.BaseClass);
assertTrue(instance2 instanceof fake.BaseClass);
assertTrue(instance3 instanceof fake.ChildClass);
}
function testGetStrictMockClassCreatesAllProxies() {
var mock1 = mockClassFactory.getStrictMockClass(fake, fake.BaseClass, 1);
// toString(), toLocaleString() and others are treaded specially in
// createProxy_().
mock1.toString();
mock1.toLocaleString();
mock1.$replay();
var instance1 = new fake.BaseClass(1);
instance1.toString();
instance1.toLocaleString();
mock1.$verify();
}
function testGetLooseMockClass() {
var mock1 = mockClassFactory.getLooseMockClass(fake, fake.BaseClass, 1);
mock1.foo().$anyTimes().$returns(3);
mock1.$replay();
var mock2 = mockClassFactory.getLooseMockClass(fake, fake.BaseClass, 2);
mock2.foo().$times(3);
mock2.$replay();
var mock3 = mockClassFactory.getLooseMockClass(fake, fake.ChildClass, 3);
mock3.foo().$atLeastOnce().$returns(5);
mock3.bar().$atLeastOnce();
mock3.$replay();
var instance1 = new fake.BaseClass(1);
assertEquals(3, instance1.foo());
assertEquals(3, instance1.foo());
assertEquals(3, instance1.foo());
assertEquals(3, instance1.foo());
assertEquals(3, instance1.foo());
mock1.$verify();
var instance2 = new fake.BaseClass(2);
instance2.foo();
instance2.foo();
instance2.foo();
mock2.$verify();
var instance3 = new fake.ChildClass(3);
assertEquals(5, instance3.foo());
assertEquals(5, instance3.foo());
instance3.bar();
mock3.$verify();
assertThrows(function() {new fake.BaseClass(-1)});
assertTrue(instance1 instanceof fake.BaseClass);
assertTrue(instance2 instanceof fake.BaseClass);
assertTrue(instance3 instanceof fake.ChildClass);
}
function testGetStrictStaticMock() {
var staticMock = mockClassFactory.getStrictStaticMock(fake,
fake.ChildClass);
var mock = mockClassFactory.getStrictMockClass(fake, fake.ChildClass, 1);
mock.foo();
mock.bar();
staticMock.staticFoo();
mock.$replay();
staticMock.$replay();
var instance = new fake.ChildClass(1);
instance.foo();
instance.bar();
fake.ChildClass.staticFoo();
mock.$verify();
staticMock.$verify();
assertTrue(instance instanceof fake.BaseClass);
assertTrue(instance instanceof fake.ChildClass);
assertThrows(function() {
mockClassFactory.getLooseStaticMock(fake, fake.ChildClass);
});
}
function testGetStrictStaticMockKeepsStaticProperties() {
var OriginalChildClass = fake.ChildClass;
var staticMock = mockClassFactory.getStrictStaticMock(fake,
fake.ChildClass);
assertEquals(OriginalChildClass.staticProperty,
fake.ChildClass.staticProperty);
}
function testGetLooseStaticMockKeepsStaticProperties() {
var OriginalChildClass = fake.ChildClass;
var staticMock = mockClassFactory.getLooseStaticMock(fake,
fake.ChildClass);
assertEquals(OriginalChildClass.staticProperty,
fake.ChildClass.staticProperty);
}
function testGetLooseStaticMock() {
var staticMock = mockClassFactory.getLooseStaticMock(fake,
fake.ChildClass);
var mock = mockClassFactory.getStrictMockClass(fake, fake.ChildClass, 1);
mock.foo();
mock.bar();
staticMock.staticFoo().$atLeastOnce();
mock.$replay();
staticMock.$replay();
var instance = new fake.ChildClass(1);
instance.foo();
instance.bar();
fake.ChildClass.staticFoo();
fake.ChildClass.staticFoo();
mock.$verify();
staticMock.$verify();
assertTrue(instance instanceof fake.BaseClass);
assertTrue(instance instanceof fake.ChildClass);
assertThrows(function() {
mockClassFactory.getStrictStaticMock(fake, fake.ChildClass);
});
}
function testFlexibleClassMockInstantiation() {
// This mock should be returned for all instances created with a number
// as the first argument.
var mock = mockClassFactory.getStrictMockClass(fake, fake.ChildClass,
matchers.isNumber);
mock.foo(); // Will be called by the first mock instance.
mock.foo(); // Will be called by the second mock instance.
mock.$replay();
var instance1 = new fake.ChildClass(1);
var instance2 = new fake.ChildClass(2);
instance1.foo();
instance2.foo();
assertThrows(function() {
new fake.ChildClass('foo');
});
mock.$verify();
}
function testMockTopLevelClass() {
var mock = mockClassFactory.getStrictMockClass(goog.global,
goog.global.TopLevelBaseClass);
}
@@ -0,0 +1,591 @@
// Copyright 2007 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 Mock Clock implementation for working with setTimeout,
* setInterval, clearTimeout and clearInterval within unit tests.
*
* Derived from jsUnitMockTimeout.js, contributed to JsUnit by
* Pivotal Computer Systems, www.pivotalsf.com
*
*/
goog.provide('goog.testing.MockClock');
goog.require('goog.Disposable');
goog.require('goog.async.run');
goog.require('goog.testing.PropertyReplacer');
goog.require('goog.testing.events');
goog.require('goog.testing.events.Event');
goog.require('goog.testing.watchers');
/**
* Class for unit testing code that uses setTimeout and clearTimeout.
*
* NOTE: If you are using MockClock to test code that makes use of
* goog.fx.Animation, then you must either:
*
* 1. Install and dispose of the MockClock in setUpPage() and tearDownPage()
* respectively (rather than setUp()/tearDown()).
*
* or
*
* 2. Ensure that every test clears the animation queue by calling
* mockClock.tick(x) at the end of each test function (where `x` is large
* enough to complete all animations).
*
* Otherwise, if any animation is left pending at the time that
* MockClock.dispose() is called, that will permanently prevent any future
* animations from playing on the page.
*
* @param {boolean=} opt_autoInstall Install the MockClock at construction time.
* @constructor
* @extends {goog.Disposable}
* @final
*/
goog.testing.MockClock = function(opt_autoInstall) {
goog.Disposable.call(this);
/**
* Reverse-order queue of timers to fire.
*
* The last item of the queue is popped off. Insertion happens from the
* right. For example, the expiration times for each element of the queue
* might be in the order 300, 200, 200.
*
* @type {Array<Object>}
* @private
*/
this.queue_ = [];
/**
* Set of timeouts that should be treated as cancelled.
*
* Rather than removing cancelled timers directly from the queue, this set
* simply marks them as deleted so that they can be ignored when their
* turn comes up. The keys are the timeout keys that are cancelled, each
* mapping to true.
*
* @type {Object}
* @private
*/
this.deletedKeys_ = {};
if (opt_autoInstall) {
this.install();
}
};
goog.inherits(goog.testing.MockClock, goog.Disposable);
/**
* Default wait timeout for mocking requestAnimationFrame (in milliseconds).
*
* @type {number}
* @const
*/
goog.testing.MockClock.REQUEST_ANIMATION_FRAME_TIMEOUT = 20;
/**
* Count of the number of timeouts made.
* @type {number}
* @private
*/
goog.testing.MockClock.prototype.timeoutsMade_ = 0;
/**
* PropertyReplacer instance which overwrites and resets setTimeout,
* setInterval, etc. or null if the MockClock is not installed.
* @type {goog.testing.PropertyReplacer}
* @private
*/
goog.testing.MockClock.prototype.replacer_ = null;
/**
* Map of deleted keys. These keys represents keys that were deleted in a
* clearInterval, timeoutid -> object.
* @type {Object}
* @private
*/
goog.testing.MockClock.prototype.deletedKeys_ = null;
/**
* The current simulated time in milliseconds.
* @type {number}
* @private
*/
goog.testing.MockClock.prototype.nowMillis_ = 0;
/**
* Additional delay between the time a timeout was set to fire, and the time
* it actually fires. Useful for testing workarounds for this Firefox 2 bug:
* https://bugzilla.mozilla.org/show_bug.cgi?id=291386
* May be negative.
* @type {number}
* @private
*/
goog.testing.MockClock.prototype.timeoutDelay_ = 0;
/**
* Installs the MockClock by overriding the global object's implementation of
* setTimeout, setInterval, clearTimeout and clearInterval.
*/
goog.testing.MockClock.prototype.install = function() {
if (!this.replacer_) {
var r = this.replacer_ = new goog.testing.PropertyReplacer();
r.set(goog.global, 'setTimeout', goog.bind(this.setTimeout_, this));
r.set(goog.global, 'setInterval', goog.bind(this.setInterval_, this));
r.set(goog.global, 'setImmediate', goog.bind(this.setImmediate_, this));
r.set(goog.global, 'clearTimeout', goog.bind(this.clearTimeout_, this));
r.set(goog.global, 'clearInterval', goog.bind(this.clearInterval_, this));
// goog.Promise uses goog.async.run. In order to be able to test
// Promise-based code, we need to make sure that goog.async.run uses
// nextTick instead of native browser Promises. This means that it will
// default to setImmediate, which is replaced above. Note that we test for
// the presence of goog.async.run.forceNextTick to be resilient to the case
// where tests replace goog.async.run directly.
goog.async.run.forceNextTick && goog.async.run.forceNextTick();
// Replace the requestAnimationFrame functions.
this.replaceRequestAnimationFrame_();
// PropertyReplacer#set can't be called with renameable functions.
this.oldGoogNow_ = goog.now;
goog.now = goog.bind(this.getCurrentTime, this);
}
};
/**
* Installs the mocks for requestAnimationFrame and cancelRequestAnimationFrame.
* @private
*/
goog.testing.MockClock.prototype.replaceRequestAnimationFrame_ = function() {
var r = this.replacer_;
var requestFuncs = ['requestAnimationFrame',
'webkitRequestAnimationFrame',
'mozRequestAnimationFrame',
'oRequestAnimationFrame',
'msRequestAnimationFrame'];
var cancelFuncs = ['cancelAnimationFrame',
'cancelRequestAnimationFrame',
'webkitCancelRequestAnimationFrame',
'mozCancelRequestAnimationFrame',
'oCancelRequestAnimationFrame',
'msCancelRequestAnimationFrame'];
for (var i = 0; i < requestFuncs.length; ++i) {
if (goog.global && goog.global[requestFuncs[i]]) {
r.set(goog.global, requestFuncs[i],
goog.bind(this.requestAnimationFrame_, this));
}
}
for (var i = 0; i < cancelFuncs.length; ++i) {
if (goog.global && goog.global[cancelFuncs[i]]) {
r.set(goog.global, cancelFuncs[i],
goog.bind(this.cancelRequestAnimationFrame_, this));
}
}
};
/**
* Removes the MockClock's hooks into the global object's functions and revert
* to their original values.
*/
goog.testing.MockClock.prototype.uninstall = function() {
if (this.replacer_) {
this.replacer_.reset();
this.replacer_ = null;
goog.now = this.oldGoogNow_;
}
this.fireResetEvent();
};
/** @override */
goog.testing.MockClock.prototype.disposeInternal = function() {
this.uninstall();
this.queue_ = null;
this.deletedKeys_ = null;
goog.testing.MockClock.superClass_.disposeInternal.call(this);
};
/**
* Resets the MockClock, removing all timeouts that are scheduled and resets
* the fake timer count.
*/
goog.testing.MockClock.prototype.reset = function() {
this.queue_ = [];
this.deletedKeys_ = {};
this.nowMillis_ = 0;
this.timeoutsMade_ = 0;
this.timeoutDelay_ = 0;
this.fireResetEvent();
};
/**
* Signals that the mock clock has been reset, allowing objects that
* maintain their own internal state to reset.
*/
goog.testing.MockClock.prototype.fireResetEvent = function() {
goog.testing.watchers.signalClockReset();
};
/**
* Sets the amount of time between when a timeout is scheduled to fire and when
* it actually fires.
* @param {number} delay The delay in milliseconds. May be negative.
*/
goog.testing.MockClock.prototype.setTimeoutDelay = function(delay) {
this.timeoutDelay_ = delay;
};
/**
* @return {number} delay The amount of time between when a timeout is
* scheduled to fire and when it actually fires, in milliseconds. May
* be negative.
*/
goog.testing.MockClock.prototype.getTimeoutDelay = function() {
return this.timeoutDelay_;
};
/**
* Increments the MockClock's time by a given number of milliseconds, running
* any functions that are now overdue.
* @param {number=} opt_millis Number of milliseconds to increment the counter.
* If not specified, clock ticks 1 millisecond.
* @return {number} Current mock time in milliseconds.
*/
goog.testing.MockClock.prototype.tick = function(opt_millis) {
if (typeof opt_millis != 'number') {
opt_millis = 1;
}
var endTime = this.nowMillis_ + opt_millis;
this.runFunctionsWithinRange_(endTime);
this.nowMillis_ = endTime;
return endTime;
};
/**
* Takes a promise and then ticks the mock clock. If the promise successfully
* resolves, returns the value produced by the promise. If the promise is
* rejected, it throws the rejection as an exception. If the promise is not
* resolved at all, throws an exception.
* Also ticks the general clock by the specified amount.
*
* @param {!goog.Thenable<T>} promise A promise that should be resolved after
* the mockClock is ticked for the given opt_millis.
* @param {number=} opt_millis Number of milliseconds to increment the counter.
* If not specified, clock ticks 1 millisecond.
* @return {T}
* @template T
*/
goog.testing.MockClock.prototype.tickPromise = function(promise, opt_millis) {
var value;
var error;
var resolved = false;
promise.then(function(v) {
value = v;
resolved = true;
}, function(e) {
error = e;
resolved = true;
});
this.tick(opt_millis);
if (!resolved) {
throw new Error(
'Promise was expected to be resolved after mock clock tick.');
}
if (error) {
throw error;
}
return value;
};
/**
* @return {number} The number of timeouts that have been scheduled.
*/
goog.testing.MockClock.prototype.getTimeoutsMade = function() {
return this.timeoutsMade_;
};
/**
* @return {number} The MockClock's current time in milliseconds.
*/
goog.testing.MockClock.prototype.getCurrentTime = function() {
return this.nowMillis_;
};
/**
* @param {number} timeoutKey The timeout key.
* @return {boolean} Whether the timer has been set and not cleared,
* independent of the timeout's expiration. In other words, the timeout
* could have passed or could be scheduled for the future. Either way,
* this function returns true or false depending only on whether the
* provided timeoutKey represents a timeout that has been set and not
* cleared.
*/
goog.testing.MockClock.prototype.isTimeoutSet = function(timeoutKey) {
return timeoutKey <= this.timeoutsMade_ && !this.deletedKeys_[timeoutKey];
};
/**
* Runs any function that is scheduled before a certain time. Timeouts can
* be made to fire early or late if timeoutDelay_ is non-0.
* @param {number} endTime The latest time in the range, in milliseconds.
* @private
*/
goog.testing.MockClock.prototype.runFunctionsWithinRange_ = function(
endTime) {
var adjustedEndTime = endTime - this.timeoutDelay_;
// Repeatedly pop off the last item since the queue is always sorted.
while (this.queue_ && this.queue_.length &&
this.queue_[this.queue_.length - 1].runAtMillis <= adjustedEndTime) {
var timeout = this.queue_.pop();
if (!(timeout.timeoutKey in this.deletedKeys_)) {
// Only move time forwards.
this.nowMillis_ = Math.max(this.nowMillis_,
timeout.runAtMillis + this.timeoutDelay_);
// Call timeout in global scope and pass the timeout key as the argument.
timeout.funcToCall.call(goog.global, timeout.timeoutKey);
// In case the interval was cleared in the funcToCall
if (timeout.recurring) {
this.scheduleFunction_(
timeout.timeoutKey, timeout.funcToCall, timeout.millis, true);
}
}
}
};
/**
* Schedules a function to be run at a certain time.
* @param {number} timeoutKey The timeout key.
* @param {Function} funcToCall The function to call.
* @param {number} millis The number of milliseconds to call it in.
* @param {boolean} recurring Whether to function call should recur.
* @private
*/
goog.testing.MockClock.prototype.scheduleFunction_ = function(
timeoutKey, funcToCall, millis, recurring) {
if (!goog.isFunction(funcToCall)) {
// Early error for debuggability rather than dying in the next .tick()
throw new TypeError('The provided callback must be a function, not a ' +
typeof funcToCall);
}
var timeout = {
runAtMillis: this.nowMillis_ + millis,
funcToCall: funcToCall,
recurring: recurring,
timeoutKey: timeoutKey,
millis: millis
};
goog.testing.MockClock.insert_(timeout, this.queue_);
};
/**
* Inserts a timer descriptor into a descending-order queue.
*
* Later-inserted duplicates appear at lower indices. For example, the
* asterisk in (5,4,*,3,2,1) would be the insertion point for 3.
*
* @param {Object} timeout The timeout to insert, with numerical runAtMillis
* property.
* @param {Array<Object>} queue The queue to insert into, with each element
* having a numerical runAtMillis property.
* @private
*/
goog.testing.MockClock.insert_ = function(timeout, queue) {
// Although insertion of N items is quadratic, requiring goog.structs.Heap
// from a unit test will make tests more prone to breakage. Since unit
// tests are normally small, scalability is not a primary issue.
// Find an insertion point. Since the queue is in reverse order (so we
// can pop rather than unshift), and later timers with the same time stamp
// should be executed later, we look for the element strictly greater than
// the one we are inserting.
for (var i = queue.length; i != 0; i--) {
if (queue[i - 1].runAtMillis > timeout.runAtMillis) {
break;
}
queue[i] = queue[i - 1];
}
queue[i] = timeout;
};
/**
* Maximum 32-bit signed integer.
*
* Timeouts over this time return immediately in many browsers, due to integer
* overflow. Such known browsers include Firefox, Chrome, and Safari, but not
* IE.
*
* @type {number}
* @private
*/
goog.testing.MockClock.MAX_INT_ = 2147483647;
/**
* Schedules a function to be called after {@code millis} milliseconds.
* Mock implementation for setTimeout.
* @param {Function} funcToCall The function to call.
* @param {number=} opt_millis The number of milliseconds to call it after.
* @return {number} The number of timeouts created.
* @private
*/
goog.testing.MockClock.prototype.setTimeout_ = function(
funcToCall, opt_millis) {
var millis = opt_millis || 0;
if (millis > goog.testing.MockClock.MAX_INT_) {
throw Error(
'Bad timeout value: ' + millis + '. Timeouts over MAX_INT ' +
'(24.8 days) cause timeouts to be fired ' +
'immediately in most browsers, except for IE.');
}
this.timeoutsMade_ = this.timeoutsMade_ + 1;
this.scheduleFunction_(this.timeoutsMade_, funcToCall, millis, false);
return this.timeoutsMade_;
};
/**
* Schedules a function to be called every {@code millis} milliseconds.
* Mock implementation for setInterval.
* @param {Function} funcToCall The function to call.
* @param {number=} opt_millis The number of milliseconds between calls.
* @return {number} The number of timeouts created.
* @private
*/
goog.testing.MockClock.prototype.setInterval_ =
function(funcToCall, opt_millis) {
var millis = opt_millis || 0;
this.timeoutsMade_ = this.timeoutsMade_ + 1;
this.scheduleFunction_(this.timeoutsMade_, funcToCall, millis, true);
return this.timeoutsMade_;
};
/**
* Schedules a function to be called when an animation frame is triggered.
* Mock implementation for requestAnimationFrame.
* @param {Function} funcToCall The function to call.
* @return {number} The number of timeouts created.
* @private
*/
goog.testing.MockClock.prototype.requestAnimationFrame_ = function(funcToCall) {
return this.setTimeout_(goog.bind(function() {
if (funcToCall) {
funcToCall(this.getCurrentTime());
} else if (goog.global.mozRequestAnimationFrame) {
var event = new goog.testing.events.Event('MozBeforePaint', goog.global);
event['timeStamp'] = this.getCurrentTime();
goog.testing.events.fireBrowserEvent(event);
}
}, this), goog.testing.MockClock.REQUEST_ANIMATION_FRAME_TIMEOUT);
};
/**
* Schedules a function to be called immediately after the current JS
* execution.
* Mock implementation for setImmediate.
* @param {Function} funcToCall The function to call.
* @return {number} The number of timeouts created.
* @private
*/
goog.testing.MockClock.prototype.setImmediate_ = function(funcToCall) {
return this.setTimeout_(funcToCall, 0);
};
/**
* Clears a timeout.
* Mock implementation for clearTimeout.
* @param {number} timeoutKey The timeout key to clear.
* @private
*/
goog.testing.MockClock.prototype.clearTimeout_ = function(timeoutKey) {
// Some common libraries register static state with timers.
// This is bad. It leads to all sorts of crazy test problems where
// 1) Test A sets up a new mock clock and a static timer.
// 2) Test B sets up a new mock clock, but re-uses the static timer
// from Test A.
// 3) A timeout key from test A gets cleared, breaking a timeout in
// Test B.
//
// For now, we just hackily fail silently if someone tries to clear a timeout
// key before we've allocated it.
// Ideally, we should throw an exception if we see this happening.
//
// TODO(chrishenry): We might also try allocating timeout ids from a global
// pool rather than a local pool.
if (this.isTimeoutSet(timeoutKey)) {
this.deletedKeys_[timeoutKey] = true;
}
};
/**
* Clears an interval.
* Mock implementation for clearInterval.
* @param {number} timeoutKey The interval key to clear.
* @private
*/
goog.testing.MockClock.prototype.clearInterval_ = function(timeoutKey) {
this.clearTimeout_(timeoutKey);
};
/**
* Clears a requestAnimationFrame.
* Mock implementation for cancelRequestAnimationFrame.
* @param {number} timeoutKey The requestAnimationFrame key to clear.
* @private
*/
goog.testing.MockClock.prototype.cancelRequestAnimationFrame_ =
function(timeoutKey) {
this.clearTimeout_(timeoutKey);
};
@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2007 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>
Closure Unit Tests - goog.testing.MockClock
</title>
<script src="../base.js">
</script>
<script>
goog.require('goog.testing.MockClockTest');
</script>
</head>
<body>
</body>
</html>
@@ -0,0 +1,610 @@
// Copyright 2007 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.
goog.provide('goog.testing.MockClockTest');
goog.setTestOnly('goog.testing.MockClockTest');
goog.require('goog.Promise');
goog.require('goog.Timer');
goog.require('goog.events');
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 stubs = new goog.testing.PropertyReplacer();
function tearDown() {
stubs.reset();
}
function testMockClockWasInstalled() {
var clock = new goog.testing.MockClock();
var originalTimeout = window.setTimeout;
clock.install();
assertNotEquals(window.setTimeout, originalTimeout);
setTimeout(function() {}, 100);
assertEquals(1, clock.getTimeoutsMade());
setInterval(function() {}, 200);
assertEquals(2, clock.getTimeoutsMade());
clock.uninstall();
assertEquals(window.setTimeout, originalTimeout);
assertNull(clock.replacer_);
}
function testSetTimeoutAndTick() {
var clock = new goog.testing.MockClock(true);
var m5 = false, m10 = false, m15 = false, m20 = false;
setTimeout(function() { m5 = true; }, 5);
setTimeout(function() { m10 = true; }, 10);
setTimeout(function() { m15 = true; }, 15);
setTimeout(function() { m20 = true; }, 20);
assertEquals(4, clock.getTimeoutsMade());
assertEquals(4, clock.tick(4));
assertEquals(4, clock.getCurrentTime());
assertFalse(m5);
assertFalse(m10);
assertFalse(m15);
assertFalse(m20);
assertEquals(5, clock.tick(1));
assertEquals(5, clock.getCurrentTime());
assertTrue('m5 should now be true', m5);
assertFalse(m10);
assertFalse(m15);
assertFalse(m20);
assertEquals(10, clock.tick(5));
assertEquals(10, clock.getCurrentTime());
assertTrue('m5 should be true', m5);
assertTrue('m10 should now be true', m10);
assertFalse(m15);
assertFalse(m20);
assertEquals(15, clock.tick(5));
assertEquals(15, clock.getCurrentTime());
assertTrue('m5 should be true', m5);
assertTrue('m10 should be true', m10);
assertTrue('m15 should now be true', m15);
assertFalse(m20);
assertEquals(20, clock.tick(5));
assertEquals(20, clock.getCurrentTime());
assertTrue('m5 should be true', m5);
assertTrue('m10 should be true', m10);
assertTrue('m15 should be true', m15);
assertTrue('m20 should now be true', m20);
clock.uninstall();
}
function testSetImmediateAndTick() {
var clock = new goog.testing.MockClock(true);
var tick0 = false;
var tick1 = false;
setImmediate(function() { tick0 = true; });
setImmediate(function() { tick1 = true; });
assertEquals(2, clock.getTimeoutsMade());
clock.tick(0);
assertTrue(tick0);
assertTrue(tick1);
clock.uninstall();
}
function testSetInterval() {
var clock = new goog.testing.MockClock(true);
var times = 0;
setInterval(function() { times++; }, 100);
clock.tick(500);
assertEquals(5, times);
clock.tick(100);
assertEquals(6, times);
clock.tick(100);
assertEquals(7, times);
clock.tick(50);
assertEquals(7, times);
clock.tick(50);
assertEquals(8, times);
clock.uninstall();
}
function testRequestAnimationFrame() {
goog.global.requestAnimationFrame = function() {
};
var clock = new goog.testing.MockClock(true);
var times = [];
var recFunc = goog.testing.recordFunction(function(now) {
times.push(now);
});
goog.global.requestAnimationFrame(recFunc);
clock.tick(50);
assertEquals(1, recFunc.getCallCount());
assertEquals(20, times[0]);
goog.global.requestAnimationFrame(recFunc);
clock.tick(100);
assertEquals(2, recFunc.getCallCount());
assertEquals(70, times[1]);
clock.uninstall();
}
function testClearTimeout() {
var clock = new goog.testing.MockClock(true);
var ran = false;
var c = setTimeout(function() { ran = true; }, 100);
clock.tick(50);
assertFalse(ran);
clearTimeout(c);
clock.tick(100);
assertFalse(ran);
clock.uninstall();
}
function testClearInterval() {
var clock = new goog.testing.MockClock(true);
var times = 0;
var c = setInterval(function() { times++; }, 100);
clock.tick(500);
assertEquals(5, times);
clock.tick(100);
assertEquals(6, times);
clock.tick(100);
clearInterval(c);
assertEquals(7, times);
clock.tick(50);
assertEquals(7, times);
clock.tick(50);
assertEquals(7, times);
clock.uninstall();
}
function testClearInterval2() {
// Tests that we can clear the interval from inside the function
var clock = new goog.testing.MockClock(true);
var times = 0;
var c = setInterval(function() {
times++;
if (times == 6) {
clearInterval(c);
}
}, 100);
clock.tick(500);
assertEquals(5, times);
clock.tick(100);
assertEquals(6, times);
clock.tick(100);
assertEquals(6, times);
clock.tick(50);
assertEquals(6, times);
clock.tick(50);
assertEquals(6, times);
clock.uninstall();
}
function testCancelRequestAnimationFrame() {
goog.global.requestAnimationFrame = function() {
};
goog.global.cancelRequestAnimationFrame = function() {
};
var clock = new goog.testing.MockClock(true);
var ran = false;
var c = goog.global.requestAnimationFrame(function() { ran = true; });
clock.tick(10);
assertFalse(ran);
goog.global.cancelRequestAnimationFrame(c);
clock.tick(20);
assertFalse(ran);
clock.uninstall();
}
function testMockGoogNow() {
assertNotEquals(0, goog.now());
var clock = new goog.testing.MockClock(true);
assertEquals(0, goog.now());
clock.tick(50);
assertEquals(50, goog.now());
clock.uninstall();
assertNotEquals(50, goog.now());
}
function testTimeoutDelay() {
var clock = new goog.testing.MockClock(true);
var m5 = false, m10 = false, m20 = false;
setTimeout(function() { m5 = true; }, 5);
setTimeout(function() { m10 = true; }, 10);
setTimeout(function() { m20 = true; }, 20);
// Fire 3ms early, so m5 fires at t=2
clock.setTimeoutDelay(-3);
clock.tick(1);
assertFalse(m5);
assertFalse(m10);
clock.tick(1);
assertTrue(m5);
assertFalse(m10);
// Fire 3ms late, so m10 fires at t=13
clock.setTimeoutDelay(3);
assertEquals(12, clock.tick(10));
assertEquals(12, clock.getCurrentTime());
assertFalse(m10);
clock.tick(1);
assertTrue(m10);
assertFalse(m20);
// Fire 10ms early, so m20 fires now, since it's after t=10
clock.setTimeoutDelay(-10);
assertFalse(m20);
assertEquals(14, clock.tick(1));
assertEquals(14, clock.getCurrentTime());
assertTrue(m20);
clock.uninstall();
}
function testTimerCallbackCanCreateIntermediateTimer() {
var clock = new goog.testing.MockClock(true);
var sequence = [];
// Create 3 timers: 1, 2, and 3. Timer 1 should fire at T=1, timer 2 at
// T=2, and timer 3 at T=3. The catch: Timer 2 is created by the
// callback within timer 0.
// Testing method: Create a simple string sequencing each timer and at
// what time it fired.
setTimeout(function() {
sequence.push('timer1 at T=' + goog.now());
setTimeout(function() {
sequence.push('timer2 at T=' + goog.now());
}, 1);
}, 1);
setTimeout(function() {
sequence.push('timer3 at T=' + goog.now());
}, 3);
clock.tick(4);
assertEquals(
'Each timer should fire in sequence at the correct time.',
'timer1 at T=1, timer2 at T=2, timer3 at T=3',
sequence.join(', '));
clock.uninstall();
}
function testCorrectArgumentsPassedToCallback() {
var clock = new goog.testing.MockClock(true);
var timeoutId;
var timeoutExecuted = false;
timeoutId = setTimeout(function(arg) {
assertEquals('"this" must be goog.global',
goog.global, this);
assertEquals('The timeout ID must be the first parameter',
timeoutId, arg);
assertEquals('Exactly one argument must be passed',
1, arguments.length);
timeoutExecuted = true;
}, 1);
clock.tick(4);
assertTrue('The timeout was not executed', timeoutExecuted);
clock.uninstall();
}
function testTickZero() {
var clock = new goog.testing.MockClock(true);
var calls = 0;
setTimeout(function() {
assertEquals('I need to be first', 0, calls);
calls++;
}, 0);
setTimeout(function() {
assertEquals('I need to be second', 1, calls);
calls++;
}, 0);
clock.tick(0);
assertEquals(2, calls);
setTimeout(function() {
assertEquals('I need to be third', 2, calls);
calls++;
}, 0);
clock.tick(0);
assertEquals(3, calls);
assertEquals('Time should still be zero', 0, goog.now());
clock.uninstall();
}
function testReset() {
var clock = new goog.testing.MockClock(true);
setTimeout(function() {
fail('Timeouts should be cleared after a reset');
}, 0);
clock.reset();
clock.tick(999999);
clock.uninstall();
}
function testQueueInsertionHelper() {
var queue = [];
function queueToString() {
var buffer = [];
for (var i = 0; i < queue.length; i++) {
buffer.push(queue[i].runAtMillis);
}
return buffer.join(',');
}
goog.testing.MockClock.insert_({runAtMillis: 2}, queue);
assertEquals('Only item',
'2', queueToString());
goog.testing.MockClock.insert_({runAtMillis: 4}, queue);
assertEquals('Biggest item',
'4,2', queueToString());
goog.testing.MockClock.insert_({runAtMillis: 5}, queue);
assertEquals('An even bigger item',
'5,4,2', queueToString());
goog.testing.MockClock.insert_({runAtMillis: 1}, queue);
assertEquals('Smallest item',
'5,4,2,1', queueToString());
goog.testing.MockClock.insert_({runAtMillis: 1, dup: true}, queue);
assertEquals('Duplicate smallest item',
'5,4,2,1,1', queueToString());
assertTrue('Duplicate item comes at a smaller index', queue[3].dup);
goog.testing.MockClock.insert_({runAtMillis: 3}, queue);
goog.testing.MockClock.insert_({runAtMillis: 3, dup: true}, queue);
assertEquals('Duplicate a middle item',
'5,4,3,3,2,1,1', queueToString());
assertTrue('Duplicate item comes at a smaller index', queue[2].dup);
}
function testIsTimeoutSet() {
var clock = new goog.testing.MockClock(true);
var timeoutKey = setTimeout(function() {}, 1);
assertTrue('Timeout ' + timeoutKey + ' should be set',
clock.isTimeoutSet(timeoutKey));
var nextTimeoutKey = timeoutKey + 1;
assertFalse('Timeout ' + nextTimeoutKey + ' should not be set',
clock.isTimeoutSet(nextTimeoutKey));
clearTimeout(timeoutKey);
assertFalse('Timeout ' + timeoutKey + ' should no longer be set',
clock.isTimeoutSet(timeoutKey));
var newTimeoutKey = setTimeout(function() {}, 1);
clock.tick(5);
assertFalse('Timeout ' + timeoutKey + ' should not be set',
clock.isTimeoutSet(timeoutKey));
assertTrue('Timeout ' + newTimeoutKey + ' should be set',
clock.isTimeoutSet(newTimeoutKey));
clock.uninstall();
}
function testBalksOnTimeoutsGreaterThanMaxInt() {
// Browsers have trouble with timeout greater than max int, so we
// want Mock Clock to fail if this happens.
var clock = new goog.testing.MockClock(true);
// Functions on window don't seem to be able to throw exceptions in
// IE6. Explicitly reading the property makes it work.
var setTimeout = window.setTimeout;
assertThrows('Timeouts > MAX_INT should fail',
function() {
setTimeout(goog.nullFunction, 2147483648);
});
assertThrows('Timeouts much greater than MAX_INT should fail',
function() {
setTimeout(goog.nullFunction, 2147483648 * 10);
});
clock.uninstall();
}
function testCorrectSetTimeoutIsRestored() {
var safe = goog.functions.error('should not have been called');
stubs.set(window, 'setTimeout', safe);
var clock = new goog.testing.MockClock(true);
assertNotEquals('setTimeout is replaced', safe, window.setTimeout);
clock.uninstall();
// NOTE: If this assertion proves to be flaky in IE, the string value of
// the two functions have to be compared as described in
// goog.testing.TestCase#finalize.
assertEquals('setTimeout is restored', safe, window.setTimeout);
}
function testMozRequestAnimationFrame() {
// Setting this function will indirectly tell the mock clock to mock it out.
stubs.set(window, 'mozRequestAnimationFrame', goog.nullFunction);
var clock = new goog.testing.MockClock(true);
var mozBeforePaint = goog.testing.recordFunction();
goog.events.listen(window, 'MozBeforePaint', mozBeforePaint);
window.mozRequestAnimationFrame(null);
assertEquals(0, mozBeforePaint.getCallCount());
clock.tick(goog.testing.MockClock.REQUEST_ANIMATION_FRAME_TIMEOUT);
assertEquals(1, mozBeforePaint.getCallCount());
clock.dispose();
}
function testClearBeforeSet() {
var clock = new goog.testing.MockClock(true);
var expectedId = 1;
window.clearTimeout(expectedId);
var fn = goog.testing.recordFunction();
var actualId = window.setTimeout(fn, 0);
assertEquals(
'In order for this test to work, we have to guess the ids in advance',
expectedId, actualId);
clock.tick(1);
assertEquals(1, fn.getCallCount());
clock.dispose();
}
function testNonFunctionArguments() {
var clock = new goog.testing.MockClock(true);
// Unlike normal setTimeout and friends, we only accept functions (not
// strings, not undefined, etc). Make sure that if we get a non-function, we
// fail early rather than on the next .tick() operation.
assertThrows('setTimeout with a non-function value should fail',
function() {
window.setTimeout(undefined, 0);
});
clock.tick(1);
assertThrows('setTimeout with a string should fail',
function() {
window.setTimeout('throw new Error("setTimeout string eval!");', 0);
});
clock.tick(1);
clock.dispose();
}
function testUnspecifiedTimeout() {
var clock = new goog.testing.MockClock(true);
var m0a = false, m0b = false, m10 = false;
setTimeout(function() { m0a = true; });
setTimeout(function() { m10 = true; }, 10);
assertEquals(2, clock.getTimeoutsMade());
assertFalse(m0a);
assertFalse(m0b);
assertFalse(m10);
assertEquals(0, clock.tick(0));
assertEquals(0, clock.getCurrentTime());
assertTrue(m0a);
assertFalse(m0b);
assertFalse(m10);
setTimeout(function() { m0b = true; });
assertEquals(3, clock.getTimeoutsMade());
assertEquals(0, clock.tick(0));
assertEquals(0, clock.getCurrentTime());
assertTrue(m0a);
assertTrue(m0b);
assertFalse(m10);
assertEquals(10, clock.tick(10));
assertEquals(10, clock.getCurrentTime());
assertTrue(m0a);
assertTrue(m0b);
assertTrue(m10);
clock.uninstall();
}
function testUnspecifiedInterval() {
var clock = new goog.testing.MockClock(true);
var times = 0;
var handle = setInterval(function() {
if (++times >= 5) {
clearInterval(handle);
}
});
clock.tick(0);
assertEquals(5, times);
clock.uninstall();
}
function testTickPromise() {
var clock = new goog.testing.MockClock(true);
var p = goog.Promise.resolve('foo');
assertEquals('foo', clock.tickPromise(p));
var rejected = goog.Promise.reject(new Error('failed'));
var e = assertThrows(function() {
clock.tickPromise(rejected);
});
assertEquals('failed', e.message);
var delayed = goog.Timer.promise(500, 'delayed');
e = assertThrows(function() {
clock.tickPromise(delayed);
});
assertEquals('Promise was expected to be resolved after mock clock tick.',
e.message);
assertEquals('delayed', clock.tickPromise(delayed, 500));
clock.dispose();
}
@@ -0,0 +1,220 @@
// 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 A MockControl holds a set of mocks for a particular test.
* It consolidates calls to $replay, $verify, and $tearDown, which simplifies
* the test and helps avoid omissions.
*
* You can create and control a mock:
* var mockFoo = mockControl.addMock(new MyMock(Foo));
*
* MockControl also exposes some convenience functions that create
* controlled mocks for common mocks: StrictMock, LooseMock,
* FunctionMock, MethodMock, and GlobalFunctionMock.
*
*/
goog.provide('goog.testing.MockControl');
goog.require('goog.array');
goog.require('goog.testing');
goog.require('goog.testing.LooseMock');
goog.require('goog.testing.StrictMock');
/**
* Controls a set of mocks. Controlled mocks are replayed, verified, and
* cleaned-up at the same time.
* @constructor
*/
goog.testing.MockControl = function() {
/**
* The list of mocks being controlled.
* @type {Array<goog.testing.MockInterface>}
* @private
*/
this.mocks_ = [];
};
/**
* Takes control of this mock.
* @param {goog.testing.MockInterface} mock Mock to be controlled.
* @return {goog.testing.MockInterface} The same mock passed in,
* for convenience.
*/
goog.testing.MockControl.prototype.addMock = function(mock) {
this.mocks_.push(mock);
return mock;
};
/**
* Calls replay on each controlled mock.
*/
goog.testing.MockControl.prototype.$replayAll = function() {
goog.array.forEach(this.mocks_, function(m) {
m.$replay();
});
};
/**
* Calls reset on each controlled mock.
*/
goog.testing.MockControl.prototype.$resetAll = function() {
goog.array.forEach(this.mocks_, function(m) {
m.$reset();
});
};
/**
* Calls verify on each controlled mock.
*/
goog.testing.MockControl.prototype.$verifyAll = function() {
goog.array.forEach(this.mocks_, function(m) {
m.$verify();
});
};
/**
* Calls tearDown on each controlled mock, if necesssary.
*/
goog.testing.MockControl.prototype.$tearDown = function() {
goog.array.forEach(this.mocks_, function(m) {
// $tearDown if defined.
if (m.$tearDown) {
m.$tearDown();
}
// TODO(user): Somehow determine if verifyAll should have been called
// but was not.
});
};
/**
* Creates a controlled StrictMock. Passes its arguments through to the
* StrictMock constructor.
* @param {Object|Function} objectToMock The object that should be mocked, or
* the constructor of an object to mock.
* @param {boolean=} opt_mockStaticMethods An optional argument denoting that
* a mock should be constructed from the static functions of a class.
* @param {boolean=} opt_createProxy An optional argument denoting that
* a proxy for the target mock should be created.
* @return {!goog.testing.StrictMock} The mock object.
*/
goog.testing.MockControl.prototype.createStrictMock = function(
objectToMock, opt_mockStaticMethods, opt_createProxy) {
var m = new goog.testing.StrictMock(objectToMock, opt_mockStaticMethods,
opt_createProxy);
this.addMock(m);
return m;
};
/**
* Creates a controlled LooseMock. Passes its arguments through to the
* LooseMock constructor.
* @param {Object|Function} objectToMock The object that should be mocked, or
* the constructor of an object to mock.
* @param {boolean=} opt_ignoreUnexpectedCalls Whether to ignore unexpected
* calls.
* @param {boolean=} opt_mockStaticMethods An optional argument denoting that
* a mock should be constructed from the static functions of a class.
* @param {boolean=} opt_createProxy An optional argument denoting that
* a proxy for the target mock should be created.
* @return {!goog.testing.LooseMock} The mock object.
*/
goog.testing.MockControl.prototype.createLooseMock = function(
objectToMock, opt_ignoreUnexpectedCalls,
opt_mockStaticMethods, opt_createProxy) {
var m = new goog.testing.LooseMock(objectToMock, opt_ignoreUnexpectedCalls,
opt_mockStaticMethods, opt_createProxy);
this.addMock(m);
return m;
};
/**
* Creates a controlled FunctionMock. Passes its arguments through to the
* FunctionMock constructor.
* @param {string=} opt_functionName The optional name of the function to mock
* set to '[anonymous mocked function]' if not passed in.
* @param {number=} opt_strictness One of goog.testing.Mock.LOOSE or
* goog.testing.Mock.STRICT. The default is STRICT.
* @return {goog.testing.MockInterface} The mocked function.
*/
goog.testing.MockControl.prototype.createFunctionMock = function(
opt_functionName, opt_strictness) {
var m = goog.testing.createFunctionMock(opt_functionName, opt_strictness);
this.addMock(m);
return m;
};
/**
* Creates a controlled MethodMock. Passes its arguments through to the
* MethodMock constructor.
* @param {Object} scope The scope of the method to be mocked out.
* @param {string} functionName The name of the function we're going to mock.
* @param {number=} opt_strictness One of goog.testing.Mock.LOOSE or
* goog.testing.Mock.STRICT. The default is STRICT.
* @return {!goog.testing.MockInterface} The mocked method.
*/
goog.testing.MockControl.prototype.createMethodMock = function(
scope, functionName, opt_strictness) {
var m = goog.testing.createMethodMock(scope, functionName, opt_strictness);
this.addMock(m);
return m;
};
/**
* Creates a controlled MethodMock for a constructor. Passes its arguments
* through to the MethodMock constructor. See
* {@link goog.testing.createConstructorMock} for details.
* @param {Object} scope The scope of the constructor to be mocked out.
* @param {string} constructorName The name of the function we're going to mock.
* @param {number=} opt_strictness One of goog.testing.Mock.LOOSE or
* goog.testing.Mock.STRICT. The default is STRICT.
* @return {!goog.testing.MockInterface} The mocked method.
*/
goog.testing.MockControl.prototype.createConstructorMock = function(
scope, constructorName, opt_strictness) {
var m = goog.testing.createConstructorMock(scope, constructorName,
opt_strictness);
this.addMock(m);
return m;
};
/**
* Creates a controlled GlobalFunctionMock. Passes its arguments through to the
* GlobalFunctionMock constructor.
* @param {string} functionName The name of the function we're going to mock.
* @param {number=} opt_strictness One of goog.testing.Mock.LOOSE or
* goog.testing.Mock.STRICT. The default is STRICT.
* @return {goog.testing.MockInterface} The mocked function.
*/
goog.testing.MockControl.prototype.createGlobalFunctionMock = function(
functionName, opt_strictness) {
var m = goog.testing.createGlobalFunctionMock(functionName, opt_strictness);
this.addMock(m);
return m;
};
@@ -0,0 +1,25 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2008 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<!--
-->
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>
Closure Unit Tests - goog.testing.MockControl
</title>
<script src="../base.js">
</script>
<script>
goog.require('goog.testing.MockControlTest');
</script>
</head>
<body>
</body>
</html>
@@ -0,0 +1,121 @@
// 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.
goog.provide('goog.testing.MockControlTest');
goog.setTestOnly('goog.testing.MockControlTest');
goog.require('goog.testing.Mock');
goog.require('goog.testing.MockControl');
goog.require('goog.testing.jsunit');
// Emulate the behavior of a mock.
function MockMock() {
this.replayCalled = false;
this.resetCalled = false;
this.verifyCalled = false;
this.tearDownCalled = false;
}
MockMock.prototype.$replay = function() {
this.replayCalled = true;
};
MockMock.prototype.$reset = function() {
this.resetCalled = true;
};
MockMock.prototype.$verify = function() {
this.verifyCalled = true;
};
function setUp() {
var mock = new goog.testing.Mock(MockMock);
}
function testAdd() {
var mockMock = new MockMock();
var control = new goog.testing.MockControl();
assertEquals(mockMock, control.addMock(mockMock));
}
function testReplayAll() {
var mockMock1 = new MockMock();
var mockMock2 = new MockMock();
var mockMockExcluded = new MockMock();
var control = new goog.testing.MockControl();
control.addMock(mockMock1);
control.addMock(mockMock2);
control.$replayAll();
assertTrue(mockMock1.replayCalled);
assertTrue(mockMock2.replayCalled);
assertFalse(mockMockExcluded.replayCalled);
}
function testResetAll() {
var mockMock1 = new MockMock();
var mockMock2 = new MockMock();
var mockMockExcluded = new MockMock();
var control = new goog.testing.MockControl();
control.addMock(mockMock1);
control.addMock(mockMock2);
control.$resetAll();
assertTrue(mockMock1.resetCalled);
assertTrue(mockMock2.resetCalled);
assertFalse(mockMockExcluded.resetCalled);
}
function testVerifyAll() {
var mockMock1 = new MockMock();
var mockMock2 = new MockMock();
var mockMockExcluded = new MockMock();
var control = new goog.testing.MockControl();
control.addMock(mockMock1);
control.addMock(mockMock2);
control.$verifyAll();
assertTrue(mockMock1.verifyCalled);
assertTrue(mockMock2.verifyCalled);
assertFalse(mockMockExcluded.verifyCalled);
}
function testTearDownAll() {
var mockMock1 = new MockMock();
var mockMock2 = new MockMock();
var mockMockExcluded = new MockMock();
// $tearDown is optional.
mockMock2.$tearDown = function() {
this.tearDownCalled = true;
};
mockMockExcluded.$tearDown = function() {
this.tearDownCalled = true;
};
var control = new goog.testing.MockControl();
control.addMock(mockMock1);
control.addMock(mockMock2);
control.$tearDown();
// mockMock2 has a tearDown method and is in the control.
assertTrue(mockMock2.tearDownCalled);
assertFalse(mockMock1.tearDownCalled);
assertFalse(mockMockExcluded.tearDownCalled);
}
@@ -0,0 +1,45 @@
// Copyright 2010 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 An interface that all mocks should share.
* @author nicksantos@google.com (Nick Santos)
*/
goog.provide('goog.testing.MockInterface');
/** @interface */
goog.testing.MockInterface = function() {};
/**
* Write down all the expected functions that have been called on the
* mock so far. From here on out, future function calls will be
* compared against this list.
*/
goog.testing.MockInterface.prototype.$replay = function() {};
/**
* Reset the mock.
*/
goog.testing.MockInterface.prototype.$reset = function() {};
/**
* Assert that the expected function calls match the actual calls.
*/
goog.testing.MockInterface.prototype.$verify = function() {};
@@ -0,0 +1,400 @@
// 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 Matchers to be used with the mock utilities. They allow for
* flexible matching by type. Custom matchers can be created by passing a
* matcher function into an ArgumentMatcher instance.
*
* For examples, please see the unit test.
*
*/
goog.provide('goog.testing.mockmatchers');
goog.provide('goog.testing.mockmatchers.ArgumentMatcher');
goog.provide('goog.testing.mockmatchers.IgnoreArgument');
goog.provide('goog.testing.mockmatchers.InstanceOf');
goog.provide('goog.testing.mockmatchers.ObjectEquals');
goog.provide('goog.testing.mockmatchers.RegexpMatch');
goog.provide('goog.testing.mockmatchers.SaveArgument');
goog.provide('goog.testing.mockmatchers.TypeOf');
goog.require('goog.array');
goog.require('goog.dom');
goog.require('goog.testing.asserts');
/**
* A simple interface for executing argument matching. A match in this case is
* testing to see if a supplied object fits a given criteria. True is returned
* if the given criteria is met.
* @param {Function=} opt_matchFn A function that evaluates a given argument
* and returns true if it meets a given criteria.
* @param {?string=} opt_matchName The name expressing intent as part of
* an error message for when a match fails.
* @constructor
*/
goog.testing.mockmatchers.ArgumentMatcher =
function(opt_matchFn, opt_matchName) {
/**
* A function that evaluates a given argument and returns true if it meets a
* given criteria.
* @type {Function}
* @private
*/
this.matchFn_ = opt_matchFn || null;
/**
* A string indicating the match intent (e.g. isBoolean or isString).
* @type {?string}
* @private
*/
this.matchName_ = opt_matchName || null;
};
/**
* A function that takes a match argument and an optional MockExpectation
* which (if provided) will get error information and returns whether or
* not it matches.
* @param {*} toVerify The argument that should be verified.
* @param {goog.testing.MockExpectation?=} opt_expectation The expectation
* for this match.
* @return {boolean} Whether or not a given argument passes verification.
*/
goog.testing.mockmatchers.ArgumentMatcher.prototype.matches =
function(toVerify, opt_expectation) {
if (this.matchFn_) {
var isamatch = this.matchFn_(toVerify);
if (!isamatch && opt_expectation) {
if (this.matchName_) {
opt_expectation.addErrorMessage('Expected: ' +
this.matchName_ + ' but was: ' + _displayStringForValue(toVerify));
} else {
opt_expectation.addErrorMessage('Expected: missing mockmatcher' +
' description but was: ' +
_displayStringForValue(toVerify));
}
}
return isamatch;
} else {
throw Error('No match function defined for this mock matcher');
}
};
/**
* A matcher that verifies that an argument is an instance of a given class.
* @param {Function} ctor The class that will be used for verification.
* @constructor
* @extends {goog.testing.mockmatchers.ArgumentMatcher}
* @final
*/
goog.testing.mockmatchers.InstanceOf = function(ctor) {
goog.testing.mockmatchers.ArgumentMatcher.call(this,
function(obj) {
return obj instanceof ctor;
// NOTE: Browser differences on ctor.toString() output
// make using that here problematic. So for now, just let
// people know the instanceOf() failed without providing
// browser specific details...
}, 'instanceOf()');
};
goog.inherits(goog.testing.mockmatchers.InstanceOf,
goog.testing.mockmatchers.ArgumentMatcher);
/**
* A matcher that verifies that an argument is of a given type (e.g. "object").
* @param {string} type The type that a given argument must have.
* @constructor
* @extends {goog.testing.mockmatchers.ArgumentMatcher}
* @final
*/
goog.testing.mockmatchers.TypeOf = function(type) {
goog.testing.mockmatchers.ArgumentMatcher.call(this,
function(obj) {
return goog.typeOf(obj) == type;
}, 'typeOf(' + type + ')');
};
goog.inherits(goog.testing.mockmatchers.TypeOf,
goog.testing.mockmatchers.ArgumentMatcher);
/**
* A matcher that verifies that an argument matches a given RegExp.
* @param {RegExp} regexp The regular expression that the argument must match.
* @constructor
* @extends {goog.testing.mockmatchers.ArgumentMatcher}
* @final
*/
goog.testing.mockmatchers.RegexpMatch = function(regexp) {
goog.testing.mockmatchers.ArgumentMatcher.call(this,
function(str) {
return regexp.test(str);
}, 'match(' + regexp + ')');
};
goog.inherits(goog.testing.mockmatchers.RegexpMatch,
goog.testing.mockmatchers.ArgumentMatcher);
/**
* A matcher that always returns true. It is useful when the user does not care
* for some arguments.
* For example: mockFunction('username', 'password', IgnoreArgument);
* @constructor
* @extends {goog.testing.mockmatchers.ArgumentMatcher}
* @final
*/
goog.testing.mockmatchers.IgnoreArgument = function() {
goog.testing.mockmatchers.ArgumentMatcher.call(this,
function() {
return true;
}, 'true');
};
goog.inherits(goog.testing.mockmatchers.IgnoreArgument,
goog.testing.mockmatchers.ArgumentMatcher);
/**
* A matcher that verifies that the argument is an object that equals the given
* expected object, using a deep comparison.
* @param {Object} expectedObject An object to match against when
* verifying the argument.
* @constructor
* @extends {goog.testing.mockmatchers.ArgumentMatcher}
*/
goog.testing.mockmatchers.ObjectEquals = function(expectedObject) {
goog.testing.mockmatchers.ArgumentMatcher.call(this,
function(matchObject) {
assertObjectEquals('Expected equal objects', expectedObject,
matchObject);
return true;
}, 'objectEquals(' + expectedObject + ')');
};
goog.inherits(goog.testing.mockmatchers.ObjectEquals,
goog.testing.mockmatchers.ArgumentMatcher);
/** @override */
goog.testing.mockmatchers.ObjectEquals.prototype.matches =
function(toVerify, opt_expectation) {
// Override the default matches implementation to capture the exception thrown
// by assertObjectEquals (if any) and add that message to the expectation.
try {
return goog.testing.mockmatchers.ObjectEquals.superClass_.matches.call(
this, toVerify, opt_expectation);
} catch (e) {
if (opt_expectation) {
opt_expectation.addErrorMessage(e.message);
}
return false;
}
};
/**
* A matcher that saves the argument that it is verifying so that your unit test
* can perform extra tests with this argument later. For example, if the
* argument is a callback method, the unit test can then later call this
* callback to test the asynchronous portion of the call.
* @param {goog.testing.mockmatchers.ArgumentMatcher|Function=} opt_matcher
* Argument matcher or matching function that will be used to validate the
* argument. By default, argument will always be valid.
* @param {?string=} opt_matchName The name expressing intent as part of
* an error message for when a match fails.
* @constructor
* @extends {goog.testing.mockmatchers.ArgumentMatcher}
* @final
*/
goog.testing.mockmatchers.SaveArgument = function(opt_matcher, opt_matchName) {
goog.testing.mockmatchers.ArgumentMatcher.call(
this, /** @type {Function} */ (opt_matcher), opt_matchName);
if (opt_matcher instanceof goog.testing.mockmatchers.ArgumentMatcher) {
/**
* Delegate match requests to this matcher.
* @type {goog.testing.mockmatchers.ArgumentMatcher}
* @private
*/
this.delegateMatcher_ = opt_matcher;
} else if (!opt_matcher) {
this.delegateMatcher_ = goog.testing.mockmatchers.ignoreArgument;
}
};
goog.inherits(goog.testing.mockmatchers.SaveArgument,
goog.testing.mockmatchers.ArgumentMatcher);
/** @override */
goog.testing.mockmatchers.SaveArgument.prototype.matches = function(
toVerify, opt_expectation) {
this.arg = toVerify;
if (this.delegateMatcher_) {
return this.delegateMatcher_.matches(toVerify, opt_expectation);
}
return goog.testing.mockmatchers.SaveArgument.superClass_.matches.call(
this, toVerify, opt_expectation);
};
/**
* Saved argument that was verified.
* @type {*}
*/
goog.testing.mockmatchers.SaveArgument.prototype.arg;
/**
* An instance of the IgnoreArgument matcher. Returns true for all matches.
* @type {goog.testing.mockmatchers.IgnoreArgument}
*/
goog.testing.mockmatchers.ignoreArgument =
new goog.testing.mockmatchers.IgnoreArgument();
/**
* A matcher that verifies that an argument is an array.
* @type {goog.testing.mockmatchers.ArgumentMatcher}
*/
goog.testing.mockmatchers.isArray =
new goog.testing.mockmatchers.ArgumentMatcher(goog.isArray,
'isArray');
/**
* A matcher that verifies that an argument is a array-like. A NodeList is an
* example of a collection that is very close to an array.
* @type {goog.testing.mockmatchers.ArgumentMatcher}
*/
goog.testing.mockmatchers.isArrayLike =
new goog.testing.mockmatchers.ArgumentMatcher(goog.isArrayLike,
'isArrayLike');
/**
* A matcher that verifies that an argument is a date-like.
* @type {goog.testing.mockmatchers.ArgumentMatcher}
*/
goog.testing.mockmatchers.isDateLike =
new goog.testing.mockmatchers.ArgumentMatcher(goog.isDateLike,
'isDateLike');
/**
* A matcher that verifies that an argument is a string.
* @type {goog.testing.mockmatchers.ArgumentMatcher}
*/
goog.testing.mockmatchers.isString =
new goog.testing.mockmatchers.ArgumentMatcher(goog.isString,
'isString');
/**
* A matcher that verifies that an argument is a boolean.
* @type {goog.testing.mockmatchers.ArgumentMatcher}
*/
goog.testing.mockmatchers.isBoolean =
new goog.testing.mockmatchers.ArgumentMatcher(goog.isBoolean,
'isBoolean');
/**
* A matcher that verifies that an argument is a number.
* @type {goog.testing.mockmatchers.ArgumentMatcher}
*/
goog.testing.mockmatchers.isNumber =
new goog.testing.mockmatchers.ArgumentMatcher(goog.isNumber,
'isNumber');
/**
* A matcher that verifies that an argument is a function.
* @type {goog.testing.mockmatchers.ArgumentMatcher}
*/
goog.testing.mockmatchers.isFunction =
new goog.testing.mockmatchers.ArgumentMatcher(goog.isFunction,
'isFunction');
/**
* A matcher that verifies that an argument is an object.
* @type {goog.testing.mockmatchers.ArgumentMatcher}
*/
goog.testing.mockmatchers.isObject =
new goog.testing.mockmatchers.ArgumentMatcher(goog.isObject,
'isObject');
/**
* A matcher that verifies that an argument is like a DOM node.
* @type {goog.testing.mockmatchers.ArgumentMatcher}
*/
goog.testing.mockmatchers.isNodeLike =
new goog.testing.mockmatchers.ArgumentMatcher(goog.dom.isNodeLike,
'isNodeLike');
/**
* A function that checks to see if an array matches a given set of
* expectations. The expectations array can be a mix of ArgumentMatcher
* implementations and values. True will be returned if values are identical or
* if a matcher returns a positive result.
* @param {Array<?>} expectedArr An array of expectations which can be either
* values to check for equality or ArgumentMatchers.
* @param {Array<?>} arr The array to match.
* @param {goog.testing.MockExpectation?=} opt_expectation The expectation
* for this match.
* @return {boolean} Whether or not the given array matches the expectations.
*/
goog.testing.mockmatchers.flexibleArrayMatcher =
function(expectedArr, arr, opt_expectation) {
return goog.array.equals(expectedArr, arr, function(a, b) {
var errCount = 0;
if (opt_expectation) {
errCount = opt_expectation.getErrorMessageCount();
}
var isamatch = a === b ||
a instanceof goog.testing.mockmatchers.ArgumentMatcher &&
a.matches(b, opt_expectation);
var failureMessage = null;
if (!isamatch) {
failureMessage = goog.testing.asserts.findDifferences(a, b);
isamatch = !failureMessage;
}
if (!isamatch && opt_expectation) {
// If the error count changed, the match sent out an error
// message. If the error count has not changed, then
// we need to send out an error message...
if (errCount == opt_expectation.getErrorMessageCount()) {
// Use the _displayStringForValue() from assert.js
// for consistency...
if (!failureMessage) {
failureMessage = 'Expected: ' + _displayStringForValue(a) +
' but was: ' + _displayStringForValue(b);
}
opt_expectation.addErrorMessage(failureMessage);
}
}
return isamatch;
});
};
@@ -0,0 +1,28 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2008 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<!--
@author earmbrust@google.com (Erick Armbrust)
-->
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>
Closure Unit Tests - goog.testing.mockmatchers
</title>
<script src="../base.js">
</script>
<script>
goog.require('goog.testing.mockmatchersTest');
</script>
</head>
<body>
<div id="someDiv">
</div>
</body>
</html>
@@ -0,0 +1,378 @@
// 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.
goog.provide('goog.testing.mockmatchersTest');
goog.setTestOnly('goog.testing.mockmatchersTest');
goog.require('goog.dom');
goog.require('goog.testing.jsunit');
goog.require('goog.testing.mockmatchers');
goog.require('goog.testing.mockmatchers.ArgumentMatcher');
// A local reference to the mockmatchers namespace.
var matchers = goog.testing.mockmatchers;
// Simple classes to test the InstanceOf matcher.
var foo = function() {};
var bar = function() {};
// Simple class to test adding error messages to
// MockExpectation objects
function MockMock() {
this.errorMessages = [];
}
var mockExpect = null;
MockMock.prototype.addErrorMessage = function(msg) {
this.errorMessages.push(msg);
};
MockMock.prototype.getErrorMessageCount = function() {
return this.errorMessages.length;
};
function setUp() {
mockExpect = new MockMock();
}
function testNoMatchName() {
// A matcher that does not fill in the match name
var matcher = new goog.testing.mockmatchers.ArgumentMatcher(goog.isString);
// Make sure the lack of match name doesn't affect the ability
// to return True/False
assertTrue(matcher.matches('hello'));
assertFalse(matcher.matches(123));
// Make sure we handle the lack of a match name
assertFalse(matcher.matches(456, mockExpect));
assertEquals(1, mockExpect.errorMessages.length);
assertEquals('Expected: missing mockmatcher description ' +
'but was: <456> (Number)', mockExpect.errorMessages[0]);
}
function testInstanceOf() {
var matcher = new matchers.InstanceOf(foo);
assertTrue(matcher.matches(new foo()));
assertFalse(matcher.matches(new bar()));
assertFalse(matcher.matches(new bar(), mockExpect));
assertEquals(1, mockExpect.errorMessages.length);
assertEquals('Expected: instanceOf() ' +
'but was: <[object Object]> (Object)',
mockExpect.errorMessages[0]);
}
function testTypeOf() {
var matcher = new matchers.TypeOf('number');
assertTrue(matcher.matches(1));
assertTrue(matcher.matches(2));
assertFalse(matcher.matches('test'));
assertFalse(matcher.matches(true, mockExpect));
assertEquals(1, mockExpect.errorMessages.length);
assertEquals('Expected: typeOf(number) but was: <true> (Boolean)',
mockExpect.errorMessages[0]);
}
function testRegexpMatch() {
var matcher = new matchers.RegexpMatch(/^cho[dtp]/);
assertTrue(matcher.matches('chodhop'));
assertTrue(matcher.matches('chopper'));
assertFalse(matcher.matches('chocolate'));
assertFalse(matcher.matches(null));
assertFalse(matcher.matches('an anger', mockExpect));
assertEquals(1, mockExpect.errorMessages.length);
assertEquals('Expected: match(/^cho[dtp]/) but was: <an anger> (String)',
mockExpect.errorMessages[0]);
}
function testObjectEquals() {
// Test a simple match.
var simpleMatcher = new matchers.ObjectEquals({name: 'Bob', age: 42});
assertTrue(simpleMatcher.matches({name: 'Bob', age: 42}, mockExpect));
assertEquals(0, mockExpect.getErrorMessageCount());
expectObjectEqualsFailure(simpleMatcher, {name: 'Bill', age: 42},
'name: Expected <Bob> (String) but was <Bill> (String)');
expectObjectEqualsFailure(simpleMatcher, {name: 'Bob', age: 21},
'age: Expected <42> (Number) but was <21> (Number)');
expectObjectEqualsFailure(simpleMatcher, {name: 'Bob'},
'property age not present in actual Object');
expectObjectEqualsFailure(simpleMatcher,
{name: 'Bob', age: 42, country: 'USA'},
'property country not present in expected Object');
// Multiple mismatches should include multiple messages.
expectObjectEqualsFailure(simpleMatcher, {name: 'Jim', age: 36},
'name: Expected <Bob> (String) but was <Jim> (String)\n' +
' age: Expected <42> (Number) but was <36> (Number)');
}
function testComplexObjectEquals() {
var complexMatcher = new matchers.ObjectEquals(
{a: 'foo', b: 2, c: ['bar', 3], d: {sub1: 'baz', sub2: -1}});
assertTrue(complexMatcher.matches(
{a: 'foo', b: 2, c: ['bar', 3], d: {sub1: 'baz', sub2: -1}}));
expectObjectEqualsFailure(complexMatcher,
{a: 'foo', b: 2, c: ['bar', 3], d: {sub1: 'zap', sub2: -1}},
'sub1: Expected <baz> (String) but was <zap> (String)');
expectObjectEqualsFailure(complexMatcher,
{a: 'foo', b: 2, c: ['bar', 6], d: {sub1: 'baz', sub2: -1}},
'c[1]: Expected <3> (Number) but was <6> (Number)');
}
function testSaveArgument() {
var saveMatcher = new matchers.SaveArgument();
assertTrue(saveMatcher.matches(42));
assertEquals(42, saveMatcher.arg);
saveMatcher = new matchers.SaveArgument(goog.isString);
assertTrue(saveMatcher.matches('test'));
assertEquals('test', saveMatcher.arg);
assertFalse(saveMatcher.matches(17));
assertEquals(17, saveMatcher.arg);
saveMatcher = new matchers.SaveArgument(new matchers.ObjectEquals({
value: 'value'
}));
assertTrue(saveMatcher.matches({ value: 'value' }));
assertEquals('value', saveMatcher.arg.value);
assertFalse(saveMatcher.matches('test'));
assertEquals('test', saveMatcher.arg);
}
function testIsArray() {
assertTrue(matchers.isArray.matches([]));
assertTrue(matchers.isArray.matches(new Array()));
assertFalse(matchers.isArray.matches('test'));
assertFalse(matchers.isArray.matches({}, mockExpect));
assertEquals(1, mockExpect.errorMessages.length);
assertEquals('Expected: isArray but was: <[object Object]> (Object)',
mockExpect.errorMessages[0]);
}
function testIsArrayLike() {
var nodeList = (function() {
var div = document.createElement('div');
div.appendChild(document.createElement('p'));
div.appendChild(document.createElement('p'));
return div.getElementsByTagName('div');
})();
assertTrue(matchers.isArrayLike.matches([]));
assertTrue(matchers.isArrayLike.matches(new Array()));
assertTrue(matchers.isArrayLike.matches(nodeList));
assertFalse(matchers.isArrayLike.matches('test'));
assertFalse(matchers.isArrayLike.matches(3, mockExpect));
assertEquals(1, mockExpect.errorMessages.length);
assertEquals('Expected: isArrayLike but was: <3> (Number)',
mockExpect.errorMessages[0]);
}
function testIsDateLike() {
assertTrue(matchers.isDateLike.matches(new Date()));
assertFalse(matchers.isDateLike.matches('test'));
assertFalse(matchers.isDateLike.matches('test', mockExpect));
assertEquals(1, mockExpect.errorMessages.length);
assertEquals('Expected: isDateLike but was: <test> (String)',
mockExpect.errorMessages[0]);
}
function testIsString() {
assertTrue(matchers.isString.matches('a'));
assertTrue(matchers.isString.matches('b'));
assertFalse(matchers.isString.matches(null));
assertFalse(matchers.isString.matches(null, mockExpect));
assertEquals(1, mockExpect.errorMessages.length);
assertEquals('Expected: isString but was: <null>',
mockExpect.errorMessages[0]);
}
function testIsBoolean() {
assertTrue(matchers.isBoolean.matches(true));
assertTrue(matchers.isBoolean.matches(false));
assertFalse(matchers.isBoolean.matches(null));
assertFalse(matchers.isBoolean.matches([], mockExpect));
assertEquals(1, mockExpect.errorMessages.length);
assertEquals('Expected: isBoolean but was: <> (Array)',
mockExpect.errorMessages[0]);
}
function testIsNumber() {
assertTrue(matchers.isNumber.matches(-1));
assertTrue(matchers.isNumber.matches(1));
assertTrue(matchers.isNumber.matches(1.25));
assertFalse(matchers.isNumber.matches(null));
assertFalse(matchers.isNumber.matches('hello', mockExpect));
assertEquals(1, mockExpect.errorMessages.length);
assertEquals('Expected: isNumber but was: <hello> (String)',
mockExpect.errorMessages[0]);
}
function testIsFunction() {
assertTrue(matchers.isFunction.matches(function() {}));
assertFalse(matchers.isFunction.matches('test'));
assertFalse(matchers.isFunction.matches({}, mockExpect));
assertEquals(1, mockExpect.errorMessages.length);
assertEquals('Expected: isFunction but was: <[object Object]> (Object)',
mockExpect.errorMessages[0]);
}
function testIsObject() {
assertTrue(matchers.isObject.matches({}));
assertTrue(matchers.isObject.matches(new Object()));
assertTrue(matchers.isObject.matches(new function() {}));
assertTrue(matchers.isObject.matches([]));
assertTrue(matchers.isObject.matches(new Array()));
assertTrue(matchers.isObject.matches(function() {}));
assertFalse(matchers.isObject.matches(null));
assertFalse(matchers.isObject.matches(1234, mockExpect));
assertEquals(1, mockExpect.errorMessages.length);
assertEquals('Expected: isObject but was: <1234> (Number)',
mockExpect.errorMessages[0]);
}
function testIsNodeLike() {
assertFalse(matchers.isNodeLike.matches({}));
assertFalse(matchers.isNodeLike.matches(1));
assertFalse(matchers.isNodeLike.matches(function() {}));
assertFalse(matchers.isNodeLike.matches(false));
assertTrue(matchers.isNodeLike.matches(document.body));
assertTrue(matchers.isNodeLike.matches(goog.dom.getElement('someDiv')));
assertFalse(matchers.isNodeLike.matches('test', mockExpect));
assertEquals(1, mockExpect.errorMessages.length);
assertEquals('Expected: isNodeLike but was: <test> (String)',
mockExpect.errorMessages[0]);
}
function testIgnoreArgumentsMatcher() {
// ignoreArgument always returns true:
assertTrue(matchers.ignoreArgument.matches());
assertTrue(matchers.ignoreArgument.matches(356));
assertTrue(matchers.ignoreArgument.matches('str'));
assertTrue(matchers.ignoreArgument.matches(['array', 123, false]));
assertTrue(matchers.ignoreArgument.matches({'map': 1, key2: 'value2'}));
}
function testFlexibleArrayMatcher() {
// Test that basic lists are verified properly.
var a1 = [1, 'test'];
var a2 = [1, 'test'];
var a3 = [1, 'test', 'extra'];
assertTrue(matchers.flexibleArrayMatcher(a1, a2));
assertFalse(matchers.flexibleArrayMatcher(a1, a3));
// Test that basic lists with basic class instances are verified properly.
var instance = new foo();
a1 = [1, 'test', instance];
a2 = [1, 'test', instance];
a3 = [1, 'test', new foo()];
assertTrue(matchers.flexibleArrayMatcher(a1, a2));
assertTrue(matchers.flexibleArrayMatcher(a1, a3));
// Create an argument verifier that returns a consistent value.
var verifyValue = true;
var argVerifier = function() {};
goog.inherits(argVerifier, matchers.ArgumentMatcher);
argVerifier.prototype.matches = function(arg) {
return verifyValue;
};
// Test that the arguments are always verified when the verifier returns
// true.
a1 = [1, 'test', new argVerifier()];
a2 = [1, 'test', 'anything'];
a3 = [1, 'test', 12345];
assertTrue(matchers.flexibleArrayMatcher(a1, a2));
assertTrue(matchers.flexibleArrayMatcher(a1, a3));
// Now test the case when then verifier returns false.
verifyValue = false;
assertFalse(matchers.flexibleArrayMatcher(a1, a2));
assertFalse(matchers.flexibleArrayMatcher(a1, a3));
// And test we report errors back up via the opt_expectation
assertFalse(matchers.flexibleArrayMatcher(a2, a3, mockExpect));
assertEquals(1, mockExpect.errorMessages.length);
assertEquals(
'Expected <anything> (String) but was <12345> (Number)\n' +
' Expected <anything> (String) but was <12345> (Number)',
mockExpect.errorMessages[0]);
// And test we report errors found via the matcher
a1 = [1, goog.testing.mockmatchers.isString];
a2 = [1, 'test string'];
a3 = [1, null];
assertTrue(matchers.flexibleArrayMatcher(a1, a2, mockExpect));
assertFalse(matchers.flexibleArrayMatcher(a1, a3, mockExpect));
// Old error is still there
assertEquals(2, mockExpect.errorMessages.length);
assertEquals(
'Expected <anything> (String) but was <12345> (Number)\n' +
' Expected <anything> (String) but was <12345> (Number)',
mockExpect.errorMessages[0]);
// plus the new error...
assertEquals('Expected: isString but was: <null>',
mockExpect.errorMessages[1]);
}
/**
* Utility method for checking for an ObjectEquals match failure. Checks that
* the expected error message was included in the error messages appended to
* the expectation object.
* @param {goog.testing.mockmatchers.ArgumentMatcher.ObjectEquals} matcher
* The matcher to test against.
* @param {Object} matchObject The object to compare.
* @param {string=} opt_errorMsg The deep object comparison failure message
* to check for.
*/
function expectObjectEqualsFailure(matcher, matchObject, opt_errorMsg) {
mockExpect.errorMessages = [];
assertFalse(matcher.matches(matchObject, mockExpect));
assertNotEquals(0, mockExpect.getErrorMessageCount());
if (opt_errorMsg) {
assertContains(opt_errorMsg, mockExpect.errorMessages[0]);
}
}

Some files were not shown because too many files have changed in this diff Show More