Adding float-no-zero branch hosted build

This commit is contained in:
ahocevar
2014-03-07 10:55:12 +01:00
parent 84cad42f6d
commit bd9092199b
1664 changed files with 731463 additions and 0 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,174 @@
// 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
*/
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');
}
};

View File

@@ -0,0 +1,824 @@
// 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.
*
* 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.TestCase.Test');
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
*/
goog.testing.AsyncTestCase.ControlBreakingException = function(opt_message) {
/**
* The exception message.
* @type {string}
*/
this.message = opt_message || '';
};
/**
* 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;
/**
* 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.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);
}
};
/**
* 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.continueTesting();
} 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.MAX_RUN_TIME &&
!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.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));
};

View File

@@ -0,0 +1,88 @@
// 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.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 testSource = goog.testing.TestCase.getGlobals(prefix);
var benchmarks = {};
var names = [];
for (var name in testSource) {
try {
var 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.
}
if ((new RegExp('^' + prefix)).test(name) && 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_();
};

View File

@@ -0,0 +1,689 @@
// 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.TestCase.Test');
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}
*/
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}
* @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}
*/
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}
*/
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_;
}
};

View File

@@ -0,0 +1,155 @@
// 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.async.Deferred');
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);
};

View File

@@ -0,0 +1,608 @@
// 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.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.classes');
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');
/**
* A unique object to use as an end tag marker.
* @type {Object}
* @private
*/
goog.testing.dom.END_TAG_MARKER_ = {};
/**
* 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.dom.TagIterator} iterator The iterator.
* @return {Node|Object} 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 {!Element} 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) {
assertEquals('Tag names should match' + errorSuffix,
expectedNode.tagName, actualNode.tagName);
assertObjectEquals('Should have same styles' + errorSuffix,
goog.style.parseStyleAttribute(expectedNode.style.cssText),
goog.style.parseStyleAttribute(actualNode.style.cssText));
goog.testing.dom.assertAttributesEqual_(errorSuffix, expectedNode,
actualNode, !!opt_strictAttributes);
if (IE_TEXT_COLLAPSE &&
goog.style.getCascadedStyle(
/** @type {Element} */ (actualNode), '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.isEmpty(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 {Node} expectedNode The node whose attributes we are expecting.
* @param {Node} actualNode The node 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,
expectedNode, actualNode, strictAttributes) {
if (strictAttributes) {
goog.testing.dom.compareClassAttribute_(expectedNode, actualNode);
}
var expectedAttributes = expectedNode.attributes;
var actualAttributes = actualNode.attributes;
for (var i = 0, len = expectedAttributes.length; i < len; i++) {
var expectedName = expectedAttributes[i].name;
var expectedValue = goog.testing.dom.getAttributeValue_(expectedNode,
expectedName);
var actualAttribute = actualAttributes[expectedName];
var actualValue = goog.testing.dom.getAttributeValue_(actualNode,
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_(actualNode) + errorSuffix,
actualAttribute);
assertEquals('Expected attribute ' + expectedName +
' has a different value ' + errorSuffix,
expectedValue,
goog.testing.dom.getAttributeValue_(actualNode, 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_(actualNode) + errorSuffix,
expectedAttributes[actualName]);
}
}
};
/**
* Assert the class attribute of actualNode is the same as the one in
* expectedNode, ignoring classes that are useragents.
* @param {Node} expectedNode The DOM node whose class we expect.
* @param {Node} actualNode The DOM node with the actual class.
* @private
*/
goog.testing.dom.compareClassAttribute_ = function(expectedNode,
actualNode) {
var classes = goog.dom.classes.get(expectedNode);
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.dom.classes.get(actualNode);
actualClasses.sort();
assertArrayEquals(
'Expected class was: ' + expectedClasses.join(' ') +
', but actual class was: ' + actualNode.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);
}
};

View File

@@ -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;
}
}
};

View File

@@ -0,0 +1,99 @@
// 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.
*/
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;
};
};
goog.inherits(goog.testing.editor.FieldMock, goog.testing.LooseMock);

View File

@@ -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 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}
*/
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.
*/
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.base(this, 'disposeInternal');
};

View File

@@ -0,0 +1,86 @@
// 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
*/
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;
};

View File

@@ -0,0 +1,713 @@
// 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}
* @suppress {underscore} 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}
* @suppress {underscore} 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, 'Target should not 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 on IE.
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) &&
!(goog.userAgent.IE && !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 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;
}
}
}
};

View File

@@ -0,0 +1,41 @@
// 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}
*/
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);

View File

@@ -0,0 +1,64 @@
// 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}
*/
goog.testing.events.OnlineHandler = function(initialState) {
goog.base(this);
/**
* 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);
}
};

View File

@@ -0,0 +1,236 @@
// 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
*/
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}
* @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_ = [];
};

View File

@@ -0,0 +1,116 @@
// 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;
/**
* @see http://www.w3.org/TR/FileAPI/#dfn-slice
* @param {number} start The start byte offset.
* @param {number} length The number of bytes to slice.
* @param {string=} opt_contentType The type of the resulting Blob.
* @return {!goog.testing.fs.Blob} The result of the slice operation.
*/
goog.testing.fs.Blob.prototype.slice = function(
start, length, opt_contentType) {
start = Math.max(0, start);
return new goog.testing.fs.Blob(
this.data_.substring(start, start + Math.max(length, 0)),
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;
};

View File

@@ -0,0 +1,621 @@
// 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 : '');
return this.checkNotDeleted(msg).addCallback(function() {
var name = opt_newName || this.getName();
var entry = this.clone();
parent.children[name] = entry;
parent.lastModifiedTimestamp_ = goog.now();
entry.name_ = name;
entry.parent = 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();
return this.checkNotDeleted(msg).addCallback(function() {
delete this.parent.children[this.getName()];
this.parent.lastModifiedTimestamp_ = goog.now();
this.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) {
d.errback(new goog.fs.Error(goog.fs.Error.ErrorCode.NOT_FOUND, action));
} 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}
*/
goog.testing.fs.DirectoryEntry = function(fs, parent, name, children) {
goog.base(this, 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(
goog.fs.Error.ErrorCode.INVALID_MODIFICATION,
'removing ' + this.getFullPath()));
}, 0, this);
return d;
} else {
return goog.base(this, 'remove');
}
};
/** @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.
* @return {!goog.testing.fs.FileEntry} The loaded file.
*/
goog.testing.fs.DirectoryEntry.prototype.getFileSync = function(
path, opt_behavior) {
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, '');
}, 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(
goog.fs.Error.ErrorCode.NOT_FOUND,
'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(
goog.fs.Error.ErrorCode.NOT_FOUND,
'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(
goog.fs.Error.ErrorCode.PATH_EXISTS,
'loading ' + path + ' from ' + this.getFullPath());
} else if (entry.isFile() != isFile) {
throw new goog.fs.Error(
goog.fs.Error.ErrorCode.TYPE_MISMATCH,
'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.
* @constructor
* @extends {goog.testing.fs.Entry}
* @implements {goog.fs.FileEntry}
*/
goog.testing.fs.FileEntry = function(fs, parent, name, data) {
goog.base(this, 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);
/**
* 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_;
};

View File

@@ -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.
/**
* @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}
*/
goog.testing.fs.File = function(name, opt_lastModified, opt_data, opt_type) {
goog.base(this, 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);

View File

@@ -0,0 +1,272 @@
// 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.EventType');
goog.require('goog.fs.FileReader.ReadyState');
goog.require('goog.testing.fs.File');
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.base(this);
/**
* 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(goog.fs.Error.ErrorCode.INVALID_STATE, 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(goog.fs.Error.ErrorCode.INVALID_STATE, 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(
goog.fs.Error.ErrorCode.ABORT, '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));
};

View File

@@ -0,0 +1,63 @@
// 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}
*/
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_;
};

View File

@@ -0,0 +1,261 @@
// 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.Event');
goog.require('goog.events.EventTarget');
goog.require('goog.fs.Error');
goog.require('goog.fs.FileSaver.EventType');
goog.require('goog.fs.FileSaver.ReadyState');
goog.require('goog.string');
goog.require('goog.testing.fs.File');
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}
*/
goog.testing.fs.FileWriter = function(fileEntry) {
goog.base(this);
/**
* 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(goog.fs.Error.ErrorCode.INVALID_STATE, 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(goog.fs.Error.ErrorCode.INVALID_STATE, 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(goog.fs.Error.ErrorCode.INVALID_STATE, 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(goog.fs.Error.ErrorCode.INVALID_STATE, 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(
goog.fs.Error.ErrorCode.ABORT, '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));
};

View File

@@ -0,0 +1,147 @@
// 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');
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(''));
};
/**
* 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, 'blobToString', goog.testing.fs.blobToString);
};

View File

@@ -0,0 +1,81 @@
// 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}
*/
goog.testing.fs.ProgressEvent = function(type, loaded, total) {
goog.base(this, 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_;
};

View File

@@ -0,0 +1,177 @@
// 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.MockInterface');
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);
};

View File

@@ -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.Segment');
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]);
}
}
};

View File

@@ -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);
};

View File

@@ -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.i18n.asserts');
goog.require('goog.testing.ExpectedFailures');
// 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);
}
}

View File

@@ -0,0 +1,156 @@
// 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);
// 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;
};
}
}
})();

View File

@@ -0,0 +1,240 @@
// 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
*/
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} objectToMock The 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);
}
}
}
};

View File

@@ -0,0 +1,79 @@
// 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
*/
goog.testing.messaging.MockMessageChannel = function(mockControl) {
goog.base(this);
/**
* 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);
};

View File

@@ -0,0 +1,100 @@
// 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');
/**
* 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
*/
goog.testing.messaging.MockMessageEvent = function(
data, opt_origin, opt_lastEventId, opt_source, opt_ports) {
goog.base(this, 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));
};

View File

@@ -0,0 +1,85 @@
// 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}
*/
goog.testing.messaging.MockMessagePort = function(id, mockControl) {
goog.base(this);
/**
* 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;
};

View File

@@ -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.
/**
* @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}
*/
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];
};

View File

@@ -0,0 +1,619 @@
// 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
*/
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}
*/
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 */
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 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(''));
};

View File

@@ -0,0 +1,578 @@
// 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
*/
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
*/
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) {
if (namespace === goog.global) {
namespace = goog.testing.TestCase.getGlobals();
}
for (var prop in namespace) {
if (namespace[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_ = {};
};

View File

@@ -0,0 +1,520 @@
// 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.testing.PropertyReplacer');
goog.require('goog.testing.events');
goog.require('goog.testing.events.Event');
/**
* 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}
*/
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));
// 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 = ['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_;
}
};
/** @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;
};
/**
* 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;
};
/**
* @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_.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) {
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} millis The number of milliseconds to call it after.
* @return {number} The number of timeouts created.
* @private
*/
goog.testing.MockClock.prototype.setTimeout_ = function(funcToCall, millis) {
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} millis The number of milliseconds between calls.
* @return {number} The number of timeouts created.
* @private
*/
goog.testing.MockClock.prototype.setInterval_ = function(funcToCall, millis) {
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(user): 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);
};

View File

@@ -0,0 +1,219 @@
// 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.MockInterface');
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} objectToMock The 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} objectToMock The 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;
};

View File

@@ -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() {};

View File

@@ -0,0 +1,395 @@
// 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}
*/
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}
*/
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}
*/
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}
*/
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}
*/
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;
});
};

View File

@@ -0,0 +1,126 @@
// 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 MockRandom provides a mechanism for specifying a stream of
* numbers to expect from calls to Math.random().
*
*/
goog.provide('goog.testing.MockRandom');
goog.require('goog.Disposable');
/**
* Class for unit testing code that uses Math.random.
*
* @param {Array.<number>} sequence The sequence of numbers to return.
* @param {boolean=} opt_install Whether to install the MockRandom at
* construction time.
* @extends {goog.Disposable}
* @constructor
*/
goog.testing.MockRandom = function(sequence, opt_install) {
goog.Disposable.call(this);
/**
* The sequence of numbers to be returned by calls to random()
* @type {Array.<number>}
* @private
*/
this.sequence_ = sequence || [];
/**
* The original Math.random function.
* @type {function(): number}
* @private
*/
this.mathRandom_ = Math.random;
if (opt_install) {
this.install();
}
};
goog.inherits(goog.testing.MockRandom, goog.Disposable);
/**
* Whether this MockRandom has been installed.
* @type {boolean}
* @private
*/
goog.testing.MockRandom.prototype.installed_;
/**
* Installs this MockRandom as the system number generator.
*/
goog.testing.MockRandom.prototype.install = function() {
if (!this.installed_) {
Math.random = goog.bind(this.random, this);
this.installed_ = true;
}
};
/**
* @return {number} The next number in the sequence. If there are no more values
* left, this will return a random number.
*/
goog.testing.MockRandom.prototype.random = function() {
return this.hasMoreValues() ? this.sequence_.shift() : this.mathRandom_();
};
/**
* @return {boolean} Whether there are more numbers left in the sequence.
*/
goog.testing.MockRandom.prototype.hasMoreValues = function() {
return this.sequence_.length > 0;
};
/**
* Injects new numbers into the beginning of the sequence.
* @param {Array.<number>|number} values Number or array of numbers to inject.
*/
goog.testing.MockRandom.prototype.inject = function(values) {
if (goog.isArray(values)) {
this.sequence_ = values.concat(this.sequence_);
} else {
this.sequence_.splice(0, 0, values);
}
};
/**
* Uninstalls the MockRandom.
*/
goog.testing.MockRandom.prototype.uninstall = function() {
if (this.installed_) {
Math.random = this.mathRandom_;
this.installed_ = false;
}
};
/** @override */
goog.testing.MockRandom.prototype.disposeInternal = function() {
this.uninstall();
delete this.sequence_;
delete this.mathRandom_;
goog.testing.MockRandom.superClass_.disposeInternal.call(this);
};

View File

@@ -0,0 +1,66 @@
// 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 LooseMock of goog.dom.AbstractRange.
*
*/
goog.provide('goog.testing.MockRange');
goog.require('goog.dom.AbstractRange');
goog.require('goog.testing.LooseMock');
/**
* LooseMock of goog.dom.AbstractRange. Useful because the mock framework cannot
* simply create a mock out of an abstract class, and cannot create a mock out
* of classes that implements __iterator__ because it relies on the default
* behavior of iterating through all of an object's properties.
* @constructor
* @extends {goog.testing.LooseMock}
*/
goog.testing.MockRange = function() {
goog.testing.LooseMock.call(this, goog.testing.MockRange.ConcreteRange_);
};
goog.inherits(goog.testing.MockRange, goog.testing.LooseMock);
// *** Private helper class ************************************************* //
/**
* Concrete subclass of goog.dom.AbstractRange that simply sets the abstract
* method __iterator__ to undefined so that javascript defaults to iterating
* through all of the object's properties.
* @constructor
* @extends {goog.dom.AbstractRange}
* @private
*/
goog.testing.MockRange.ConcreteRange_ = function() {
goog.dom.AbstractRange.call(this);
};
goog.inherits(goog.testing.MockRange.ConcreteRange_, goog.dom.AbstractRange);
/**
* Undefine the iterator so the mock framework can loop through this class'
* properties.
* @override
*/
goog.testing.MockRange.ConcreteRange_.prototype.__iterator__ =
// This isn't really type-safe.
/** @type {?} */ (undefined);

View File

@@ -0,0 +1,107 @@
// 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 Provides a JS storage class implementing the HTML5 Storage
* interface.
*/
goog.require('goog.structs.Map');
goog.provide('goog.testing.MockStorage');
/**
* A JS storage instance, implementing the HMTL5 Storage interface.
* See http://www.w3.org/TR/webstorage/ for details.
*
* @constructor
* @implements {Storage}
*/
goog.testing.MockStorage = function() {
/**
* The underlying storage object.
* @type {goog.structs.Map}
* @private
*/
this.store_ = new goog.structs.Map();
/**
* The number of elements in the storage.
* @type {number}
*/
this.length = 0;
};
/**
* Sets an item to the storage.
* @param {string} key Storage key.
* @param {*} value Storage value. Must be convertible to string.
* @override
*/
goog.testing.MockStorage.prototype.setItem = function(key, value) {
this.store_.set(key, String(value));
this.length = this.store_.getCount();
};
/**
* Gets an item from the storage. The item returned is the "structured clone"
* of the value from setItem. In practice this means it's the value cast to a
* string.
* @param {string} key Storage key.
* @return {?string} Storage value for key; null if does not exist.
* @override
*/
goog.testing.MockStorage.prototype.getItem = function(key) {
var val = this.store_.get(key);
// Enforce that getItem returns string values.
return (val != null) ? /** @type {string} */ (val) : null;
};
/**
* Removes and item from the storage.
* @param {string} key Storage key.
* @override
*/
goog.testing.MockStorage.prototype.removeItem = function(key) {
this.store_.remove(key);
this.length = this.store_.getCount();
};
/**
* Clears the storage.
* @override
*/
goog.testing.MockStorage.prototype.clear = function() {
this.store_.clear();
this.length = 0;
};
/**
* Returns the key at the given index.
* @param {number} index The index for the key.
* @return {?string} Key at the given index, null if not found.
* @override
*/
goog.testing.MockStorage.prototype.key = function(index) {
return this.store_.getKeys()[index] || null;
};

