blob: 87dc859b493f5b90f8aa2dc122539c35e35497e0 [file] [log] [blame]
goog.provide('webdriver.test.AppTester');
goog.require('goog.testing.PropertyReplacer');
goog.require('webdriver.promise.Application');
goog.require('webdriver.test.testutil');
/**
* @param {!goog.testing.MockClock} clock The clock to use.
* @constructor
*/
webdriver.test.AppTester = function(clock) {
/**
* @type {!goog.testing.MockClock}
* @private
*/
this.clock_ = clock;
/**
* @type {!goog.testing.PropertyReplacer}
* @private
*/
this.stubs_ = new goog.testing.PropertyReplacer();
/**
* @type {!webdriver.promise.Application}
* @private
*/
this.app_ = new webdriver.promise.Application();
this.watcher_ = webdriver.test.testutil.callbackPair();
this.$attachAppListener(this.watcher_);
var app = this.app_;
this.stubs_.set(webdriver.promise.Application, 'getInstance', function() {
return app;
});
};
webdriver.test.AppTester.prototype.$attachAppListener = function(callbackPair) {
this.app_.addListener(webdriver.promise.Application.EventType.IDLE,
callbackPair.callback);
this.app_.addListener(
webdriver.promise.Application.EventType.UNCAUGHT_EXCEPTION,
callbackPair.errback);
};
webdriver.test.AppTester.prototype.$removeAppListener = function(callbackPair) {
this.app_.removeListener(webdriver.promise.Application.EventType.IDLE,
callbackPair.callback);
this.app_.removeListener(
webdriver.promise.Application.EventType.UNCAUGHT_EXCEPTION,
callbackPair.errback);
};
/**
* Advances the clock so the {@link webdriver.promise.Application}'s event
* loop will run once.
* @param {Function=} opt_fn The function to call, if any, after turning the
* event loop once.
* @param {...*} var_args Any arguments that should be passed to the function
* after the event loop.
*/
webdriver.test.AppTester.prototype.$turnEventLoop = function(opt_fn, var_args) {
this.clock_.tick(webdriver.promise.Application.EVENT_LOOP_FREQUENCY);
if (opt_fn) {
opt_fn.apply(null, goog.array.slice(arguments, 1));
}
};
/**
* Runs the application, turning its event loop until it is expected to have
* shutdown (as indicated by having no more frames, or no frames with pending
* tasks). The application will be expected to either pass or fail based on
* whether a function is provided for opt_callback or opt_errback (only one
* may be specfied). If neither callback is provided, the application will be
* expected to pass.
* @param {Function=} opt_callback The function to call after the application
* passes (if it is expected to pass).
* @param {Function=} opt_errback The function to call after the application
* fails (if it is expected to fail).
* @param {boolean=} opt_ignoreResult Whether the final outcome of the
* application should be ignored.
*/
webdriver.test.AppTester.prototype.$runApplication = function(
opt_callback, opt_errback, opt_ignoreResult) {
if ((goog.isFunction(opt_callback) && goog.isDefAndNotNull(opt_errback)) ||
(goog.isDefAndNotNull(opt_callback) && goog.isFunction(opt_errback))) {
fail('You may only expect the application to pass or fail, not both!');
}
var app = this.app_;
var isDone = false;
var self = this;
var callbacks = webdriver.test.testutil.callbackPair(
function() {
isDone = true;
opt_callback && opt_callback();
},
function(e) {
isDone = true;
opt_errback && opt_errback(e);
});
this.$attachAppListener(callbacks);
var shouldBeDone = false;
try {
while (!isDone) {
this.$turnEventLoop(
shouldBeDone ? assertIsDone : determineIfShouldBeDone);
// If the event loop generated an unhandled promise, it won't be reported
// until one more turn of the JS event loop, so we need to tick the
// clock once more. This is necessary for our tests to simulate a real
// JS environment.
this.clock_.tick();
}
} finally {
this.$removeAppListener(callbacks);
}
if (!opt_ignoreResult) {
opt_errback ?
callbacks.assertErrback('App was expected to fail') :
callbacks.assertCallback('App was not expected to fail');
}
function assertIsDone() {
// Shutdown is done in one extra turn of the event loop.
self.clock_.tick();
if (!isDone && !app.activeFrame_) {
// Not done yet, but there are no frames left. This can happen if the
// very first scheduled task was scheduled inside of a promise callback.
// Turn the event loop one more time; the app should detect that it is now
// finished and start the shutdown procedure. Don't recurse here since
// we could go into an infinite loop if the app is broken.
self.$turnEventLoop();
self.clock_.tick();
}
assertTrue('Should be done now: ' + app.getSchedule(), isDone);
}
function determineIfShouldBeDone() {
shouldBeDone = !app.activeFrame_;
}
};
webdriver.test.AppTester.prototype.$assertAppNotRunning = function() {
this.watcher_.assertEither('App is still running!');
};
webdriver.test.AppTester.prototype.$assertAppIsStillRunning = function() {
this.watcher_.assertNeither('App should not be done yet');
};
webdriver.test.AppTester.prototype.$tearDown = function() {
webdriver.test.testutil.consumeTimeouts();
this.app_.reset();
this.stubs_.reset();
this.clock_.dispose();
};
webdriver.test.AppTester.proxyAppCall = function(prototypeFn) {
return function() {
return prototypeFn.apply(this.app_, arguments);
};
};
webdriver.test.AppTester.prototype.schedule =
webdriver.test.AppTester.proxyAppCall(
webdriver.promise.Application.prototype.schedule);
webdriver.test.AppTester.prototype.scheduleWait =
webdriver.test.AppTester.proxyAppCall(
webdriver.promise.Application.prototype.scheduleWait);
webdriver.test.AppTester.prototype.getHistory =
webdriver.test.AppTester.proxyAppCall(
webdriver.promise.Application.prototype.getHistory);
webdriver.test.AppTester.prototype.clearHistory =
webdriver.test.AppTester.proxyAppCall(
webdriver.promise.Application.prototype.clearHistory);