View File

@@ -0,0 +1,141 @@
// 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 MockUserAgent overrides goog.userAgent.getUserAgentString()
* depending on a specified configuration.
*
*/
goog.provide('goog.testing.MockUserAgent');
goog.require('goog.Disposable');
goog.require('goog.userAgent');
/**
* Class for unit testing code that uses goog.userAgent.
*
* @extends {goog.Disposable}
* @constructor
*/
goog.testing.MockUserAgent = function() {
goog.Disposable.call(this);
/**
* The userAgent string used by goog.userAgent.
* @type {?string}
* @private
*/
this.userAgent_ = goog.userAgent.getUserAgentString();
/**
* The original goog.userAgent.getUserAgentString function.
* @type {function():?string}
* @private
*/
this.originalUserAgentFunction_ = goog.userAgent.getUserAgentString;
/**
* The navigator object used by goog.userAgent
* @type {Object}
* @private
*/
this.navigator_ = goog.userAgent.getNavigator();
/**
* The original goog.userAgent.getNavigator function
* @type {function():Object}
* @private
*/
this.originalNavigatorFunction_ = goog.userAgent.getNavigator;
};
goog.inherits(goog.testing.MockUserAgent, goog.Disposable);
/**
* Whether this MockUserAgent has been installed.
* @type {boolean}
* @private
*/
goog.testing.MockUserAgent.prototype.installed_;
/**
* Installs this MockUserAgent.
*/
goog.testing.MockUserAgent.prototype.install = function() {
if (!this.installed_) {
goog.userAgent.getUserAgentString =
goog.bind(this.getUserAgentString, this);
goog.userAgent.getNavigator = goog.bind(this.getNavigator, this);
this.installed_ = true;
}
};
/**
* @return {?string} The userAgent set in this class.
*/
goog.testing.MockUserAgent.prototype.getUserAgentString = function() {
return this.userAgent_;
};
/**
* @param {string} userAgent The desired userAgent string to use.
*/
goog.testing.MockUserAgent.prototype.setUserAgentString = function(userAgent) {
this.userAgent_ = userAgent;
};
/**
* @return {Object} The Navigator set in this class.
*/
goog.testing.MockUserAgent.prototype.getNavigator = function() {
return this.navigator_;
};
/**
* @param {Object} navigator The desired Navigator object to use.
*/
goog.testing.MockUserAgent.prototype.setNavigator = function(navigator) {
this.navigator_ = navigator;
};
/**
* Uninstalls the MockUserAgent.
*/
goog.testing.MockUserAgent.prototype.uninstall = function() {
if (this.installed_) {
goog.userAgent.getUserAgentString = this.originalUserAgentFunction_;
goog.userAgent.getNavigator = this.originalNavigatorFunction_;
this.installed_ = false;
}
};
/** @override */
goog.testing.MockUserAgent.prototype.disposeInternal = function() {
this.uninstall();
delete this.userAgent_;
delete this.originalUserAgentFunction_;
delete this.navigator_;
delete this.originalNavigatorFunction_;
goog.testing.MockUserAgent.superClass_.disposeInternal.call(this);
};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,709 @@
// 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 of XhrIo for unit testing.
*/
goog.provide('goog.testing.net.XhrIo');
goog.require('goog.array');
goog.require('goog.dom.xml');
goog.require('goog.events');
goog.require('goog.events.EventTarget');
goog.require('goog.json');
goog.require('goog.net.ErrorCode');
goog.require('goog.net.EventType');
goog.require('goog.net.HttpStatus');
goog.require('goog.net.XhrIo');
goog.require('goog.net.XmlHttp');
goog.require('goog.object');
goog.require('goog.structs.Map');
/**
* Mock implementation of goog.net.XhrIo. This doesn't provide a mock
* implementation for all cases, but it's not too hard to add them as needed.
* @param {goog.testing.TestQueue=} opt_testQueue Test queue for inserting test
* events.
* @constructor
* @extends {goog.events.EventTarget}
*/
goog.testing.net.XhrIo = function(opt_testQueue) {
goog.events.EventTarget.call(this);
/**
* Map of default headers to add to every request, use:
* XhrIo.headers.set(name, value)
* @type {goog.structs.Map}
*/
this.headers = new goog.structs.Map();
/**
* Queue of events write to.
* @type {goog.testing.TestQueue?}
* @private
*/
this.testQueue_ = opt_testQueue || null;
};
goog.inherits(goog.testing.net.XhrIo, goog.events.EventTarget);
/**
* Alias this enum here to make mocking of goog.net.XhrIo easier.
* @enum {string}
*/
goog.testing.net.XhrIo.ResponseType = goog.net.XhrIo.ResponseType;
/**
* All non-disposed instances of goog.testing.net.XhrIo created
* by {@link goog.testing.net.XhrIo.send} are in this Array.
* @see goog.testing.net.XhrIo.cleanup
* @type {Array.<goog.testing.net.XhrIo>}
* @private
*/
goog.testing.net.XhrIo.sendInstances_ = [];
/**
* Returns an Array containing all non-disposed instances of
* goog.testing.net.XhrIo created by {@link goog.testing.net.XhrIo.send}.
* @return {Array} Array of goog.testing.net.XhrIo instances.
*/
goog.testing.net.XhrIo.getSendInstances = function() {
return goog.testing.net.XhrIo.sendInstances_;
};
/**
* Disposes all non-disposed instances of goog.testing.net.XhrIo created by
* {@link goog.testing.net.XhrIo.send}.
* @see goog.net.XhrIo.cleanup
*/
goog.testing.net.XhrIo.cleanup = function() {
var instances = goog.testing.net.XhrIo.sendInstances_;
while (instances.length) {
instances.pop().dispose();
}
};
/**
* Simulates the static XhrIo send method.
* @param {string} url Uri to make request to.
* @param {Function=} opt_callback Callback function for when request is
* complete.
* @param {string=} opt_method Send method, default: GET.
* @param {string=} opt_content Post data.
* @param {Object|goog.structs.Map=} opt_headers Map of headers to add to the
* request.
* @param {number=} opt_timeoutInterval Number of milliseconds after which an
* incomplete request will be aborted; 0 means no timeout is set.
*/
goog.testing.net.XhrIo.send = function(url, opt_callback, opt_method,
opt_content, opt_headers,
opt_timeoutInterval) {
var x = new goog.testing.net.XhrIo();
goog.testing.net.XhrIo.sendInstances_.push(x);
if (opt_callback) {
goog.events.listen(x, goog.net.EventType.COMPLETE, opt_callback);
}
goog.events.listen(x,
goog.net.EventType.READY,
goog.partial(goog.testing.net.XhrIo.cleanupSend_, x));
if (opt_timeoutInterval) {
x.setTimeoutInterval(opt_timeoutInterval);
}
x.send(url, opt_method, opt_content, opt_headers);
};
/**
* Disposes of the specified goog.testing.net.XhrIo created by
* {@link goog.testing.net.XhrIo.send} and removes it from
* {@link goog.testing.net.XhrIo.pendingStaticSendInstances_}.
* @param {goog.testing.net.XhrIo} XhrIo An XhrIo created by
* {@link goog.testing.net.XhrIo.send}.
* @private
*/
goog.testing.net.XhrIo.cleanupSend_ = function(XhrIo) {
XhrIo.dispose();
goog.array.remove(goog.testing.net.XhrIo.sendInstances_, XhrIo);
};
/**
* Stores the simulated response headers for the requests which are sent through
* this XhrIo.
* @type {Object}
* @private
*/
goog.testing.net.XhrIo.prototype.responseHeaders_;
/**
* Whether MockXhrIo is active.
* @type {boolean}
* @private
*/
goog.testing.net.XhrIo.prototype.active_ = false;
/**
* Last URI that was requested.
* @type {string}
* @private
*/
goog.testing.net.XhrIo.prototype.lastUri_ = '';
/**
* Last HTTP method that was requested.
* @type {string|undefined}
* @private
*/
goog.testing.net.XhrIo.prototype.lastMethod_;
/**
* Last POST content that was requested.
* @type {string|undefined}
* @private
*/
goog.testing.net.XhrIo.prototype.lastContent_;
/**
* Additional headers that were requested in the last query.
* @type {Object|goog.structs.Map|undefined}
* @private
*/
goog.testing.net.XhrIo.prototype.lastHeaders_;
/**
* Last error code.
* @type {goog.net.ErrorCode}
* @private
*/
goog.testing.net.XhrIo.prototype.lastErrorCode_ =
goog.net.ErrorCode.NO_ERROR;
/**
* Last error message.
* @type {string}
* @private
*/
goog.testing.net.XhrIo.prototype.lastError_ = '';
/**
* The response object.
* @type {string|Document}
* @private
*/
goog.testing.net.XhrIo.prototype.response_ = '';
/**
* Mock ready state.
* @type {number}
* @private
*/
goog.testing.net.XhrIo.prototype.readyState_ =
goog.net.XmlHttp.ReadyState.UNINITIALIZED;
/**
* Number of milliseconds after which an incomplete request will be aborted and
* a {@link goog.net.EventType.TIMEOUT} event raised; 0 means no timeout is set.
* @type {number}
* @private
*/
goog.testing.net.XhrIo.prototype.timeoutInterval_ = 0;
/**
* Window timeout ID used to cancel the timeout event handler if the request
* completes successfully.
* @type {Object}
* @private
*/
goog.testing.net.XhrIo.prototype.timeoutId_ = null;
/**
* The requested type for the response. The empty string means use the default
* XHR behavior.
* @type {goog.net.XhrIo.ResponseType}
* @private
*/
goog.testing.net.XhrIo.prototype.responseType_ =
goog.net.XhrIo.ResponseType.DEFAULT;
/**
* Whether a "credentialed" request is to be sent (one that is aware of cookies
* and authentication) . This is applicable only for cross-domain requests and
* more recent browsers that support this part of the HTTP Access Control
* standard.
*
* @see http://dev.w3.org/2006/webapi/XMLHttpRequest-2/#withcredentials
*
* @type {boolean}
* @private
*/
goog.testing.net.XhrIo.prototype.withCredentials_ = false;
/**
* Whether there's currently an underlying XHR object.
* @type {boolean}
* @private
*/
goog.testing.net.XhrIo.prototype.xhr_ = false;
/**
* Returns the number of milliseconds after which an incomplete request will be
* aborted, or 0 if no timeout is set.
* @return {number} Timeout interval in milliseconds.
*/
goog.testing.net.XhrIo.prototype.getTimeoutInterval = function() {
return this.timeoutInterval_;
};
/**
* Sets the number of milliseconds after which an incomplete request will be
* aborted and a {@link goog.net.EventType.TIMEOUT} event raised; 0 means no
* timeout is set.
* @param {number} ms Timeout interval in milliseconds; 0 means none.
*/
goog.testing.net.XhrIo.prototype.setTimeoutInterval = function(ms) {
this.timeoutInterval_ = Math.max(0, ms);
};
/**
* Causes timeout events to be fired.
*/
goog.testing.net.XhrIo.prototype.simulateTimeout = function() {
this.lastErrorCode_ = goog.net.ErrorCode.TIMEOUT;
this.dispatchEvent(goog.net.EventType.TIMEOUT);
this.abort(goog.net.ErrorCode.TIMEOUT);
};
/**
* Sets the desired type for the response. At time of writing, this is only
* supported in very recent versions of WebKit (10.0.612.1 dev and later).
*
* If this is used, the response may only be accessed via {@link #getResponse}.
*
* @param {goog.net.XhrIo.ResponseType} type The desired type for the response.
*/
goog.testing.net.XhrIo.prototype.setResponseType = function(type) {
this.responseType_ = type;
};
/**
* Gets the desired type for the response.
* @return {goog.net.XhrIo.ResponseType} The desired type for the response.
*/
goog.testing.net.XhrIo.prototype.getResponseType = function() {
return this.responseType_;
};
/**
* Sets whether a "credentialed" request that is aware of cookie and
* authentication information should be made. This option is only supported by
* browsers that support HTTP Access Control. As of this writing, this option
* is not supported in IE.
*
* @param {boolean} withCredentials Whether this should be a "credentialed"
* request.
*/
goog.testing.net.XhrIo.prototype.setWithCredentials =
function(withCredentials) {
this.withCredentials_ = withCredentials;
};
/**
* Gets whether a "credentialed" request is to be sent.
* @return {boolean} The desired type for the response.
*/
goog.testing.net.XhrIo.prototype.getWithCredentials = function() {
return this.withCredentials_;
};
/**
* Abort the current XMLHttpRequest
* @param {goog.net.ErrorCode=} opt_failureCode Optional error code to use -
* defaults to ABORT.
*/
goog.testing.net.XhrIo.prototype.abort = function(opt_failureCode) {
if (this.active_) {
try {
this.active_ = false;
this.lastErrorCode_ = opt_failureCode || goog.net.ErrorCode.ABORT;
this.dispatchEvent(goog.net.EventType.COMPLETE);
this.dispatchEvent(goog.net.EventType.ABORT);
} finally {
this.simulateReady();
}
}
};
/**
* Simulates the XhrIo send.
* @param {string} url Uri to make request too.
* @param {string=} opt_method Send method, default: GET.
* @param {string=} opt_content Post data.
* @param {Object|goog.structs.Map=} opt_headers Map of headers to add to the
* request.
*/
goog.testing.net.XhrIo.prototype.send = function(url, opt_method, opt_content,
opt_headers) {
if (this.xhr_) {
throw Error('[goog.net.XhrIo] Object is active with another request');
}
this.lastUri_ = url;
this.lastMethod_ = opt_method || 'GET';
this.lastContent_ = opt_content;
this.lastHeaders_ = opt_headers;
if (this.testQueue_) {
this.testQueue_.enqueue(['s', url, opt_method, opt_content, opt_headers]);
}
this.xhr_ = true;
this.active_ = true;
this.readyState_ = goog.net.XmlHttp.ReadyState.UNINITIALIZED;
this.simulateReadyStateChange(goog.net.XmlHttp.ReadyState.LOADING);
};
/**
* Creates a new XHR object.
* @return {XMLHttpRequest|GearsHttpRequest} The newly created XHR object.
* @protected
*/
goog.testing.net.XhrIo.prototype.createXhr = function() {
return goog.net.XmlHttp();
};
/**
* Simulates changing to the new ready state.
* @param {number} readyState Ready state to change to.
*/
goog.testing.net.XhrIo.prototype.simulateReadyStateChange =
function(readyState) {
if (readyState < this.readyState_) {
throw Error('Readystate cannot go backwards');
}
// INTERACTIVE can be dispatched repeatedly as more data is reported.
if (readyState == goog.net.XmlHttp.ReadyState.INTERACTIVE &&
readyState == this.readyState_) {
this.dispatchEvent(goog.net.EventType.READY_STATE_CHANGE);
return;
}
while (this.readyState_ < readyState) {
this.readyState_++;
this.dispatchEvent(goog.net.EventType.READY_STATE_CHANGE);
if (this.readyState_ == goog.net.XmlHttp.ReadyState.COMPLETE) {
this.active_ = false;
this.dispatchEvent(goog.net.EventType.COMPLETE);
}
}
};
/**
* Simulate receiving some bytes but the request not fully completing, and
* the XHR entering the 'INTERACTIVE' state.
* @param {string} partialResponse A string to append to the response text.
* @param {Object=} opt_headers Simulated response headers.
*/
goog.testing.net.XhrIo.prototype.simulatePartialResponse =
function(partialResponse, opt_headers) {
this.response_ += partialResponse;
this.responseHeaders_ = opt_headers || {};
this.statusCode_ = 200;
this.simulateReadyStateChange(goog.net.XmlHttp.ReadyState.INTERACTIVE);
};
/**
* Simulates receiving a response.
* @param {number} statusCode Simulated status code.
* @param {string|Document|null} response Simulated response.
* @param {Object=} opt_headers Simulated response headers.
*/
goog.testing.net.XhrIo.prototype.simulateResponse = function(statusCode,
response, opt_headers) {
this.statusCode_ = statusCode;
this.response_ = response || '';
this.responseHeaders_ = opt_headers || {};
try {
if (this.isSuccess()) {
this.simulateReadyStateChange(goog.net.XmlHttp.ReadyState.COMPLETE);
this.dispatchEvent(goog.net.EventType.SUCCESS);
} else {
this.lastErrorCode_ = goog.net.ErrorCode.HTTP_ERROR;
this.lastError_ = this.getStatusText() + ' [' + this.getStatus() + ']';
this.simulateReadyStateChange(goog.net.XmlHttp.ReadyState.COMPLETE);
this.dispatchEvent(goog.net.EventType.ERROR);
}
} finally {
this.simulateReady();
}
};
/**
* Simulates the Xhr is ready for the next request.
*/
goog.testing.net.XhrIo.prototype.simulateReady = function() {
this.active_ = false;
this.xhr_ = false;
this.dispatchEvent(goog.net.EventType.READY);
};
/**
* @return {boolean} Whether there is an active request.
*/
goog.testing.net.XhrIo.prototype.isActive = function() {
return !!this.xhr_;
};
/**
* Has the request completed.
* @return {boolean} Whether the request has completed.
*/
goog.testing.net.XhrIo.prototype.isComplete = function() {
return this.readyState_ == goog.net.XmlHttp.ReadyState.COMPLETE;
};
/**
* Has the request compeleted with a success.
* @return {boolean} Whether the request compeleted successfully.
*/
goog.testing.net.XhrIo.prototype.isSuccess = function() {
switch (this.getStatus()) {
case goog.net.HttpStatus.OK:
case goog.net.HttpStatus.NO_CONTENT:
case goog.net.HttpStatus.NOT_MODIFIED:
return true;
default:
return false;
}
};
/**
* Returns the readystate.
* @return {number} goog.net.XmlHttp.ReadyState.*.
*/
goog.testing.net.XhrIo.prototype.getReadyState = function() {
return this.readyState_;
};
/**
* Get the status from the Xhr object. Will only return correct result when
* called from the context of a callback.
* @return {number} Http status.
*/
goog.testing.net.XhrIo.prototype.getStatus = function() {
return this.statusCode_;
};
/**
* Get the status text from the Xhr object. Will only return correct result
* when called from the context of a callback.
* @return {string} Status text.
*/
goog.testing.net.XhrIo.prototype.getStatusText = function() {
return '';
};
/**
* Gets the last error message.
* @return {goog.net.ErrorCode} Last error code.
*/
goog.testing.net.XhrIo.prototype.getLastErrorCode = function() {
return this.lastErrorCode_;
};
/**
* Gets the last error message.
* @return {string} Last error message.
*/
goog.testing.net.XhrIo.prototype.getLastError = function() {
return this.lastError_;
};
/**
* Gets the last URI that was requested.
* @return {string} Last URI.
*/
goog.testing.net.XhrIo.prototype.getLastUri = function() {
return this.lastUri_;
};
/**
* Gets the last HTTP method that was requested.
* @return {string|undefined} Last HTTP method used by send.
*/
goog.testing.net.XhrIo.prototype.getLastMethod = function() {
return this.lastMethod_;
};
/**
* Gets the last POST content that was requested.
* @return {string|undefined} Last POST content or undefined if last request was
* a GET.
*/
goog.testing.net.XhrIo.prototype.getLastContent = function() {
return this.lastContent_;
};
/**
* Gets the headers of the last request.
* @return {Object|goog.structs.Map|undefined} Last headers manually set in send
* call or undefined if no additional headers were specified.
*/
goog.testing.net.XhrIo.prototype.getLastRequestHeaders = function() {
return this.lastHeaders_;
};
/**
* Gets the response text from the Xhr object. Will only return correct result
* when called from the context of a callback.
* @return {string} Result from the server.
*/
goog.testing.net.XhrIo.prototype.getResponseText = function() {
return goog.isString(this.response_) ? this.response_ :
goog.dom.xml.serialize(this.response_);
};
/**
* Gets the response body from the Xhr object. Will only return correct result
* when called from the context of a callback.
* @return {Object} Binary result from the server or null.
*/
goog.testing.net.XhrIo.prototype.getResponseBody = function() {
return null;
};
/**
* Gets the response and evaluates it as JSON from the Xhr object. Will only
* return correct result when called from the context of a callback.
* @param {string=} opt_xssiPrefix Optional XSSI prefix string to use for
* stripping of the response before parsing. This needs to be set only if
* your backend server prepends the same prefix string to the JSON response.
* @return {Object} JavaScript object.
*/
goog.testing.net.XhrIo.prototype.getResponseJson = function(opt_xssiPrefix) {
var responseText = this.getResponseText();
if (opt_xssiPrefix && responseText.indexOf(opt_xssiPrefix) == 0) {
responseText = responseText.substring(opt_xssiPrefix.length);
}
return goog.json.parse(responseText);
};
/**
* Gets the response XML from the Xhr object. Will only return correct result
* when called from the context of a callback.
* @return {Document} Result from the server if it was XML.
*/
goog.testing.net.XhrIo.prototype.getResponseXml = function() {
// NOTE(user): I haven't found out how to check in Internet Explorer
// whether the response is XML document, so I do it the other way around.
return goog.isString(this.response_) ? null : this.response_;
};
/**
* Get the response as the type specificed by {@link #setResponseType}. At time
* of writing, this is only supported in very recent versions of WebKit
* (10.0.612.1 dev and later).
*
* @return {*} The response.
*/
goog.testing.net.XhrIo.prototype.getResponse = function() {
return this.response_;
};
/**
* Get the value of the response-header with the given name from the Xhr object
* Will only return correct result when called from the context of a callback
* and the request has completed
* @param {string} key The name of the response-header to retrieve.
* @return {string|undefined} The value of the response-header named key.
*/
goog.testing.net.XhrIo.prototype.getResponseHeader = function(key) {
return this.isComplete() ? this.responseHeaders_[key] : undefined;
};
/**
* Gets the text of all the headers in the response.
* Will only return correct result when called from the context of a callback
* and the request has completed
* @return {string} The string containing all the response headers.
*/
goog.testing.net.XhrIo.prototype.getAllResponseHeaders = function() {
if (!this.isComplete()) {
return '';
}
var headers = [];
goog.object.forEach(this.responseHeaders_, function(value, name) {
headers.push(name + ': ' + value);
});
return headers.join('\n');
};

View File

@@ -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 An XhrIo pool that uses a single mock XHR object for testing.
*
*/
goog.provide('goog.testing.net.XhrIoPool');
goog.require('goog.net.XhrIoPool');
goog.require('goog.testing.net.XhrIo');
/**
* A pool containing a single mock XhrIo object.
*
* @param {goog.testing.net.XhrIo=} opt_xhr The mock XhrIo object.
* @constructor
* @extends {goog.net.XhrIoPool}
*/
goog.testing.net.XhrIoPool = function(opt_xhr) {
/**
* The mock XhrIo object.
* @type {!goog.testing.net.XhrIo}
* @private
*/
this.xhr_ = opt_xhr || new goog.testing.net.XhrIo();
// Run this after setting xhr_ because xhr_ is used to initialize the pool.
goog.base(this, undefined, 1, 1);
};
goog.inherits(goog.testing.net.XhrIoPool, goog.net.XhrIoPool);
/**
* @override
* @suppress {invalidCasts}
*/
goog.testing.net.XhrIoPool.prototype.createObject = function() {
return (/** @type {!goog.net.XhrIo} */ (this.xhr_));
};
/**
* Get the mock XhrIo used by this pool.
*
* @return {!goog.testing.net.XhrIo} The mock XhrIo.
*/
goog.testing.net.XhrIoPool.prototype.getXhr = function() {
return this.xhr_;
};

View File

@@ -0,0 +1,67 @@
// 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 Helper for passing property names as string literals in
* compiled test code.
*
*/
goog.provide('goog.testing.ObjectPropertyString');
/**
* Object to pass a property name as a string literal and its containing object
* when the JSCompiler is rewriting these names. This should only be used in
* test code.
*
* @param {Object} object The containing object.
* @param {Object|string} propertyString Property name as a string literal.
* @constructor
*/
goog.testing.ObjectPropertyString = function(object, propertyString) {
this.object_ = object;
this.propertyString_ = /** @type {string} */ (propertyString);
};
/**
* @type {Object}
* @private
*/
goog.testing.ObjectPropertyString.prototype.object_;
/**
* @type {string}
* @private
*/
goog.testing.ObjectPropertyString.prototype.propertyString_;
/**
* @return {Object} The object.
*/
goog.testing.ObjectPropertyString.prototype.getObject = function() {
return this.object_;
};
/**
* @return {string} The property string.
*/
goog.testing.ObjectPropertyString.prototype.getPropertyString = function() {
return this.propertyString_;
};

View File

@@ -0,0 +1,190 @@
// 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 A table for showing the results of performance testing.
*
* {@see goog.testing.benchmark} for an easy way to use this functionality.
*
* @author attila@google.com (Attila Bodis)
* @author nicksantos@google.com (Nick Santos)
*/
goog.provide('goog.testing.PerformanceTable');
goog.require('goog.dom');
goog.require('goog.testing.PerformanceTimer');
/**
* A UI widget that runs performance tests and displays the results.
* @param {Element} root The element where the table should be attached.
* @param {goog.testing.PerformanceTimer=} opt_timer A timer to use for
* executing functions and profiling them.
* @param {number=} opt_precision Number of digits of precision to include in
* results. Defaults to 0.
* @constructor
*/
goog.testing.PerformanceTable = function(root, opt_timer, opt_precision) {
/**
* Where the table should be attached.
* @type {Element}
* @private
*/
this.root_ = root;
/**
* Number of digits of precision to include in results.
* Defaults to 0.
* @type {number}
* @private
*/
this.precision_ = opt_precision || 0;
var timer = opt_timer;
if (!timer) {
timer = new goog.testing.PerformanceTimer();
timer.setNumSamples(5);
timer.setDiscardOutliers(true);
}
/**
* A timer for running the tests.
* @type {goog.testing.PerformanceTimer}
* @private
*/
this.timer_ = timer;
this.initRoot_();
};
/**
* @return {goog.testing.PerformanceTimer} The timer being used.
*/
goog.testing.PerformanceTable.prototype.getTimer = function() {
return this.timer_;
};
/**
* Render the initial table.
* @private
*/
goog.testing.PerformanceTable.prototype.initRoot_ = function() {
this.root_.innerHTML =
'<table class="test-results" cellspacing="1">' +
' <thead>' +
' <tr>' +
' <th rowspan="2">Test Description</th>' +
' <th rowspan="2">Runs</th>' +
' <th colspan="4">Results (ms)</th>' +
' </tr>' +
' <tr>' +
' <th>Average</th>' +
' <th>Std Dev</th>' +
' <th>Minimum</th>' +
' <th>Maximum</th>' +
' </tr>' +
' </thead>' +
' <tbody>' +
' </tbody>' +
'</table>';
};
/**
* @return {Element} The body of the table.
* @private
*/
goog.testing.PerformanceTable.prototype.getTableBody_ = function() {
return this.root_.getElementsByTagName(goog.dom.TagName.TBODY)[0];
};
/**
* Round to the specified precision.
* @param {number} num The number to round.
* @return {string} The rounded number, as a string.
* @private
*/
goog.testing.PerformanceTable.prototype.round_ = function(num) {
var factor = Math.pow(10, this.precision_);
return String(Math.round(num * factor) / factor);
};
/**
* Run the given function with the performance timer, and show the results.
* @param {Function} fn The function to run.
* @param {string=} opt_desc A description to associate with this run.
*/
goog.testing.PerformanceTable.prototype.run = function(fn, opt_desc) {
this.runTask(
new goog.testing.PerformanceTimer.Task(/** @type {function()} */ (fn)),
opt_desc);
};
/**
* Run the given task with the performance timer, and show the results.
* @param {goog.testing.PerformanceTimer.Task} task The performance timer task
* to run.
* @param {string=} opt_desc A description to associate with this run.
*/
goog.testing.PerformanceTable.prototype.runTask = function(task, opt_desc) {
var results = this.timer_.runTask(task);
this.recordResults(results, opt_desc);
};
/**
* Record a performance timer results object to the performance table. See
* {@code goog.testing.PerformanceTimer} for details of the format of this
* object.
* @param {Object} results The performance timer results object.
* @param {string=} opt_desc A description to associate with these results.
*/
goog.testing.PerformanceTable.prototype.recordResults = function(
results, opt_desc) {
var average = results['average'];
var standardDeviation = results['standardDeviation'];
var isSuspicious = average < 0 || standardDeviation > average * .5;
var resultsRow = goog.dom.createDom('tr', null,
goog.dom.createDom('td', 'test-description',
opt_desc || 'No description'),
goog.dom.createDom('td', 'test-count', String(results['count'])),
goog.dom.createDom('td', 'test-average', this.round_(average)),
goog.dom.createDom('td', 'test-standard-deviation',
this.round_(standardDeviation)),
goog.dom.createDom('td', 'test-minimum', String(results['minimum'])),
goog.dom.createDom('td', 'test-maximum', String(results['maximum'])));
if (isSuspicious) {
resultsRow.className = 'test-suspicious';
}
this.getTableBody_().appendChild(resultsRow);
};
/**
* Report an error in the table.
* @param {*} reason The reason for the error.
*/
goog.testing.PerformanceTable.prototype.reportError = function(reason) {
this.getTableBody_().appendChild(
goog.dom.createDom('tr', null,
goog.dom.createDom('td', {'class': 'test-error', 'colSpan': 5},
String(reason))));
};

View File

@@ -0,0 +1,405 @@
// 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 Performance timer.
*
* {@see goog.testing.benchmark} for an easy way to use this functionality.
*
* @author attila@google.com (Attila Bodis)
*/
goog.provide('goog.testing.PerformanceTimer');
goog.provide('goog.testing.PerformanceTimer.Task');
goog.require('goog.array');
goog.require('goog.async.Deferred');
goog.require('goog.math');
/**
* Creates a performance timer that runs test functions a number of times to
* generate timing samples, and provides performance statistics (minimum,
* maximum, average, and standard deviation).
* @param {number=} opt_numSamples Number of times to run the test function;
* defaults to 10.
* @param {number=} opt_timeoutInterval Number of milliseconds after which the
* test is to be aborted; defaults to 5 seconds (5,000ms).
* @constructor
*/
goog.testing.PerformanceTimer = function(opt_numSamples, opt_timeoutInterval) {
/**
* Number of times the test function is to be run; defaults to 10.
* @type {number}
* @private
*/
this.numSamples_ = opt_numSamples || 10;
/**
* Number of milliseconds after which the test is to be aborted; defaults to
* 5,000ms.
* @type {number}
* @private
*/
this.timeoutInterval_ = opt_timeoutInterval || 5000;
/**
* Whether to discard outliers (i.e. the smallest and the largest values)
* from the sample set before computing statistics. Defaults to false.
* @type {boolean}
* @private
*/
this.discardOutliers_ = false;
};
/**
* @return {number} The number of times the test function will be run.
*/
goog.testing.PerformanceTimer.prototype.getNumSamples = function() {
return this.numSamples_;
};
/**
* Sets the number of times the test function will be run.
* @param {number} numSamples Number of times to run the test function.
*/
goog.testing.PerformanceTimer.prototype.setNumSamples = function(numSamples) {
this.numSamples_ = numSamples;
};
/**
* @return {number} The number of milliseconds after which the test times out.
*/
goog.testing.PerformanceTimer.prototype.getTimeoutInterval = function() {
return this.timeoutInterval_;
};
/**
* Sets the number of milliseconds after which the test times out.
* @param {number} timeoutInterval Timeout interval in ms.
*/
goog.testing.PerformanceTimer.prototype.setTimeoutInterval = function(
timeoutInterval) {
this.timeoutInterval_ = timeoutInterval;
};
/**
* Sets whether to ignore the smallest and the largest values when computing
* stats.
* @param {boolean} discard Whether to discard outlier values.
*/
goog.testing.PerformanceTimer.prototype.setDiscardOutliers = function(discard) {
this.discardOutliers_ = discard;
};
/**
* @return {boolean} Whether outlier values are discarded prior to computing
* stats.
*/
goog.testing.PerformanceTimer.prototype.isDiscardOutliers = function() {
return this.discardOutliers_;
};
/**
* Executes the test function the required number of times (or until the
* test run exceeds the timeout interval, whichever comes first). Returns
* an object containing the following:
* <pre>
* {
* 'average': average execution time (ms)
* 'count': number of executions (may be fewer than expected due to timeout)
* 'maximum': longest execution time (ms)
* 'minimum': shortest execution time (ms)
* 'standardDeviation': sample standard deviation (ms)
* 'total': total execution time (ms)
* }
* </pre>
*
* @param {Function} testFn Test function whose performance is to
* be measured.
* @return {Object} Object containing performance stats.
*/
goog.testing.PerformanceTimer.prototype.run = function(testFn) {
return this.runTask(new goog.testing.PerformanceTimer.Task(
/** @type {goog.testing.PerformanceTimer.TestFunction} */ (testFn)));
};
/**
* Executes the test function of the specified task as described in
* {@code run}. In addition, if specified, the set up and tear down functions of
* the task are invoked before and after each invocation of the test function.
* @see goog.testing.PerformanceTimer#run
* @param {goog.testing.PerformanceTimer.Task} task A task describing the test
* function to invoke.
* @return {Object} Object containing performance stats.
*/
goog.testing.PerformanceTimer.prototype.runTask = function(task) {
var samples = [];
var testStart = goog.now();
var totalRunTime = 0;
var testFn = task.getTest();
var setUpFn = task.getSetUp();
var tearDownFn = task.getTearDown();
for (var i = 0; i < this.numSamples_ && totalRunTime <= this.timeoutInterval_;
i++) {
setUpFn();
var sampleStart = goog.now();
testFn();
var sampleEnd = goog.now();
tearDownFn();
samples[i] = sampleEnd - sampleStart;
totalRunTime = sampleEnd - testStart;
}
return this.finishTask_(samples);
};
/**
* Finishes the run of a task by creating a result object from samples, in the
* format described in {@code run}.
* @see goog.testing.PerformanceTimer#run
* @return {Object} Object containing performance stats.
* @private
*/
goog.testing.PerformanceTimer.prototype.finishTask_ = function(samples) {
if (this.discardOutliers_ && samples.length > 2) {
goog.array.remove(samples, Math.min.apply(null, samples));
goog.array.remove(samples, Math.max.apply(null, samples));
}
return goog.testing.PerformanceTimer.createResults(samples);
};
/**
* Executes the test function of the specified task asynchronously. The test
* function is expected to take a callback as input and has to call it to signal
* that it's done. In addition, if specified, the setUp and tearDown functions
* of the task are invoked before and after each invocation of the test
* function. Note that setUp/tearDown functions take a callback as input and
* must call this callback when they are done.
* @see goog.testing.PerformanceTimer#run
* @param {goog.testing.PerformanceTimer.Task} task A task describing the test
* function to invoke.
* @return {!goog.async.Deferred} The deferred result, eventually an object
* containing performance stats.
*/
goog.testing.PerformanceTimer.prototype.runAsyncTask = function(task) {
var samples = [];
var testStart = goog.now();
var testFn = task.getTest();
var setUpFn = task.getSetUp();
var tearDownFn = task.getTearDown();
// Note that this uses a separate code path from runTask() because
// implementing runTask() in terms of runAsyncTask() could easily cause
// a stack overflow if there are many iterations.
var result = new goog.async.Deferred();
this.runAsyncTaskSample_(testFn, setUpFn, tearDownFn, result, samples,
testStart);
return result;
};
/**
* Runs a task once, waits for the test function to complete asynchronously
* and starts another run if not enough samples have been collected. Otherwise
* finishes this task.
* @param {goog.testing.PerformanceTimer.TestFunction} testFn The test function.
* @param {goog.testing.PerformanceTimer.TestFunction} setUpFn The set up
* function that will be called once before the test function is run.
* @param {goog.testing.PerformanceTimer.TestFunction} tearDownFn The set up
* function that will be called once after the test function completed.
* @param {!goog.async.Deferred} result The deferred result, eventually an
* object containing performance stats.
* @param {!Array.<number>} samples The time samples from all runs of the test
* function so far.
* @param {number} testStart The timestamp when the first sample was started.
* @private
*/
goog.testing.PerformanceTimer.prototype.runAsyncTaskSample_ = function(testFn,
setUpFn, tearDownFn, result, samples, testStart) {
var timer = this;
timer.handleOptionalDeferred_(setUpFn, function() {
var sampleStart = goog.now();
timer.handleOptionalDeferred_(testFn, function() {
var sampleEnd = goog.now();
timer.handleOptionalDeferred_(tearDownFn, function() {
samples.push(sampleEnd - sampleStart);
var totalRunTime = sampleEnd - testStart;
if (samples.length < timer.numSamples_ &&
totalRunTime <= timer.timeoutInterval_) {
timer.runAsyncTaskSample_(testFn, setUpFn, tearDownFn, result,
samples, testStart);
} else {
result.callback(timer.finishTask_(samples));
}
});
});
});
};
/**
* Execute a function that optionally returns a deferred object and continue
* with the given continuation function only once the deferred object has a
* result.
* @param {goog.testing.PerformanceTimer.TestFunction} deferredFactory The
* function that optionally returns a deferred object.
* @param {function()} continuationFunction The function that should be called
* after the optional deferred has a result.
* @private
*/
goog.testing.PerformanceTimer.prototype.handleOptionalDeferred_ = function(
deferredFactory, continuationFunction) {
var deferred = deferredFactory();
if (deferred) {
deferred.addCallback(continuationFunction);
} else {
continuationFunction();
}
};
/**
* Creates a performance timer results object by analyzing a given array of
* sample timings.
* @param {Array.<number>} samples The samples to analyze.
* @return {Object} Object containing performance stats.
*/
goog.testing.PerformanceTimer.createResults = function(samples) {
return {
'average': goog.math.average.apply(null, samples),
'count': samples.length,
'maximum': Math.max.apply(null, samples),
'minimum': Math.min.apply(null, samples),
'standardDeviation': goog.math.standardDeviation.apply(null, samples),
'total': goog.math.sum.apply(null, samples)
};
};
/**
* A test function whose performance should be measured or a setUp/tearDown
* function. It may optionally return a deferred object. If it does so, the
* test harness will assume the function is asynchronous and it must signal
* that it's done by setting an (empty) result on the deferred object. If the
* function doesn't return anything, the test harness will assume it's
* synchronous.
* @typedef {function():(goog.async.Deferred|undefined)}
*/
goog.testing.PerformanceTimer.TestFunction;
/**
* A task for the performance timer to measure. Callers can specify optional
* setUp and tearDown methods to control state before and after each run of the
* test function.
* @param {goog.testing.PerformanceTimer.TestFunction} test Test function whose
* performance is to be measured.
* @constructor
*/
goog.testing.PerformanceTimer.Task = function(test) {
/**
* The test function to time.
* @type {goog.testing.PerformanceTimer.TestFunction}
* @private
*/
this.test_ = test;
};
/**
* An optional set up function to run before each invocation of the test
* function.
* @type {goog.testing.PerformanceTimer.TestFunction}
* @private
*/
goog.testing.PerformanceTimer.Task.prototype.setUp_ = goog.nullFunction;
/**
* An optional tear down function to run after each invocation of the test
* function.
* @type {goog.testing.PerformanceTimer.TestFunction}
* @private
*/
goog.testing.PerformanceTimer.Task.prototype.tearDown_ = goog.nullFunction;
/**
* @return {goog.testing.PerformanceTimer.TestFunction} The test function to
* time.
*/
goog.testing.PerformanceTimer.Task.prototype.getTest = function() {
return this.test_;
};
/**
* Specifies a set up function to be invoked before each invocation of the test
* function.
* @param {goog.testing.PerformanceTimer.TestFunction} setUp The set up
* function.
* @return {goog.testing.PerformanceTimer.Task} This task.
*/
goog.testing.PerformanceTimer.Task.prototype.withSetUp = function(setUp) {
this.setUp_ = setUp;
return this;
};
/**
* @return {goog.testing.PerformanceTimer.TestFunction} The set up function or
* the default no-op function if none was specified.
*/
goog.testing.PerformanceTimer.Task.prototype.getSetUp = function() {
return this.setUp_;
};
/**
* Specifies a tear down function to be invoked after each invocation of the
* test function.
* @param {goog.testing.PerformanceTimer.TestFunction} tearDown The tear down
* function.
* @return {goog.testing.PerformanceTimer.Task} This task.
*/
goog.testing.PerformanceTimer.Task.prototype.withTearDown = function(tearDown) {
this.tearDown_ = tearDown;
return this;
};
/**
* @return {goog.testing.PerformanceTimer.TestFunction} The tear down function
* or the default no-op function if none was specified.
*/
goog.testing.PerformanceTimer.Task.prototype.getTearDown = function() {
return this.tearDown_;
};

View File

@@ -0,0 +1,243 @@
// 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 for creating stubs for testing.
*
*/
goog.provide('goog.testing.PropertyReplacer');
goog.require('goog.userAgent');
/**
* Helper class for stubbing out variables and object properties for unit tests.
* This class can change the value of some variables before running the test
* cases, and to reset them in the tearDown phase.
* See googletest.StubOutForTesting as an analogy in Python:
* http://protobuf.googlecode.com/svn/trunk/python/stubout.py
*
* Example usage:
* <pre>var stubs = new goog.testing.PropertyReplacer();
*
* function setUp() {
* // Mock functions used in all test cases.
* stubs.set(Math, 'random', function() {
* return 4; // Chosen by fair dice roll. Guaranteed to be random.
* });
* }
*
* function tearDown() {
* stubs.reset();
* }
*
* function testThreeDice() {
* // Mock a constant used only in this test case.
* stubs.set(goog.global, 'DICE_COUNT', 3);
* assertEquals(12, rollAllDice());
* }</pre>
*
* Constraints on altered objects:
* <ul>
* <li>DOM subclasses aren't supported.
* <li>The value of the objects' constructor property must either be equal to
* the real constructor or kept untouched.
* </ul>
*
* @constructor
*/
goog.testing.PropertyReplacer = function() {
/**
* Stores the values changed by the set() method in chronological order.
* Its items are objects with 3 fields: 'object', 'key', 'value'. The
* original value for the given key in the given object is stored under the
* 'value' key.
* @type {Array.<Object>}
* @private
*/
this.original_ = [];
};
/**
* Indicates that a key didn't exist before having been set by the set() method.
* @type {Object}
* @private
*/
goog.testing.PropertyReplacer.NO_SUCH_KEY_ = {};
/**
* Tells if the given key exists in the object. Ignores inherited fields.
* @param {Object|Function} obj The JavaScript or native object or function
* whose key is to be checked.
* @param {string} key The key to check.
* @return {boolean} Whether the object has the key as own key.
* @private
*/
goog.testing.PropertyReplacer.hasKey_ = function(obj, key) {
if (!(key in obj)) {
return false;
}
// hasOwnProperty is only reliable with JavaScript objects. It returns false
// for built-in DOM attributes.
if (Object.prototype.hasOwnProperty.call(obj, key)) {
return true;
}
// In all browsers except Opera obj.constructor never equals to Object if
// obj is an instance of a native class. In Opera we have to fall back on
// examining obj.toString().
if (obj.constructor == Object &&
(!goog.userAgent.OPERA ||
Object.prototype.toString.call(obj) == '[object Object]')) {
return false;
}
try {
// Firefox hack to consider "className" part of the HTML elements or
// "body" part of document. Although they are defined in the prototype of
// HTMLElement or Document, accessing them this way throws an exception.
// <pre>
// var dummy = document.body.constructor.prototype.className
// [Exception... "Cannot modify properties of a WrappedNative"]
// </pre>
var dummy = obj.constructor.prototype[key];
} catch (e) {
return true;
}
return !(key in obj.constructor.prototype);
};
/**
* Deletes a key from an object. Sets it to undefined or empty string if the
* delete failed.
* @param {Object|Function} obj The object or function to delete a key from.
* @param {string} key The key to delete.
* @private
*/
goog.testing.PropertyReplacer.deleteKey_ = function(obj, key) {
try {
delete obj[key];
// Delete has no effect for built-in properties of DOM nodes in FF.
if (!goog.testing.PropertyReplacer.hasKey_(obj, key)) {
return;
}
} catch (e) {
// IE throws TypeError when trying to delete properties of native objects
// (e.g. DOM nodes or window), even if they have been added by JavaScript.
}
obj[key] = undefined;
if (obj[key] == 'undefined') {
// Some properties such as className in IE are always evaluated as string
// so undefined will become 'undefined'.
obj[key] = '';
}
};
/**
* Adds or changes a value in an object while saving its original state.
* @param {Object|Function} obj The JavaScript or native object or function to
* alter. See the constraints in the class description.
* @param {string} key The key to change the value for.
* @param {*} value The new value to set.
*/
goog.testing.PropertyReplacer.prototype.set = function(obj, key, value) {
var origValue = goog.testing.PropertyReplacer.hasKey_(obj, key) ? obj[key] :
goog.testing.PropertyReplacer.NO_SUCH_KEY_;
this.original_.push({object: obj, key: key, value: origValue});
obj[key] = value;
};
/**
* Changes an existing value in an object to another one of the same type while
* saving its original state. The advantage of {@code replace} over {@link #set}
* is that {@code replace} protects against typos and erroneously passing tests
* after some members have been renamed during a refactoring.
* @param {Object|Function} obj The JavaScript or native object or function to
* alter. See the constraints in the class description.
* @param {string} key The key to change the value for. It has to be present
* either in {@code obj} or in its prototype chain.
* @param {*} value The new value to set. It has to have the same type as the
* original value. The types are compared with {@link goog.typeOf}.
* @throws {Error} In case of missing key or type mismatch.
*/
goog.testing.PropertyReplacer.prototype.replace = function(obj, key, value) {
if (!(key in obj)) {
throw Error('Cannot replace missing property "' + key + '" in ' + obj);
}
if (goog.typeOf(obj[key]) != goog.typeOf(value)) {
throw Error('Cannot replace property "' + key + '" in ' + obj +
' with a value of different type');
}
this.set(obj, key, value);
};
/**
* Builds an object structure for the provided namespace path. Doesn't
* overwrite those prefixes of the path that are already objects or functions.
* @param {string} path The path to create or alter, e.g. 'goog.ui.Menu'.
* @param {*} value The value to set.
*/
goog.testing.PropertyReplacer.prototype.setPath = function(path, value) {
var parts = path.split('.');
var obj = goog.global;
for (var i = 0; i < parts.length - 1; i++) {
var part = parts[i];
if (part == 'prototype' && !obj[part]) {
throw Error('Cannot set the prototype of ' + parts.slice(0, i).join('.'));
}
if (!goog.isObject(obj[part]) && !goog.isFunction(obj[part])) {
this.set(obj, part, {});
}
obj = obj[part];
}
this.set(obj, parts[parts.length - 1], value);
};
/**
* Deletes the key from the object while saving its original value.
* @param {Object|Function} obj The JavaScript or native object or function to
* alter. See the constraints in the class description.
* @param {string} key The key to delete.
*/
goog.testing.PropertyReplacer.prototype.remove = function(obj, key) {
if (goog.testing.PropertyReplacer.hasKey_(obj, key)) {
this.original_.push({object: obj, key: key, value: obj[key]});
goog.testing.PropertyReplacer.deleteKey_(obj, key);
}
};
/**
* Resets all changes made by goog.testing.PropertyReplacer.prototype.set.
*/
goog.testing.PropertyReplacer.prototype.reset = function() {
for (var i = this.original_.length - 1; i >= 0; i--) {
var original = this.original_[i];
if (original.value == goog.testing.PropertyReplacer.NO_SUCH_KEY_) {
goog.testing.PropertyReplacer.deleteKey_(original.object, original.key);
} else {
original.object[original.key] = original.value;
}
delete this.original_[i];
}
this.original_.length = 0;
};

View File

@@ -0,0 +1,127 @@
// 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 Test helpers to compare goog.proto2.Messages.
*
*/
goog.provide('goog.testing.proto2');
goog.require('goog.proto2.Message');
goog.require('goog.testing.asserts');
/**
* Compares two goog.proto2.Message instances of the same type.
* @param {!goog.proto2.Message} expected First message.
* @param {!goog.proto2.Message} actual Second message.
* @param {string} path Path to the messages.
* @return {string} A string describing where they differ. Empty string if they
* are equal.
* @private
*/
goog.testing.proto2.findDifferences_ = function(expected, actual, path) {
var fields = expected.getDescriptor().getFields();
for (var i = 0; i < fields.length; i++) {
var field = fields[i];
var newPath = (path ? path + '/' : '') + field.getName();
if (expected.has(field) && !actual.has(field)) {
return newPath + ' should be present';
}
if (!expected.has(field) && actual.has(field)) {
return newPath + ' should not be present';
}
if (expected.has(field)) {
var isComposite = field.isCompositeType();
if (field.isRepeated()) {
var expectedCount = expected.countOf(field);
var actualCount = actual.countOf(field);
if (expectedCount != actualCount) {
return newPath + ' should have ' + expectedCount + ' items, ' +
'but has ' + actualCount;
}
for (var j = 0; j < expectedCount; j++) {
var expectedItem = expected.get(field, j);
var actualItem = actual.get(field, j);
if (isComposite) {
var itemDiff = goog.testing.proto2.findDifferences_(
/** @type {!goog.proto2.Message} */ (expectedItem),
/** @type {!goog.proto2.Message} */ (actualItem),
newPath + '[' + j + ']');
if (itemDiff) {
return itemDiff;
}
} else {
if (expectedItem != actualItem) {
return newPath + '[' + j + '] should be ' + expectedItem +
', but was ' + actualItem;
}
}
}
} else {
var expectedValue = expected.get(field);
var actualValue = actual.get(field);
if (isComposite) {
var diff = goog.testing.proto2.findDifferences_(
/** @type {!goog.proto2.Message} */ (expectedValue),
/** @type {!goog.proto2.Message} */ (actualValue),
newPath);
if (diff) {
return diff;
}
} else {
if (expectedValue != actualValue) {
return newPath + ' should be ' + expectedValue + ', but was ' +
actualValue;
}
}
}
}
}
return '';
};
/**
* Compares two goog.proto2.Message objects. Gives more readable output than
* assertObjectEquals on mismatch.
* @param {!goog.proto2.Message} expected Expected proto2 message.
* @param {!goog.proto2.Message} actual Actual proto2 message.
* @param {string=} opt_failureMessage Failure message when the values don't
* match.
*/
goog.testing.proto2.assertEquals = function(expected, actual,
opt_failureMessage) {
var failureSummary = opt_failureMessage || '';
if (!(expected instanceof goog.proto2.Message) ||
!(actual instanceof goog.proto2.Message)) {
goog.testing.asserts.raiseException(failureSummary,
'Bad arguments were passed to goog.testing.proto2.assertEquals');
}
if (expected.constructor != actual.constructor) {
goog.testing.asserts.raiseException(failureSummary,
'Message type mismatch: ' + expected.getDescriptor().getFullName() +
' != ' + actual.getDescriptor().getFullName());
}
var diff = goog.testing.proto2.findDifferences_(expected, actual, '');
if (diff) {
goog.testing.asserts.raiseException(failureSummary, diff);
}
};

View File

@@ -0,0 +1,179 @@
// 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 PseudoRandom provides a mechanism for generating deterministic
* psuedo random numbers based on a seed. Based on the Park-Miller algorithm.
* See http://dx.doi.org/10.1145%2F63039.63042 for details.
*
*/
goog.provide('goog.testing.PseudoRandom');
goog.require('goog.Disposable');
/**
* Class for unit testing code that uses Math.random. Generates deterministic
* random numbers.
*
* @param {number=} opt_seed The seed to use.
* @param {boolean=} opt_install Whether to install the PseudoRandom at
* construction time.
* @extends {goog.Disposable}
* @constructor
*/
goog.testing.PseudoRandom = function(opt_seed, opt_install) {
goog.Disposable.call(this);
if (!goog.isDef(opt_seed)) {
opt_seed = goog.testing.PseudoRandom.seedUniquifier_++ + goog.now();
}
this.seed(opt_seed);
if (opt_install) {
this.install();
}
};
goog.inherits(goog.testing.PseudoRandom, goog.Disposable);
/**
* Helps create a unique seed.
* @type {number}
* @private
*/
goog.testing.PseudoRandom.seedUniquifier_ = 0;
/**
* Constant used as part of the algorithm.
* @type {number}
*/
goog.testing.PseudoRandom.A = 48271;
/**
* Constant used as part of the algorithm. 2^31 - 1.
* @type {number}
*/
goog.testing.PseudoRandom.M = 2147483647;
/**
* Constant used as part of the algorithm. It is equal to M / A.
* @type {number}
*/
goog.testing.PseudoRandom.Q = 44488;
/**
* Constant used as part of the algorithm. It is equal to M % A.
* @type {number}
*/
goog.testing.PseudoRandom.R = 3399;
/**
* Constant used as part of the algorithm to get values from range [0, 1).
* @type {number}
*/
goog.testing.PseudoRandom.ONE_OVER_M_MINUS_ONE =
1.0 / (goog.testing.PseudoRandom.M - 1);
/**
* The seed of the random sequence and also the next returned value (before
* normalization). Must be between 1 and M - 1 (inclusive).
* @type {number}
* @private
*/
goog.testing.PseudoRandom.prototype.seed_ = 1;
/**
* Whether this PseudoRandom has been installed.
* @type {boolean}
* @private
*/
goog.testing.PseudoRandom.prototype.installed_;
/**
* The original Math.random function.
* @type {function(): number}
* @private
*/
goog.testing.PseudoRandom.prototype.mathRandom_;
/**
* Installs this PseudoRandom as the system number generator.
*/
goog.testing.PseudoRandom.prototype.install = function() {
if (!this.installed_) {
this.mathRandom_ = Math.random;
Math.random = goog.bind(this.random, this);
this.installed_ = true;
}
};
/** @override */
goog.testing.PseudoRandom.prototype.disposeInternal = function() {
goog.testing.PseudoRandom.superClass_.disposeInternal.call(this);
this.uninstall();
};
/**
* Uninstalls the PseudoRandom.
*/
goog.testing.PseudoRandom.prototype.uninstall = function() {
if (this.installed_) {
Math.random = this.mathRandom_;
this.installed_ = false;
}
};
/**
* Seed the generator.
*
* @param {number=} seed The seed to use.
*/
goog.testing.PseudoRandom.prototype.seed = function(seed) {
this.seed_ = seed % (goog.testing.PseudoRandom.M - 1);
if (this.seed_ <= 0) {
this.seed_ += goog.testing.PseudoRandom.M - 1;
}
};
/**
* @return {number} The next number in the sequence.
*/
goog.testing.PseudoRandom.prototype.random = function() {
var hi = Math.floor(this.seed_ / goog.testing.PseudoRandom.Q);
var lo = this.seed_ % goog.testing.PseudoRandom.Q;
var test = goog.testing.PseudoRandom.A * lo -
goog.testing.PseudoRandom.R * hi;
if (test > 0) {
this.seed_ = test;
} else {
this.seed_ = test + goog.testing.PseudoRandom.M;
}
return (this.seed_ - 1) * goog.testing.PseudoRandom.ONE_OVER_M_MINUS_ONE;
};

View File

@@ -0,0 +1,201 @@
// 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 Helper class for recording the calls of a function.
*
* Example:
* <pre>
* var stubs = new goog.testing.PropertyReplacer();
*
* function tearDown() {
* stubs.reset();
* }
*
* function testShuffle() {
* stubs.set(Math, 'random', goog.testing.recordFunction(Math.random));
* var arr = shuffle([1, 2, 3, 4, 5]);
* assertSameElements([1, 2, 3, 4, 5], arr);
* assertEquals(4, Math.random.getCallCount());
* }
*
* function testOpenDialog() {
* stubs.set(goog.ui, 'Dialog',
* goog.testing.recordConstructor(goog.ui.Dialog));
* openConfirmDialog();
* var lastDialogInstance = goog.ui.Dialog.getLastCall().getThis();
* assertEquals('confirm', lastDialogInstance.getTitle());
* }
* </pre>
*
*/
goog.provide('goog.testing.FunctionCall');
goog.provide('goog.testing.recordConstructor');
goog.provide('goog.testing.recordFunction');
/**
* Wraps the function into another one which calls the inner function and
* records its calls. The recorded function will have 3 static methods:
* {@code getCallCount}, {@code getCalls} and {@code getLastCall} but won't
* inherit the original function's prototype and static fields.
*
* @param {!Function=} opt_f The function to wrap and record. Defaults to
* {@link goog.nullFunction}.
* @return {!Function} The wrapped function.
*/
goog.testing.recordFunction = function(opt_f) {
var f = opt_f || goog.nullFunction;
var calls = [];
function recordedFunction() {
try {
var ret = f.apply(this, arguments);
calls.push(new goog.testing.FunctionCall(f, this, arguments, ret, null));
return ret;
} catch (err) {
calls.push(new goog.testing.FunctionCall(f, this, arguments, undefined,
err));
throw err;
}
}
/**
* @return {number} Total number of calls.
*/
recordedFunction.getCallCount = function() {
return calls.length;
};
/**
* @return {!Array.<!goog.testing.FunctionCall>} All calls of the recorded
* function.
*/
recordedFunction.getCalls = function() {
return calls;
};
/**
* @return {goog.testing.FunctionCall} Last call of the recorded function or
* null if it hasn't been called.
*/
recordedFunction.getLastCall = function() {
return calls[calls.length - 1] || null;
};
/**
* Returns and removes the last call of the recorded function.
* @return {goog.testing.FunctionCall} Last call of the recorded function or
* null if it hasn't been called.
*/
recordedFunction.popLastCall = function() {
return calls.pop() || null;
};
/**
* Resets the recorded function and removes all calls.
*/
recordedFunction.reset = function() {
calls.length = 0;
};
return recordedFunction;
};
/**
* Same as {@link goog.testing.recordFunction} but the recorded function will
* have the same prototype and static fields as the original one. It can be
* used with constructors.
*
* @param {!Function} ctor The function to wrap and record.
* @return {!Function} The wrapped function.
*/
goog.testing.recordConstructor = function(ctor) {
var recordedConstructor = goog.testing.recordFunction(ctor);
recordedConstructor.prototype = ctor.prototype;
goog.mixin(recordedConstructor, ctor);
return recordedConstructor;
};
/**
* Struct for a single function call.
* @param {!Function} func The called function.
* @param {!Object} thisContext {@code this} context of called function.
* @param {!Arguments} args Arguments of the called function.
* @param {*} ret Return value of the function or undefined in case of error.
* @param {*} error The error thrown by the function or null if none.
* @constructor
*/
goog.testing.FunctionCall = function(func, thisContext, args, ret, error) {
this.function_ = func;
this.thisContext_ = thisContext;
this.arguments_ = Array.prototype.slice.call(args);
this.returnValue_ = ret;
this.error_ = error;
};
/**
* @return {!Function} The called function.
*/
goog.testing.FunctionCall.prototype.getFunction = function() {
return this.function_;
};
/**
* @return {!Object} {@code this} context of called function. It is the same as
* the created object if the function is a constructor.
*/
goog.testing.FunctionCall.prototype.getThis = function() {
return this.thisContext_;
};
/**
* @return {!Array} Arguments of the called function.
*/
goog.testing.FunctionCall.prototype.getArguments = function() {
return this.arguments_;
};
/**
* Returns the nth argument of the called function.
* @param {number} index 0-based index of the argument.
* @return {*} The argument value or undefined if there is no such argument.
*/
goog.testing.FunctionCall.prototype.getArgument = function(index) {
return this.arguments_[index];
};
/**
* @return {*} Return value of the function or undefined in case of error.
*/
goog.testing.FunctionCall.prototype.getReturnValue = function() {
return this.returnValue_;
};
/**
* @return {*} The error thrown by the function or null if none.
*/
goog.testing.FunctionCall.prototype.getError = function() {
return this.error_;
};

View File

@@ -0,0 +1,122 @@
// 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 Utility for sharding tests.
*
* Usage instructions:
* <ol>
* <li>Instead of writing your large test in foo_test.html, write it in
* foo_test_template.html</li>
* <li>Add a call to {@code goog.testing.ShardingTestCase.shardByFileName()}
* near the top of your test, before any test cases or setup methods.</li>
* <li>Symlink foo_test_template.html into different sharded test files
* named foo_1of4_test.html, foo_2of4_test.html, etc, using `ln -s`.</li>
* <li>Add the symlinks as foo_1of4_test.html.
* In perforce, run the command `g4 add foo_1of4_test.html` followed
* by `g4 reopen -t symlink foo_1of4_test.html` so that perforce treats the file
* as a symlink
* </li>
* </ol>
*
*/
goog.provide('goog.testing.ShardingTestCase');
goog.require('goog.asserts');
goog.require('goog.testing.TestCase');
/**
* A test case that runs tests in per-file shards.
* @param {number} shardIndex Shard index for this page,
* <strong>1-indexed</strong>.
* @param {number} numShards Number of shards to split up test cases into.
* @extends {goog.testing.TestCase}
* @constructor
*/
goog.testing.ShardingTestCase = function(shardIndex, numShards, opt_name) {
goog.base(this, opt_name);
goog.asserts.assert(shardIndex > 0, 'Shard index should be positive');
goog.asserts.assert(numShards > 0, 'Number of shards should be positive');
goog.asserts.assert(shardIndex <= numShards,
'Shard index out of bounds');
/**
* @type {number}
* @private
*/
this.shardIndex_ = shardIndex;
/**
* @type {number}
* @private
*/
this.numShards_ = numShards;
};
goog.inherits(goog.testing.ShardingTestCase, goog.testing.TestCase);
/**
* Whether we've actually partitioned the tests yet. We may execute twice
* ('Run again without reloading') without failing.
* @type {boolean}
* @private
*/
goog.testing.ShardingTestCase.prototype.sharded_ = false;
/**
* Installs a runTests global function that goog.testing.JsUnit will use to
* run tests, which will run a single shard of the tests present on the page.
* @override
*/
goog.testing.ShardingTestCase.prototype.runTests = function() {
if (!this.sharded_) {
var numTests = this.getCount();
goog.asserts.assert(numTests >= this.numShards_,
'Must have at least as many tests as shards!');
var shardSize = Math.ceil(numTests / this.numShards_);
var startIndex = (this.shardIndex_ - 1) * shardSize;
var endIndex = startIndex + shardSize;
goog.asserts.assert(this.order == goog.testing.TestCase.Order.SORTED,
'Only SORTED order is allowed for sharded tests');
this.setTests(this.getTests().slice(startIndex, endIndex));
this.sharded_ = true;
}
// Call original runTests method to execute the tests.
goog.base(this, 'runTests');
};
/**
* Shards tests based on the test filename. Assumes that the filename is
* formatted like 'foo_1of5_test.html'.
* @param {string=} opt_name A descriptive name for the test case.
*/
goog.testing.ShardingTestCase.shardByFileName = function(opt_name) {
var path = window.location.pathname;
var shardMatch = path.match(/_(\d+)of(\d+)_test\.html/);
goog.asserts.assert(shardMatch,
'Filename must be of the form "foo_1of5_test.html"');
var shardIndex = parseInt(shardMatch[1], 10);
var numShards = parseInt(shardMatch[2], 10);
var testCase = new goog.testing.ShardingTestCase(
shardIndex, numShards, opt_name);
goog.testing.TestCase.initializeTestRunner(testCase);
};

View File

@@ -0,0 +1,46 @@
// 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 This module simplifies testing code which uses stateful
* singletons. {@code goog.testing.singleton.reset} resets all instances, so
* next time when {@code getInstance} is called, a new instance is created.
* It's recommended to reset the singletons in {@code tearDown} to prevent
* interference between subsequent tests.
*
* The {@code goog.testing.singleton} functions expect that the goog.DEBUG flag
* is enabled, and the tests are either uncompiled or compiled without renaming.
*
*/
goog.provide('goog.testing.singleton');
/**
* Deletes all singleton instances, so {@code getInstance} will return a new
* instance on next call.
*/
goog.testing.singleton.reset = function() {
var singletons = goog.getObjectByName('goog.instantiatedSingletons_');
var ctor;
while (ctor = singletons.pop()) {
delete ctor.instance_;
}
};
/**
* @deprecated Please use {@code goog.addSingletonGetter}.
*/
goog.testing.singleton.addSingletonGetter = goog.addSingletonGetter;

View File

@@ -0,0 +1,555 @@
// 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 Tools for parsing and pretty printing error stack traces.
*
*/
goog.provide('goog.testing.stacktrace');
goog.provide('goog.testing.stacktrace.Frame');
/**
* Class representing one stack frame.
* @param {string} context Context object, empty in case of global functions or
* if the browser doesn't provide this information.
* @param {string} name Function name, empty in case of anonymous functions.
* @param {string} alias Alias of the function if available. For example the
* function name will be 'c' and the alias will be 'b' if the function is
* defined as <code>a.b = function c() {};</code>.
* @param {string} args Arguments of the function in parentheses if available.
* @param {string} path File path or URL including line number and optionally
* column number separated by colons.
* @constructor
*/
goog.testing.stacktrace.Frame = function(context, name, alias, args, path) {
this.context_ = context;
this.name_ = name;
this.alias_ = alias;
this.args_ = args;
this.path_ = path;
};
/**
* @return {string} The function name or empty string if the function is
* anonymous and the object field which it's assigned to is unknown.
*/
goog.testing.stacktrace.Frame.prototype.getName = function() {
return this.name_;
};
/**
* @return {boolean} Whether the stack frame contains an anonymous function.
*/
goog.testing.stacktrace.Frame.prototype.isAnonymous = function() {
return !this.name_ || this.context_ == '[object Object]';
};
/**
* Brings one frame of the stack trace into a common format across browsers.
* @return {string} Pretty printed stack frame.
*/
goog.testing.stacktrace.Frame.prototype.toCanonicalString = function() {
var htmlEscape = goog.testing.stacktrace.htmlEscape_;
var deobfuscate = goog.testing.stacktrace.maybeDeobfuscateFunctionName_;
var canonical = [
this.context_ ? htmlEscape(this.context_) + '.' : '',
this.name_ ? htmlEscape(deobfuscate(this.name_)) : 'anonymous',
htmlEscape(this.args_),
this.alias_ ? ' [as ' + htmlEscape(deobfuscate(this.alias_)) + ']' : ''
];
if (this.path_) {
canonical.push(' at ');
// If Closure Inspector is installed and running, then convert the line
// into a source link for displaying the code in Firebug.
if (goog.testing.stacktrace.isClosureInspectorActive_()) {
var lineNumber = this.path_.match(/\d+$/)[0];
canonical.push('<a href="" onclick="CLOSURE_INSPECTOR___.showLine(\'',
htmlEscape(this.path_), '\', \'', lineNumber, '\'); return false">',
htmlEscape(this.path_), '</a>');
} else {
canonical.push(htmlEscape(this.path_));
}
}
return canonical.join('');
};
/**
* Maximum number of steps while the call chain is followed.
* @type {number}
* @private
*/
goog.testing.stacktrace.MAX_DEPTH_ = 20;
/**
* Maximum length of a string that can be matched with a RegExp on
* Firefox 3x. Exceeding this approximate length will cause string.match
* to exceed Firefox's stack quota. This situation can be encountered
* when goog.globalEval is invoked with a long argument; such as
* when loading a module.
* @type {number}
* @private
*/
goog.testing.stacktrace.MAX_FIREFOX_FRAMESTRING_LENGTH_ = 500000;
/**
* RegExp pattern for JavaScript identifiers. We don't support Unicode
* identifiers defined in ECMAScript v3.
* @type {string}
* @private
*/
goog.testing.stacktrace.IDENTIFIER_PATTERN_ = '[a-zA-Z_$][\\w$]*';
/**
* RegExp pattern for function name alias in the Chrome stack trace.
* @type {string}
* @private
*/
goog.testing.stacktrace.CHROME_ALIAS_PATTERN_ =
'(?: \\[as (' + goog.testing.stacktrace.IDENTIFIER_PATTERN_ + ')\\])?';
/**
* RegExp pattern for function names and constructor calls in the Chrome stack
* trace.
* @type {string}
* @private
*/
goog.testing.stacktrace.CHROME_FUNCTION_NAME_PATTERN_ =
'(?:new )?(?:' + goog.testing.stacktrace.IDENTIFIER_PATTERN_ +
'|<anonymous>)';
/**
* RegExp pattern for function call in the Chrome stack trace.
* Creates 3 submatches with context object (optional), function name and
* function alias (optional).
* @type {string}
* @private
*/
goog.testing.stacktrace.CHROME_FUNCTION_CALL_PATTERN_ =
' (?:(.*?)\\.)?(' + goog.testing.stacktrace.CHROME_FUNCTION_NAME_PATTERN_ +
')' + goog.testing.stacktrace.CHROME_ALIAS_PATTERN_;
/**
* RegExp pattern for an URL + position inside the file.
* @type {string}
* @private
*/
goog.testing.stacktrace.URL_PATTERN_ =
'((?:http|https|file)://[^\\s)]+|javascript:.*)';
/**
* RegExp pattern for an URL + line number + column number in Chrome.
* The URL is either in submatch 1 or submatch 2.
* @type {string}
* @private
*/
goog.testing.stacktrace.CHROME_URL_PATTERN_ = ' (?:' +
'\\(unknown source\\)' + '|' +
'\\(native\\)' + '|' +
'\\((?:eval at )?' + goog.testing.stacktrace.URL_PATTERN_ + '\\)' + '|' +
goog.testing.stacktrace.URL_PATTERN_ + ')';
/**
* Regular expression for parsing one stack frame in Chrome.
* @type {!RegExp}
* @private
*/
goog.testing.stacktrace.CHROME_STACK_FRAME_REGEXP_ = new RegExp('^ at' +
'(?:' + goog.testing.stacktrace.CHROME_FUNCTION_CALL_PATTERN_ + ')?' +
goog.testing.stacktrace.CHROME_URL_PATTERN_ + '$');
/**
* RegExp pattern for function call in the Firefox stack trace.
* Creates 2 submatches with function name (optional) and arguments.
* @type {string}
* @private
*/
goog.testing.stacktrace.FIREFOX_FUNCTION_CALL_PATTERN_ =
'(' + goog.testing.stacktrace.IDENTIFIER_PATTERN_ + ')?' +
'(\\(.*\\))?@';
/**
* Regular expression for parsing one stack frame in Firefox.
* @type {!RegExp}
* @private
*/
goog.testing.stacktrace.FIREFOX_STACK_FRAME_REGEXP_ = new RegExp('^' +
goog.testing.stacktrace.FIREFOX_FUNCTION_CALL_PATTERN_ +
'(?::0|' + goog.testing.stacktrace.URL_PATTERN_ + ')$');
/**
* RegExp pattern for an anonymous function call in an Opera stack frame.
* Creates 2 (optional) submatches: the context object and function name.
* @type {string}
* @const
* @private
*/
goog.testing.stacktrace.OPERA_ANONYMOUS_FUNCTION_NAME_PATTERN_ =
'<anonymous function(?:\\: ' +
'(?:(' + goog.testing.stacktrace.IDENTIFIER_PATTERN_ +
'(?:\\.' + goog.testing.stacktrace.IDENTIFIER_PATTERN_ + ')*)\\.)?' +
'(' + goog.testing.stacktrace.IDENTIFIER_PATTERN_ + '))?>';
/**
* RegExp pattern for a function call in an Opera stack frame.
* Creates 4 (optional) submatches: the function name (if not anonymous),
* the aliased context object and function name (if anonymous), and the
* function call arguments.
* @type {string}
* @const
* @private
*/
goog.testing.stacktrace.OPERA_FUNCTION_CALL_PATTERN_ =
'(?:(?:(' + goog.testing.stacktrace.IDENTIFIER_PATTERN_ + ')|' +
goog.testing.stacktrace.OPERA_ANONYMOUS_FUNCTION_NAME_PATTERN_ +
')(\\(.*\\)))?@';
/**
* Regular expression for parsing on stack frame in Opera 11.68+
* @type {!RegExp}
* @const
* @private
*/
goog.testing.stacktrace.OPERA_STACK_FRAME_REGEXP_ = new RegExp('^' +
goog.testing.stacktrace.OPERA_FUNCTION_CALL_PATTERN_ +
goog.testing.stacktrace.URL_PATTERN_ + '?$');
/**
* Regular expression for finding the function name in its source.
* @type {!RegExp}
* @private
*/
goog.testing.stacktrace.FUNCTION_SOURCE_REGEXP_ = new RegExp(
'^function (' + goog.testing.stacktrace.IDENTIFIER_PATTERN_ + ')');
/**
* RegExp pattern for function call in a IE stack trace. This expression allows
* for identifiers like 'Anonymous function', 'eval code', and 'Global code'.
* @type {string}
* @const
* @private
*/
goog.testing.stacktrace.IE_FUNCTION_CALL_PATTERN_ = '(' +
goog.testing.stacktrace.IDENTIFIER_PATTERN_ + '(?:\\s+\\w+)*)';
/**
* Regular expression for parsing a stack frame in IE.
* @type {!RegExp}
* @const
* @private
*/
goog.testing.stacktrace.IE_STACK_FRAME_REGEXP_ = new RegExp('^ at ' +
goog.testing.stacktrace.IE_FUNCTION_CALL_PATTERN_ +
'\\s*\\((eval code:[^)]*|' + goog.testing.stacktrace.URL_PATTERN_ +
')\\)?$');
/**
* Creates a stack trace by following the call chain. Based on
* {@link goog.debug.getStacktrace}.
* @return {!Array.<!goog.testing.stacktrace.Frame>} Stack frames.
* @private
*/
goog.testing.stacktrace.followCallChain_ = function() {
var frames = [];
var fn = arguments.callee.caller;
var depth = 0;
while (fn && depth < goog.testing.stacktrace.MAX_DEPTH_) {
var fnString = Function.prototype.toString.call(fn);
var match = fnString.match(goog.testing.stacktrace.FUNCTION_SOURCE_REGEXP_);
var functionName = match ? match[1] : '';
var argsBuilder = ['('];
if (fn.arguments) {
for (var i = 0; i < fn.arguments.length; i++) {
var arg = fn.arguments[i];
if (i > 0) {
argsBuilder.push(', ');
}
if (goog.isString(arg)) {
argsBuilder.push('"', arg, '"');
} else {
// Some args are mocks, and we don't want to fail from them not having
// expected a call to toString, so instead insert a static string.
if (arg && arg['$replay']) {
argsBuilder.push('goog.testing.Mock');
} else {
argsBuilder.push(String(arg));
}
}
}
} else {
// Opera 10 doesn't know the arguments of native functions.
argsBuilder.push('unknown');
}
argsBuilder.push(')');
var args = argsBuilder.join('');
frames.push(new goog.testing.stacktrace.Frame('', functionName, '', args,
''));
/** @preserveTry */
try {
fn = fn.caller;
} catch (e) {
break;
}
depth++;
}
return frames;
};
/**
* Parses one stack frame.
* @param {string} frameStr The stack frame as string.
* @return {goog.testing.stacktrace.Frame} Stack frame object or null if the
* parsing failed.
* @private
*/
goog.testing.stacktrace.parseStackFrame_ = function(frameStr) {
var m = frameStr.match(goog.testing.stacktrace.CHROME_STACK_FRAME_REGEXP_);
if (m) {
return new goog.testing.stacktrace.Frame(m[1] || '', m[2] || '', m[3] || '',
'', m[4] || m[5] || '');
}
if (frameStr.length >
goog.testing.stacktrace.MAX_FIREFOX_FRAMESTRING_LENGTH_) {
return goog.testing.stacktrace.parseLongFirefoxFrame_(frameStr);
}
m = frameStr.match(goog.testing.stacktrace.FIREFOX_STACK_FRAME_REGEXP_);
if (m) {
return new goog.testing.stacktrace.Frame('', m[1] || '', '', m[2] || '',
m[3] || '');
}
m = frameStr.match(goog.testing.stacktrace.OPERA_STACK_FRAME_REGEXP_);
if (m) {
return new goog.testing.stacktrace.Frame(m[2] || '', m[1] || m[3] || '',
'', m[4] || '', m[5] || '');
}
m = frameStr.match(goog.testing.stacktrace.IE_STACK_FRAME_REGEXP_);
if (m) {
return new goog.testing.stacktrace.Frame('', m[1] || '', '', '',
m[2] || '');
}
return null;
};
/**
* Parses a long firefox stack frame.
* @param {string} frameStr The stack frame as string.
* @return {!goog.testing.stacktrace.Frame} Stack frame object.
* @private
*/
goog.testing.stacktrace.parseLongFirefoxFrame_ = function(frameStr) {
var firstParen = frameStr.indexOf('(');
var lastAmpersand = frameStr.lastIndexOf('@');
var lastColon = frameStr.lastIndexOf(':');
var functionName = '';
if ((firstParen >= 0) && (firstParen < lastAmpersand)) {
functionName = frameStr.substring(0, firstParen);
}
var loc = '';
if ((lastAmpersand >= 0) && (lastAmpersand + 1 < lastColon)) {
loc = frameStr.substring(lastAmpersand + 1);
}
var args = '';
if ((firstParen >= 0 && lastAmpersand > 0) &&
(firstParen < lastAmpersand)) {
args = frameStr.substring(firstParen, lastAmpersand);
}
return new goog.testing.stacktrace.Frame('', functionName, '', args, loc);
};
/**
* Function to deobfuscate function names.
* @type {function(string): string}
* @private
*/
goog.testing.stacktrace.deobfuscateFunctionName_;
/**
* Sets function to deobfuscate function names.
* @param {function(string): string} fn function to deobfuscate function names.
*/
goog.testing.stacktrace.setDeobfuscateFunctionName = function(fn) {
goog.testing.stacktrace.deobfuscateFunctionName_ = fn;
};
/**
* Deobfuscates a compiled function name with the function passed to
* {@link #setDeobfuscateFunctionName}. Returns the original function name if
* the deobfuscator hasn't been set.
* @param {string} name The function name to deobfuscate.
* @return {string} The deobfuscated function name.
* @private
*/
goog.testing.stacktrace.maybeDeobfuscateFunctionName_ = function(name) {
return goog.testing.stacktrace.deobfuscateFunctionName_ ?
goog.testing.stacktrace.deobfuscateFunctionName_(name) : name;
};
/**
* @return {boolean} Whether the Closure Inspector is active.
* @private
*/
goog.testing.stacktrace.isClosureInspectorActive_ = function() {
return Boolean(goog.global['CLOSURE_INSPECTOR___'] &&
goog.global['CLOSURE_INSPECTOR___']['supportsJSUnit']);
};
/**
* Escapes the special character in HTML.
* @param {string} text Plain text.
* @return {string} Escaped text.
* @private
*/
goog.testing.stacktrace.htmlEscape_ = function(text) {
return text.replace(/&/g, '&amp;').
replace(/</g, '&lt;').
replace(/>/g, '&gt;').
replace(/"/g, '&quot;');
};
/**
* Converts the stack frames into canonical format. Chops the beginning and the
* end of it which come from the testing environment, not from the test itself.
* @param {!Array.<goog.testing.stacktrace.Frame>} frames The frames.
* @return {string} Canonical, pretty printed stack trace.
* @private
*/
goog.testing.stacktrace.framesToString_ = function(frames) {
// Removes the anonymous calls from the end of the stack trace (they come
// from testrunner.js, testcase.js and asserts.js), so the stack trace will
// end with the test... method.
var lastIndex = frames.length - 1;
while (frames[lastIndex] && frames[lastIndex].isAnonymous()) {
lastIndex--;
}
// Removes the beginning of the stack trace until the call of the private
// _assert function (inclusive), so the stack trace will begin with a public
// asserter. Does nothing if _assert is not present in the stack trace.
var privateAssertIndex = -1;
for (var i = 0; i < frames.length; i++) {
if (frames[i] && frames[i].getName() == '_assert') {
privateAssertIndex = i;
break;
}
}
var canonical = [];
for (var i = privateAssertIndex + 1; i <= lastIndex; i++) {
canonical.push('> ');
if (frames[i]) {
canonical.push(frames[i].toCanonicalString());
} else {
canonical.push('(unknown)');
}
canonical.push('\n');
}
return canonical.join('');
};
/**
* Parses the browser's native stack trace.
* @param {string} stack Stack trace.
* @return {!Array.<goog.testing.stacktrace.Frame>} Stack frames. The
* unrecognized frames will be nulled out.
* @private
*/
goog.testing.stacktrace.parse_ = function(stack) {
var lines = stack.replace(/\s*$/, '').split('\n');
var frames = [];
for (var i = 0; i < lines.length; i++) {
frames.push(goog.testing.stacktrace.parseStackFrame_(lines[i]));
}
return frames;
};
/**
* Brings the stack trace into a common format across browsers.
* @param {string} stack Browser-specific stack trace.
* @return {string} Same stack trace in common format.
*/
goog.testing.stacktrace.canonicalize = function(stack) {
var frames = goog.testing.stacktrace.parse_(stack);
return goog.testing.stacktrace.framesToString_(frames);
};
/**
* Gets the native stack trace if available otherwise follows the call chain.
* @return {string} The stack trace in canonical format.
*/
goog.testing.stacktrace.get = function() {
var stack = '';
// IE10 will only create a stack trace when the Error is thrown.
// We use null.x() to throw an exception because the closure compiler may
// replace "throw" with a function call in an attempt to minimize the binary
// size, which in turn has the side effect of adding an unwanted stack frame.
try {
null.x();
} catch (e) {
stack = e.stack;
}
var frames = stack ? goog.testing.stacktrace.parse_(stack) :
goog.testing.stacktrace.followCallChain_();
return goog.testing.stacktrace.framesToString_(frames);
};
goog.exportSymbol('setDeobfuscateFunctionName',
goog.testing.stacktrace.setDeobfuscateFunctionName);

View File

@@ -0,0 +1,66 @@
// 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 Provides a fake storage mechanism for testing.
*/
goog.provide('goog.testing.storage.FakeMechanism');
goog.setTestOnly('goog.testing.storage.FakeMechanism');
goog.require('goog.storage.mechanism.IterableMechanism');
goog.require('goog.structs.Map');
/**
* Creates a fake iterable mechanism.
*
* @constructor
* @extends {goog.storage.mechanism.IterableMechanism}
*/
goog.testing.storage.FakeMechanism = function() {
/**
* @type {goog.structs.Map}
* @private
*/
this.storage_ = new goog.structs.Map();
};
goog.inherits(goog.testing.storage.FakeMechanism,
goog.storage.mechanism.IterableMechanism);
/** @override */
goog.testing.storage.FakeMechanism.prototype.set = function(key, value) {
this.storage_.set(key, value);
};
/** @override */
goog.testing.storage.FakeMechanism.prototype.get = function(key) {
return /** @type {?string} */ (
this.storage_.get(key, null /* default value */));
};
/** @override */
goog.testing.storage.FakeMechanism.prototype.remove = function(key) {
this.storage_.remove(key);
};
/** @override */
goog.testing.storage.FakeMechanism.prototype.__iterator__ = function(opt_keys) {
return this.storage_.__iterator__(opt_keys);
};

View File

@@ -0,0 +1,128 @@
// 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 strict mock implementation.
*/
goog.provide('goog.testing.StrictMock');
goog.require('goog.array');
goog.require('goog.testing.Mock');
/**
* This is a mock that verifies that methods are called in the order that they
* are specified during the recording phase. Since it verifies order, it
* follows 'fail fast' semantics. If it detects a deviation from the
* expectations, it will throw an exception and not wait for verify to be
* called.
* @param {Object} objectToMock The 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
* @extends {goog.testing.Mock}
*/
goog.testing.StrictMock = function(objectToMock, opt_mockStaticMethods,
opt_createProxy) {
goog.testing.Mock.call(this, objectToMock, opt_mockStaticMethods,
opt_createProxy);
/**
* An array of MockExpectations.
* @type {Array.<goog.testing.MockExpectation>}
* @private
*/
this.$expectations_ = [];
};
goog.inherits(goog.testing.StrictMock, goog.testing.Mock);
/** @override */
goog.testing.StrictMock.prototype.$recordExpectation = function() {
this.$expectations_.push(this.$pendingExpectation);
};
/** @override */
goog.testing.StrictMock.prototype.$recordCall = function(name, args) {
if (this.$expectations_.length == 0) {
this.$throwCallException(name, args);
}
// If the current expectation has a different name, make sure it was called
// enough and then discard it. We're through with it.
var currentExpectation = this.$expectations_[0];
while (!this.$verifyCall(currentExpectation, name, args)) {
// This might be an item which has passed its min, and we can now
// look past it, or it might be below its min and generate an error.
if (currentExpectation.actualCalls < currentExpectation.minCalls) {
this.$throwCallException(name, args, currentExpectation);
}
this.$expectations_.shift();
if (this.$expectations_.length < 1) {
// Nothing left, but this may be a failed attempt to call the previous
// item on the list, which may have been between its min and max.
this.$throwCallException(name, args, currentExpectation);
}
currentExpectation = this.$expectations_[0];
}
if (currentExpectation.maxCalls == 0) {
this.$throwCallException(name, args);
}
currentExpectation.actualCalls++;
// If we hit the max number of calls for this expectation, we're finished
// with it.
if (currentExpectation.actualCalls == currentExpectation.maxCalls) {
this.$expectations_.shift();
}
return this.$do(currentExpectation, args);
};
/** @override */
goog.testing.StrictMock.prototype.$reset = function() {
goog.testing.StrictMock.superClass_.$reset.call(this);
goog.array.clear(this.$expectations_);
};
/** @override */
goog.testing.StrictMock.prototype.$verify = function() {
goog.testing.StrictMock.superClass_.$verify.call(this);
while (this.$expectations_.length > 0) {
var expectation = this.$expectations_[0];
if (expectation.actualCalls < expectation.minCalls) {
this.$throwException('Missing a call to ' + expectation.name +
'\nExpected: ' + expectation.minCalls + ' but was: ' +
expectation.actualCalls);
} else {
// Don't need to check max, that's handled when the call is made
this.$expectations_.shift();
}
}
};

View File

@@ -0,0 +1,310 @@
// 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 A utility class for making layout assertions. This is a port
* of http://go/layoutbot.java
* See {@link http://go/layouttesting}.
*/
goog.provide('goog.testing.style.layoutasserts');
goog.require('goog.style');
goog.require('goog.testing.asserts');
goog.require('goog.testing.style');
/**
* Asserts that an element has:
* 1 - a CSS rendering the makes the element visible.
* 2 - a non-zero width and height.
* @param {Element|string} a The element or optionally the comment string.
* @param {Element=} opt_b The element when a comment string is present.
*/
var assertIsVisible = function(a, opt_b) {
_validateArguments(1, arguments);
var element = nonCommentArg(1, 1, arguments);
_assert(commentArg(1, arguments),
goog.testing.style.isVisible(element) &&
goog.testing.style.hasVisibleDimensions(element),
'Specified element should be visible.');
};
/**
* The counter assertion of assertIsVisible().
* @param {Element|string} a The element or optionally the comment string.
* @param {Element=} opt_b The element when a comment string is present.
*/
var assertNotVisible = function(a, opt_b) {
_validateArguments(1, arguments);
var element = nonCommentArg(1, 1, arguments);
if (!element) {
return;
}
_assert(commentArg(1, arguments),
!goog.testing.style.isVisible(element) ||
!goog.testing.style.hasVisibleDimensions(element),
'Specified element should not be visible.');
};
/**
* Asserts that the two specified elements intersect.
* @param {Element|string} a The first element or optionally the comment string.
* @param {Element} b The second element or the first element if comment string
* is present.
* @param {Element=} opt_c The second element if comment string is present.
*/
var assertIntersect = function(a, b, opt_c) {
_validateArguments(2, arguments);
var element = nonCommentArg(1, 2, arguments);
var otherElement = nonCommentArg(2, 2, arguments);
_assert(commentArg(1, arguments),
goog.testing.style.intersects(element, otherElement),
'Elements should intersect.');
};
/**
* Asserts that the two specified elements do not intersect.
* @param {Element|string} a The first element or optionally the comment string.
* @param {Element} b The second element or the first element if comment string
* is present.
* @param {Element=} opt_c The second element if comment string is present.
*/
var assertNoIntersect = function(a, b, opt_c) {
_validateArguments(2, arguments);
var element = nonCommentArg(1, 2, arguments);
var otherElement = nonCommentArg(2, 2, arguments);
_assert(commentArg(1, arguments),
!goog.testing.style.intersects(element, otherElement),
'Elements should not intersect.');
};
/**
* Asserts that the element must have the specified width.
* @param {Element|string} a The first element or optionally the comment string.
* @param {Element} b The second element or the first element if comment string
* is present.
* @param {Element=} opt_c The second element if comment string is present.
*/
var assertWidth = function(a, b, opt_c) {
_validateArguments(2, arguments);
var element = nonCommentArg(1, 2, arguments);
var width = nonCommentArg(2, 2, arguments);
var size = goog.style.getSize(element);
var elementWidth = size.width;
_assert(commentArg(1, arguments),
goog.testing.style.layoutasserts.isWithinThreshold_(
width, elementWidth, 0 /* tolerance */),
'Element should have width ' + width + ' but was ' + elementWidth + '.');
};
/**
* Asserts that the element must have the specified width within the specified
* tolerance.
* @param {Element|string} a The element or optionally the comment string.
* @param {number|Element} b The height or the element if comment string is
* present.
* @param {number} c The tolerance or the height if comment string is
* present.
* @param {number=} opt_d The tolerance if comment string is present.
*/
var assertWidthWithinTolerance = function(a, b, c, opt_d) {
_validateArguments(3, arguments);
var element = nonCommentArg(1, 3, arguments);
var width = nonCommentArg(2, 3, arguments);
var tolerance = nonCommentArg(3, 3, arguments);
var size = goog.style.getSize(element);
var elementWidth = size.width;
_assert(commentArg(1, arguments),
goog.testing.style.layoutasserts.isWithinThreshold_(
width, elementWidth, tolerance),
'Element width(' + elementWidth + ') should be within given width(' +
width + ') with tolerance value of ' + tolerance + '.');
};
/**
* Asserts that the element must have the specified height.
* @param {Element|string} a The first element or optionally the comment string.
* @param {Element} b The second element or the first element if comment string
* is present.
* @param {Element=} opt_c The second element if comment string is present.
*/
var assertHeight = function(a, b, opt_c) {
_validateArguments(2, arguments);
var element = nonCommentArg(1, 2, arguments);
var height = nonCommentArg(2, 2, arguments);
var size = goog.style.getSize(element);
var elementHeight = size.height;
_assert(commentArg(1, arguments),
goog.testing.style.layoutasserts.isWithinThreshold_(
height, elementHeight, 0 /* tolerance */),
'Element should have height ' + height + '.');
};
/**
* Asserts that the element must have the specified height within the specified
* tolerance.
* @param {Element|string} a The element or optionally the comment string.
* @param {number|Element} b The height or the element if comment string is
* present.
* @param {number} c The tolerance or the height if comment string is
* present.
* @param {number=} opt_d The tolerance if comment string is present.
*/
var assertHeightWithinTolerance = function(a, b, c, opt_d) {
_validateArguments(3, arguments);
var element = nonCommentArg(1, 3, arguments);
var height = nonCommentArg(2, 3, arguments);
var tolerance = nonCommentArg(3, 3, arguments);
var size = goog.style.getSize(element);
var elementHeight = size.height;
_assert(commentArg(1, arguments),
goog.testing.style.layoutasserts.isWithinThreshold_(
height, elementHeight, tolerance),
'Element width(' + elementHeight + ') should be within given width(' +
height + ') with tolerance value of ' + tolerance + '.');
};
/**
* Asserts that the first element is to the left of the second element.
* @param {Element|string} a The first element or optionally the comment string.
* @param {Element} b The second element or the first element if comment string
* is present.
* @param {Element=} opt_c The second element if comment string is present.
*/
var assertIsLeftOf = function(a, b, opt_c) {
_validateArguments(2, arguments);
var element = nonCommentArg(1, 2, arguments);
var otherElement = nonCommentArg(2, 2, arguments);
var elementRect = goog.style.getBounds(element);
var otherElementRect = goog.style.getBounds(otherElement);
_assert(commentArg(1, arguments),
elementRect.left < otherElementRect.left,
'Elements should be left to right.');
};
/**
* Asserts that the first element is strictly left of the second element.
* @param {Element|string} a The first element or optionally the comment string.
* @param {Element} b The second element or the first element if comment string
* is present.
* @param {Element=} opt_c The second element if comment string is present.
*/
var assertIsStrictlyLeftOf = function(a, b, opt_c) {
_validateArguments(2, arguments);
var element = nonCommentArg(1, 2, arguments);
var otherElement = nonCommentArg(2, 2, arguments);
var elementRect = goog.style.getBounds(element);
var otherElementRect = goog.style.getBounds(otherElement);
_assert(commentArg(1, arguments),
elementRect.left + elementRect.width < otherElementRect.left,
'Elements should be strictly left to right.');
};
/**
* Asserts that the first element is higher than the second element.
* @param {Element|string} a The first element or optionally the comment string.
* @param {Element} b The second element or the first element if comment string
* is present.
* @param {Element=} opt_c The second element if comment string is present.
*/
var assertIsAbove = function(a, b, opt_c) {
_validateArguments(2, arguments);
var element = nonCommentArg(1, 2, arguments);
var otherElement = nonCommentArg(2, 2, arguments);
var elementRect = goog.style.getBounds(element);
var otherElementRect = goog.style.getBounds(otherElement);
_assert(commentArg(1, arguments),
elementRect.top < otherElementRect.top,
'Elements should be top to bottom.');
};
/**
* Asserts that the first element is strictly higher than the second element.
* @param {Element|string} a The first element or optionally the comment string.
* @param {Element} b The second element or the first element if comment string
* is present.
* @param {Element=} opt_c The second element if comment string is present.
*/
var assertIsStrictlyAbove = function(a, b, opt_c) {
_validateArguments(2, arguments);
var element = nonCommentArg(1, 2, arguments);
var otherElement = nonCommentArg(2, 2, arguments);
var elementRect = goog.style.getBounds(element);
var otherElementRect = goog.style.getBounds(otherElement);
_assert(commentArg(1, arguments),
elementRect.top + elementRect.height < otherElementRect.top,
'Elements should be strictly top to bottom.');
};
/**
* Asserts that the first element's bounds contain the bounds of the second
* element.
* @param {Element|string} a The first element or optionally the comment string.
* @param {Element} b The second element or the first element if comment string
* is present.
* @param {Element=} opt_c The second element if comment string is present.
*/
var assertContained = function(a, b, opt_c) {
_validateArguments(2, arguments);
var element = nonCommentArg(1, 2, arguments);
var otherElement = nonCommentArg(2, 2, arguments);
var elementRect = goog.style.getBounds(element);
var otherElementRect = goog.style.getBounds(otherElement);
_assert(commentArg(1, arguments),
elementRect.contains(otherElementRect),
'Element should be contained within the other element.');
};
/**
* Returns true if the difference between val1 and val2 is less than or equal to
* the threashold.
* @param {number} val1 The first value.
* @param {number} val2 The second value.
* @param {number} threshold The threshold value.
* @return {boolean} Whether or not the the values are within the threshold.
* @private
*/
goog.testing.style.layoutasserts.isWithinThreshold_ = function(
val1, val2, threshold) {
return Math.abs(val1 - val2) <= threshold;
};

View File

@@ -0,0 +1,101 @@
// 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 Utilities for inspecting page layout. This is a port of
* http://go/layoutbot.java
* See {@link http://go/layouttesting}.
*/
goog.provide('goog.testing.style');
goog.require('goog.dom');
goog.require('goog.math.Rect');
goog.require('goog.style');
/**
* Determines whether the bounding rectangles of the given elements intersect.
* @param {Element} element The first element.
* @param {Element} otherElement The second element.
* @return {boolean} Whether the bounding rectangles of the given elements
* intersect.
*/
goog.testing.style.intersects = function(element, otherElement) {
var elementRect = goog.style.getBounds(element);
var otherElementRect = goog.style.getBounds(otherElement);
return goog.math.Rect.intersects(elementRect, otherElementRect);
};
/**
* Determines whether the element has visible dimensions, i.e. x > 0 && y > 0.
* @param {Element} element The element to check.
* @return {boolean} Whether the element has visible dimensions.
*/
goog.testing.style.hasVisibleDimensions = function(element) {
var elSize = goog.style.getSize(element);
var shortest = elSize.getShortest();
if (shortest <= 0) {
return false;
}
return true;
};
/**
* Determines whether the CSS style of the element renders it visible.
* @param {!Element} element The element to check.
* @return {boolean} Whether the CSS style of the element renders it visible.
*/
goog.testing.style.isVisible = function(element) {
var visibilityStyle =
goog.testing.style.getAvailableStyle_(element, 'visibility');
var displayStyle =
goog.testing.style.getAvailableStyle_(element, 'display');
return (visibilityStyle != 'hidden' && displayStyle != 'none');
};
/**
* Test whether the given element is on screen.
* @param {!Element} el The element to test.
* @return {boolean} Whether the element is on the screen.
*/
goog.testing.style.isOnScreen = function(el) {
var doc = goog.dom.getDomHelper(el).getDocument();
var viewport = goog.style.getVisibleRectForElement(doc.body);
var viewportRect = goog.math.Rect.createFromBox(viewport);
return goog.dom.contains(doc, el) &&
goog.style.getBounds(el).intersects(viewportRect);
};
/**
* This is essentially goog.style.getStyle_. goog.style.getStyle_ is private
* and is not a recommended way for general purpose style extractor. For the
* purposes of layout testing, we only use this function for retrieving
* 'visiblity' and 'display' style.
* @param {!Element} element The element to retrieve the style from.
* @param {string} style Style property name.
* @return {string} Style value.
* @private
*/
goog.testing.style.getAvailableStyle_ = function(element, style) {
return goog.style.getComputedStyle(element, style) ||
goog.style.getCascadedStyle(element, style) ||
goog.style.getStyle(element, style);
};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,66 @@
// 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 Generic queue for writing unit tests.
*/
goog.provide('goog.testing.TestQueue');
/**
* Generic queue for writing unit tests
* @constructor
*/
goog.testing.TestQueue = function() {
/**
* Events that have accumulated
* @type {Array.<Object>}
* @private
*/
this.events_ = [];
};
/**
* Adds a new event onto the queue.
* @param {Object} event The event to queue.
*/
goog.testing.TestQueue.prototype.enqueue = function(event) {
this.events_.push(event);
};
/**
* Returns whether the queue is empty.
* @return {boolean} Whether the queue is empty.
*/
goog.testing.TestQueue.prototype.isEmpty = function() {
return this.events_.length == 0;
};
/**
* Gets the next event from the queue. Throws an exception if the queue is
* empty.
* @param {string=} opt_comment Comment if the queue is empty.
* @return {Object} The next event from the queue.
*/
goog.testing.TestQueue.prototype.dequeue = function(opt_comment) {
if (this.isEmpty()) {
throw Error('Handler is empty: ' + opt_comment);
}
return this.events_.shift();
};

View File

@@ -0,0 +1,399 @@
// 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 The test runner is a singleton object that is used to execute
* a goog.testing.TestCases, display the results, and expose the results to
* Selenium for automation. If a TestCase hasn't been registered with the
* runner by the time window.onload occurs, the testRunner will try to auto-
* discover JsUnit style test pages.
*
* The hooks for selenium are :-
* - Boolean G_testRunner.isFinished()
* - Boolean G_testRunner.isSuccess()
* - String G_testRunner.getReport()
* - number G_testRunner.getRunTime()
*
* Testing code should not have dependencies outside of goog.testing so as to
* reduce the chance of masking missing dependencies.
*
*/
goog.provide('goog.testing.TestRunner');
goog.require('goog.testing.TestCase');
/**
* Construct a test runner.
*
* NOTE(user): This is currently pretty weird, I'm essentially trying to
* create a wrapper that the Selenium test can hook into to query the state of
* the running test case, while making goog.testing.TestCase general.
*
* @constructor
*/
goog.testing.TestRunner = function() {
/**
* Errors that occurred in the window.
* @type {Array.<string>}
*/
this.errors = [];
};
/**
* Reference to the active test case.
* @type {goog.testing.TestCase?}
*/
goog.testing.TestRunner.prototype.testCase = null;
/**
* Whether the test runner has been initialized yet.
* @type {boolean}
*/
goog.testing.TestRunner.prototype.initialized = false;
/**
* Element created in the document to add test results to.
* @type {Element}
* @private
*/
goog.testing.TestRunner.prototype.logEl_ = null;
/**
* Function to use when filtering errors.
* @type {(function(string))?}
* @private
*/
goog.testing.TestRunner.prototype.errorFilter_ = null;
/**
* Whether an empty test case counts as an error.
* @type {boolean}
* @private
*/
goog.testing.TestRunner.prototype.strict_ = true;
/**
* Initializes the test runner.
* @param {goog.testing.TestCase} testCase The test case to initialize with.
*/
goog.testing.TestRunner.prototype.initialize = function(testCase) {
if (this.testCase && this.testCase.running) {
throw Error('The test runner is already waiting for a test to complete');
}
this.testCase = testCase;
testCase.setTestRunner(this);
this.initialized = true;
};
/**
* By default, the test runner is strict, and fails if it runs an empty
* test case.
* @param {boolean} strict Whether the test runner should fail on an empty
* test case.
*/
goog.testing.TestRunner.prototype.setStrict = function(strict) {
this.strict_ = strict;
};
/**
* @return {boolean} Whether the test runner should fail on an empty
* test case.
*/
goog.testing.TestRunner.prototype.isStrict = function() {
return this.strict_;
};
/**
* Returns true if the test runner is initialized.
* Used by Selenium Hooks.
* @return {boolean} Whether the test runner is active.
*/
goog.testing.TestRunner.prototype.isInitialized = function() {
return this.initialized;
};
/**
* Returns true if the test runner is finished.
* Used by Selenium Hooks.
* @return {boolean} Whether the test runner is active.
*/
goog.testing.TestRunner.prototype.isFinished = function() {
return this.errors.length > 0 ||
this.initialized && !!this.testCase && this.testCase.started &&
!this.testCase.running;
};
/**
* Returns true if the test case didn't fail.
* Used by Selenium Hooks.
* @return {boolean} Whether the current test returned successfully.
*/
goog.testing.TestRunner.prototype.isSuccess = function() {
return !this.hasErrors() && !!this.testCase && this.testCase.isSuccess();
};
/**
* Returns true if the test case runner has errors that were caught outside of
* the test case.
* @return {boolean} Whether there were JS errors.
*/
goog.testing.TestRunner.prototype.hasErrors = function() {
return this.errors.length > 0;
};
/**
* Logs an error that occurred. Used in the case of environment setting up
* an onerror handler.
* @param {string} msg Error message.
*/
goog.testing.TestRunner.prototype.logError = function(msg) {
if (!this.errorFilter_ || this.errorFilter_.call(null, msg)) {
this.errors.push(msg);
}
};
/**
* Log failure in current running test.
* @param {Error} ex Exception.
*/
goog.testing.TestRunner.prototype.logTestFailure = function(ex) {
var testName = /** @type {string} */ (goog.testing.TestCase.currentTestName);
if (this.testCase) {
this.testCase.logError(testName, ex);
} else {
// NOTE: Do not forget to log the original exception raised.
throw new Error('Test runner not initialized with a test case. Original ' +
'exception: ' + ex.message);
}
};
/**
* Sets a function to use as a filter for errors.
* @param {function(string)} fn Filter function.
*/
goog.testing.TestRunner.prototype.setErrorFilter = function(fn) {
this.errorFilter_ = fn;
};
/**
* Returns a report of the test case that ran.
* Used by Selenium Hooks.
* @param {boolean=} opt_verbose If true results will include data about all
* tests, not just what failed.
* @return {string} A report summary of the test.
*/
goog.testing.TestRunner.prototype.getReport = function(opt_verbose) {
var report = [];
if (this.testCase) {
report.push(this.testCase.getReport(opt_verbose));
}
if (this.errors.length > 0) {
report.push('JavaScript errors detected by test runner:');
report.push.apply(report, this.errors);
report.push('\n');
}
return report.join('\n');
};
/**
* Returns the amount of time it took for the test to run.
* Used by Selenium Hooks.
* @return {number} The run time, in milliseconds.
*/
goog.testing.TestRunner.prototype.getRunTime = function() {
return this.testCase ? this.testCase.getRunTime() : 0;
};
/**
* Returns the number of script files that were loaded in order to run the test.
* @return {number} The number of script files.
*/
goog.testing.TestRunner.prototype.getNumFilesLoaded = function() {
return this.testCase ? this.testCase.getNumFilesLoaded() : 0;
};
/**
* Executes a test case and prints the results to the window.
*/
goog.testing.TestRunner.prototype.execute = function() {
if (!this.testCase) {
throw Error('The test runner must be initialized with a test case before ' +
'execute can be called.');
}
this.testCase.setCompletedCallback(goog.bind(this.onComplete_, this));
this.testCase.runTests();
};
/**
* Writes the results to the document when the test case completes.
* @private
*/
goog.testing.TestRunner.prototype.onComplete_ = function() {
var log = this.testCase.getReport(true);
if (this.errors.length > 0) {
log += '\n' + this.errors.join('\n');
}
if (!this.logEl_) {
var el = document.getElementById('closureTestRunnerLog');
if (el == null) {
el = document.createElement('div');
document.body.appendChild(el);
}
this.logEl_ = el;
}
// Highlight the page to indicate the overall outcome.
this.writeLog(log);
// TODO(user): Make this work with multiple test cases (b/8603638).
var runAgainLink = document.createElement('a');
runAgainLink.style.display = 'block';
runAgainLink.style.fontSize = 'small';
runAgainLink.href = '';
runAgainLink.onclick = goog.bind(function() {
this.execute();
return false;
}, this);
runAgainLink.innerHTML = 'Run again without reloading';
this.logEl_.appendChild(runAgainLink);
};
/**
* Writes a nicely formatted log out to the document.
* @param {string} log The string to write.
*/
goog.testing.TestRunner.prototype.writeLog = function(log) {
var lines = log.split('\n');
for (var i = 0; i < lines.length; i++) {
var line = lines[i];
var color;
var isFailOrError = /FAILED/.test(line) || /ERROR/.test(line);
if (/PASSED/.test(line)) {
color = 'darkgreen';
} else if (isFailOrError) {
color = 'darkred';
} else {
color = '#333';
}
var div = document.createElement('div');
if (line.substr(0, 2) == '> ') {
// The stack trace may contain links so it has to be interpreted as HTML.
div.innerHTML = line;
} else {
div.appendChild(document.createTextNode(line));
}
if (isFailOrError) {
var testNameMatch = /(\S+) (\[[^\]]*] )?: (FAILED|ERROR)/.exec(line);
if (testNameMatch) {
// Build a URL to run the test individually. If this test was already
// part of another subset test, we need to overwrite the old runTests
// query parameter. We also need to do this without bringing in any
// extra dependencies, otherwise we could mask missing dependency bugs.
var newSearch = 'runTests=' + testNameMatch[1];
var search = window.location.search;
if (search) {
var oldTests = /runTests=([^&]*)/.exec(search);
if (oldTests) {
newSearch = search.substr(0, oldTests.index) +
newSearch +
search.substr(oldTests.index + oldTests[0].length);
} else {
newSearch = search + '&' + newSearch;
}
} else {
newSearch = '?' + newSearch;
}
var href = window.location.href;
var hash = window.location.hash;
if (hash && hash.charAt(0) != '#') {
hash = '#' + hash;
}
href = href.split('#')[0].split('?')[0] + newSearch + hash;
// Add the link.
var a = document.createElement('A');
a.innerHTML = '(run individually)';
a.style.fontSize = '0.8em';
a.href = href;
div.appendChild(document.createTextNode(' '));
div.appendChild(a);
}
}
div.style.color = color;
div.style.font = 'normal 100% monospace';
if (i == 0) {
// Highlight the first line as a header that indicates the test outcome.
div.style.padding = '20px';
div.style.marginBottom = '10px';
if (isFailOrError) {
div.style.border = '5px solid ' + color;
div.style.backgroundColor = '#ffeeee';
} else {
div.style.border = '1px solid black';
div.style.backgroundColor = '#eeffee';
}
}
try {
div.style.whiteSpace = 'pre-wrap';
} catch (e) {
// NOTE(brenneman): IE raises an exception when assigning to pre-wrap.
// Thankfully, it doesn't collapse whitespace when using monospace fonts,
// so it will display correctly if we ignore the exception.
}
if (i < 2) {
div.style.fontWeight = 'bold';
}
this.logEl_.appendChild(div);
}
};
/**
* Logs a message to the current test case.
* @param {string} s The text to output to the log.
*/
goog.testing.TestRunner.prototype.log = function(s) {
if (this.testCase) {
this.testCase.log(s);
}
};

View File

@@ -0,0 +1,56 @@
// 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 Additional asserts for testing ControlRenderers.
*
* @author mkretzschmar@google.com (Martin Kretzschmar)
*/
goog.provide('goog.testing.ui.rendererasserts');
goog.require('goog.testing.asserts');
/**
* Assert that a control renderer constructor doesn't call getCssClass.
*
* @param {?function(new:goog.ui.ControlRenderer)} rendererClassUnderTest The
* renderer constructor to test.
*/
goog.testing.ui.rendererasserts.assertNoGetCssClassCallsInConstructor =
function(rendererClassUnderTest) {
var getCssClassCalls = 0;
/**
* @constructor
* @extends {goog.ui.ControlRenderer}
*/
function TestControlRenderer() {
rendererClassUnderTest.call(this);
}
goog.inherits(TestControlRenderer, rendererClassUnderTest);
/** @override */
TestControlRenderer.prototype.getCssClass = function() {
getCssClassCalls++;
return TestControlRenderer.superClass_.getCssClass.call(this);
};
var testControlRenderer = new TestControlRenderer();
assertEquals('Constructors should not call getCssClass, ' +
'getCustomRenderer must be able to override it post construction.',
0, getCssClassCalls);
};

View File

@@ -0,0 +1,176 @@
// 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.
// All Rights Reserved
/**
* @fileoverview A driver for testing renderers.
*
* @author nicksantos@google.com (Nick Santos)
*/
goog.provide('goog.testing.ui.RendererHarness');
goog.require('goog.Disposable');
goog.require('goog.dom.NodeType');
goog.require('goog.testing.asserts');
goog.require('goog.testing.dom');
/**
* A driver for testing renderers.
*
* @param {goog.ui.ControlRenderer} renderer A renderer to test.
* @param {Element} renderParent The parent of the element where controls will
* be rendered.
* @param {Element} decorateParent The parent of the element where controls will
* be decorated.
* @constructor
* @extends {goog.Disposable}
*/
goog.testing.ui.RendererHarness = function(renderer, renderParent,
decorateParent) {
goog.Disposable.call(this);
/**
* The renderer under test.
* @type {goog.ui.ControlRenderer}
* @private
*/
this.renderer_ = renderer;
/**
* The parent of the element where controls will be rendered.
* @type {Element}
* @private
*/
this.renderParent_ = renderParent;
/**
* The original HTML of the render element.
* @type {string}
* @private
*/
this.renderHtml_ = renderParent.innerHTML;
/**
* Teh parent of the element where controls will be decorated.
* @type {Element}
* @private
*/
this.decorateParent_ = decorateParent;
/**
* The original HTML of the decorated element.
* @type {string}
* @private
*/
this.decorateHtml_ = decorateParent.innerHTML;
};
goog.inherits(goog.testing.ui.RendererHarness, goog.Disposable);
/**
* A control to create by decoration.
* @type {goog.ui.Control}
* @private
*/
goog.testing.ui.RendererHarness.prototype.decorateControl_;
/**
* A control to create by rendering.
* @type {goog.ui.Control}
* @private
*/
goog.testing.ui.RendererHarness.prototype.renderControl_;
/**
* Whether all the necessary assert methods have been called.
* @type {boolean}
* @private
*/
goog.testing.ui.RendererHarness.prototype.verified_ = false;
/**
* Attach a control and render its DOM.
* @param {goog.ui.Control} control A control.
* @return {Element} The element created.
*/
goog.testing.ui.RendererHarness.prototype.attachControlAndRender =
function(control) {
this.renderControl_ = control;
control.setRenderer(this.renderer_);
control.render(this.renderParent_);
return control.getElement();
};
/**
* Attach a control and decorate the element given in the constructor.
* @param {goog.ui.Control} control A control.
* @return {Element} The element created.
*/
goog.testing.ui.RendererHarness.prototype.attachControlAndDecorate =
function(control) {
this.decorateControl_ = control;
control.setRenderer(this.renderer_);
var child = this.decorateParent_.firstChild;
assertEquals('The decorated node must be an element',
goog.dom.NodeType.ELEMENT, child.nodeType);
control.decorate(/** @type {Element} */ (child));
return control.getElement();
};
/**
* Assert that the rendered element and the decorated element match.
*/
goog.testing.ui.RendererHarness.prototype.assertDomMatches = function() {
assert('Both elements were not generated',
!!(this.renderControl_ && this.decorateControl_));
goog.testing.dom.assertHtmlMatches(
this.renderControl_.getElement().innerHTML,
this.decorateControl_.getElement().innerHTML);
this.verified_ = true;
};
/**
* Destroy the harness, verifying that all assertions had been checked.
* @override
* @protected
*/
goog.testing.ui.RendererHarness.prototype.disposeInternal = function() {
// If the harness was not verified appropriately, throw an exception.
assert('Expected assertDomMatches to be called',
this.verified_ || !this.renderControl_ || !this.decorateControl_);
if (this.decorateControl_) {
this.decorateControl_.dispose();
}
if (this.renderControl_) {
this.renderControl_.dispose();
}
this.renderParent_.innerHTML = this.renderHtml_;
this.decorateParent_.innerHTML = this.decorateHtml_;
goog.testing.ui.RendererHarness.superClass_.disposeInternal.call(this);
};

View File

@@ -0,0 +1,137 @@
// 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 Tools for testing Closure renderers against static markup
* spec pages.
*
*/
goog.provide('goog.testing.ui.style');
goog.require('goog.array');
goog.require('goog.dom');
goog.require('goog.dom.classes');
goog.require('goog.testing.asserts');
/**
* Uses document.write to add an iFrame to the page with the reference path in
* the src attribute. Used for loading an html file containing reference
* structures to test against into the page. Should be called within the body of
* the jsunit test page.
* @param {string} referencePath A path to a reference HTML file.
*/
goog.testing.ui.style.writeReferenceFrame = function(referencePath) {
document.write('<iframe id="reference" name="reference" ' +
'src="' + referencePath + '"></iframe>');
};
/**
* Returns a reference to the first element child of a node with the given id
* from the page loaded into the reference iFrame. Used to retrieve a particular
* reference DOM structure to test against.
* @param {string} referenceId The id of a container element for a reference
* structure in the reference page.
* @return {Node} The root element of the reference structure.
*/
goog.testing.ui.style.getReferenceNode = function(referenceId) {
return goog.dom.getFirstElementChild(
window.frames['reference'].document.getElementById(referenceId));
};
/**
* Returns an array of all element children of a given node.
* @param {Node} element The node to get element children of.
* @return {Array.<Node>} An array of all the element children.
*/
goog.testing.ui.style.getElementChildren = function(element) {
var first = goog.dom.getFirstElementChild(element);
if (!first) {
return [];
}
var children = [first], next;
while (next = goog.dom.getNextElementSibling(children[children.length - 1])) {
children.push(next);
}
return children;
};
/**
* Tests whether a given node is a "content" node of a reference structure,
* which means it is allowed to have arbitrary children.
* @param {Node} element The node to test.
* @return {boolean} Whether the given node is a content node or not.
*/
goog.testing.ui.style.isContentNode = function(element) {
return element.className.indexOf('content') != -1;
};
/**
* Tests that the structure, node names, and classes of the given element are
* the same as the reference structure with the given id. Throws an error if the
* element doesn't have the same nodes at each level of the DOM with the same
* classes on each. The test ignores all DOM structure within content nodes.
* @param {Node} element The root node of the DOM structure to test.
* @param {string} referenceId The id of the container for the reference
* structure to test against.
*/
goog.testing.ui.style.assertStructureMatchesReference = function(element,
referenceId) {
goog.testing.ui.style.assertStructureMatchesReferenceInner_(element,
goog.testing.ui.style.getReferenceNode(referenceId));
};
/**
* A recursive function for comparing structure, node names, and classes between
* a test and reference DOM structure. Throws an error if one of these things
* doesn't match. Used internally by
* {@link goog.testing.ui.style.assertStructureMatchesReference}.
* @param {Node} element DOM element to test.
* @param {Node} reference DOM element to use as a reference (test against).
* @private
*/
goog.testing.ui.style.assertStructureMatchesReferenceInner_ = function(element,
reference) {
if (!element && !reference) {
return;
}
assertTrue('Expected two elements.', !!element && !!reference);
assertEquals('Expected nodes to have the same nodeName.',
element.nodeName, reference.nodeName);
var elementClasses = goog.dom.classes.get(element);
goog.array.forEach(goog.dom.classes.get(reference), function(referenceClass) {
assertContains('Expected test node to have all reference classes.',
referenceClass, elementClasses);
});
// Call assertStructureMatchesReferenceInner_ on all element children
// unless this is a content node
var elChildren = goog.testing.ui.style.getElementChildren(element),
refChildren = goog.testing.ui.style.getElementChildren(reference);
if (!goog.testing.ui.style.isContentNode(reference)) {
if (elChildren.length != refChildren.length) {
assertEquals('Expected same number of children for a non-content node.',
elChildren.length, refChildren.length);
}
for (var i = 0; i < elChildren.length; i++) {
goog.testing.ui.style.assertStructureMatchesReferenceInner_(elChildren[i],
refChildren[i]);
}
}
};