summaryrefslogtreecommitdiffstats
path: root/services/sync/tps/extensions/mozmill/resource/modules
diff options
context:
space:
mode:
Diffstat (limited to 'services/sync/tps/extensions/mozmill/resource/modules')
-rw-r--r--services/sync/tps/extensions/mozmill/resource/modules/assertions.js670
-rw-r--r--services/sync/tps/extensions/mozmill/resource/modules/driver.js290
-rw-r--r--services/sync/tps/extensions/mozmill/resource/modules/errors.js102
-rw-r--r--services/sync/tps/extensions/mozmill/resource/modules/frame.js788
-rw-r--r--services/sync/tps/extensions/mozmill/resource/modules/l10n.js71
-rw-r--r--services/sync/tps/extensions/mozmill/resource/modules/stack.js43
-rw-r--r--services/sync/tps/extensions/mozmill/resource/modules/windows.js292
7 files changed, 2256 insertions, 0 deletions
diff --git a/services/sync/tps/extensions/mozmill/resource/modules/assertions.js b/services/sync/tps/extensions/mozmill/resource/modules/assertions.js
new file mode 100644
index 000000000..c9991acf0
--- /dev/null
+++ b/services/sync/tps/extensions/mozmill/resource/modules/assertions.js
@@ -0,0 +1,670 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, you can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var EXPORTED_SYMBOLS = ['Assert', 'Expect'];
+
+var Cu = Components.utils;
+
+Cu.import("resource://gre/modules/Services.jsm");
+
+var broker = {}; Cu.import('resource://mozmill/driver/msgbroker.js', broker);
+var errors = {}; Cu.import('resource://mozmill/modules/errors.js', errors);
+var stack = {}; Cu.import('resource://mozmill/modules/stack.js', stack);
+
+/**
+ * @name assertions
+ * @namespace Defines expect and assert methods to be used for assertions.
+ */
+
+/**
+ * The Assert class implements fatal assertions, and can be used in cases
+ * when a failing test has to directly abort the current test function. All
+ * remaining tasks will not be performed.
+ *
+ */
+var Assert = function () {}
+
+Assert.prototype = {
+
+ // The following deepEquals implementation is from Narwhal under this license:
+
+ // http://wiki.commonjs.org/wiki/Unit_Testing/1.0
+ //
+ // THIS IS NOT TESTED NOR LIKELY TO WORK OUTSIDE V8!
+ //
+ // Originally from narwhal.js (http://narwhaljs.org)
+ // Copyright (c) 2009 Thomas Robinson <280north.com>
+ //
+ // Permission is hereby granted, free of charge, to any person obtaining a copy
+ // of this software and associated documentation files (the 'Software'), to
+ // deal in the Software without restriction, including without limitation the
+ // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ // sell copies of the Software, and to permit persons to whom the Software is
+ // furnished to do so, subject to the following conditions:
+ //
+ // The above copyright notice and this permission notice shall be included in
+ // all copies or substantial portions of the Software.
+ //
+ // THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ // AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ // ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+ _deepEqual: function (actual, expected) {
+ // 7.1. All identical values are equivalent, as determined by ===.
+ if (actual === expected) {
+ return true;
+
+ // 7.2. If the expected value is a Date object, the actual value is
+ // equivalent if it is also a Date object that refers to the same time.
+ } else if (actual instanceof Date && expected instanceof Date) {
+ return actual.getTime() === expected.getTime();
+
+ // 7.3. Other pairs that do not both pass typeof value == 'object',
+ // equivalence is determined by ==.
+ } else if (typeof actual != 'object' && typeof expected != 'object') {
+ return actual == expected;
+
+ // 7.4. For all other Object pairs, including Array objects, equivalence is
+ // determined by having the same number of owned properties (as verified
+ // with Object.prototype.hasOwnProperty.call), the same set of keys
+ // (although not necessarily the same order), equivalent values for every
+ // corresponding key, and an identical 'prototype' property. Note: this
+ // accounts for both named and indexed properties on Arrays.
+ } else {
+ return this._objEquiv(actual, expected);
+ }
+ },
+
+ _objEquiv: function (a, b) {
+ if (a == null || a == undefined || b == null || b == undefined)
+ return false;
+ // an identical 'prototype' property.
+ if (a.prototype !== b.prototype) return false;
+
+ function isArguments(object) {
+ return Object.prototype.toString.call(object) == '[object Arguments]';
+ }
+
+ //~~~I've managed to break Object.keys through screwy arguments passing.
+ // Converting to array solves the problem.
+ if (isArguments(a)) {
+ if (!isArguments(b)) {
+ return false;
+ }
+ a = pSlice.call(a);
+ b = pSlice.call(b);
+ return _deepEqual(a, b);
+ }
+ try {
+ var ka = Object.keys(a),
+ kb = Object.keys(b),
+ key, i;
+ } catch (e) {//happens when one is a string literal and the other isn't
+ return false;
+ }
+ // having the same number of owned properties (keys incorporates
+ // hasOwnProperty)
+ if (ka.length != kb.length)
+ return false;
+ //the same set of keys (although not necessarily the same order),
+ ka.sort();
+ kb.sort();
+ //~~~cheap key test
+ for (i = ka.length - 1; i >= 0; i--) {
+ if (ka[i] != kb[i])
+ return false;
+ }
+ //equivalent values for every corresponding key, and
+ //~~~possibly expensive deep test
+ for (i = ka.length - 1; i >= 0; i--) {
+ key = ka[i];
+ if (!this._deepEqual(a[key], b[key])) return false;
+ }
+ return true;
+ },
+
+ _expectedException : function Assert__expectedException(actual, expected) {
+ if (!actual || !expected) {
+ return false;
+ }
+
+ if (expected instanceof RegExp) {
+ return expected.test(actual);
+ } else if (actual instanceof expected) {
+ return true;
+ } else if (expected.call({}, actual) === true) {
+ return true;
+ } else if (actual.name === expected.name) {
+ return true;
+ }
+
+ return false;
+ },
+
+ /**
+ * Log a test as failing by throwing an AssertionException.
+ *
+ * @param {object} aResult
+ * Test result details used for reporting.
+ * <dl>
+ * <dd>fileName</dd>
+ * <dt>Name of the file in which the assertion failed.</dt>
+ * <dd>functionName</dd>
+ * <dt>Function in which the assertion failed.</dt>
+ * <dd>lineNumber</dd>
+ * <dt>Line number of the file in which the assertion failed.</dt>
+ * <dd>message</dd>
+ * <dt>Message why the assertion failed.</dt>
+ * </dl>
+ * @throws {errors.AssertionError}
+ *
+ */
+ _logFail: function Assert__logFail(aResult) {
+ throw new errors.AssertionError(aResult.message,
+ aResult.fileName,
+ aResult.lineNumber,
+ aResult.functionName,
+ aResult.name);
+ },
+
+ /**
+ * Log a test as passing by adding a pass frame.
+ *
+ * @param {object} aResult
+ * Test result details used for reporting.
+ * <dl>
+ * <dd>fileName</dd>
+ * <dt>Name of the file in which the assertion failed.</dt>
+ * <dd>functionName</dd>
+ * <dt>Function in which the assertion failed.</dt>
+ * <dd>lineNumber</dd>
+ * <dt>Line number of the file in which the assertion failed.</dt>
+ * <dd>message</dd>
+ * <dt>Message why the assertion failed.</dt>
+ * </dl>
+ */
+ _logPass: function Assert__logPass(aResult) {
+ broker.pass({pass: aResult});
+ },
+
+ /**
+ * Test the condition and mark test as passed or failed
+ *
+ * @param {boolean} aCondition
+ * Condition to test.
+ * @param {string} aMessage
+ * Message to show for the test result
+ * @param {string} aDiagnosis
+ * Diagnose message to show for the test result
+ * @throws {errors.AssertionError}
+ *
+ * @returns {boolean} Result of the test.
+ */
+ _test: function Assert__test(aCondition, aMessage, aDiagnosis) {
+ let diagnosis = aDiagnosis || "";
+ let message = aMessage || "";
+
+ if (diagnosis)
+ message = aMessage ? message + " - " + diagnosis : diagnosis;
+
+ // Build result data
+ let frame = stack.findCallerFrame(Components.stack);
+
+ let result = {
+ 'fileName' : frame.filename.replace(/(.*)-> /, ""),
+ 'functionName' : frame.name,
+ 'lineNumber' : frame.lineNumber,
+ 'message' : message
+ };
+
+ // Log test result
+ if (aCondition) {
+ this._logPass(result);
+ }
+ else {
+ result.stack = Components.stack;
+ this._logFail(result);
+ }
+
+ return aCondition;
+ },
+
+ /**
+ * Perform an always passing test
+ *
+ * @param {string} aMessage
+ * Message to show for the test result.
+ * @returns {boolean} Always returns true.
+ */
+ pass: function Assert_pass(aMessage) {
+ return this._test(true, aMessage, undefined);
+ },
+
+ /**
+ * Perform an always failing test
+ *
+ * @param {string} aMessage
+ * Message to show for the test result.
+ * @throws {errors.AssertionError}
+ *
+ * @returns {boolean} Always returns false.
+ */
+ fail: function Assert_fail(aMessage) {
+ return this._test(false, aMessage, undefined);
+ },
+
+ /**
+ * Test if the value pass
+ *
+ * @param {boolean|string|number|object} aValue
+ * Value to test.
+ * @param {string} aMessage
+ * Message to show for the test result.
+ * @throws {errors.AssertionError}
+ *
+ * @returns {boolean} Result of the test.
+ */
+ ok: function Assert_ok(aValue, aMessage) {
+ let condition = !!aValue;
+ let diagnosis = "got '" + aValue + "'";
+
+ return this._test(condition, aMessage, diagnosis);
+ },
+
+ /**
+ * Test if both specified values are identical.
+ *
+ * @param {boolean|string|number|object} aValue
+ * Value to test.
+ * @param {boolean|string|number|object} aExpected
+ * Value to strictly compare with.
+ * @param {string} aMessage
+ * Message to show for the test result
+ * @throws {errors.AssertionError}
+ *
+ * @returns {boolean} Result of the test.
+ */
+ equal: function Assert_equal(aValue, aExpected, aMessage) {
+ let condition = (aValue === aExpected);
+ let diagnosis = "'" + aValue + "' should equal '" + aExpected + "'";
+
+ return this._test(condition, aMessage, diagnosis);
+ },
+
+ /**
+ * Test if both specified values are not identical.
+ *
+ * @param {boolean|string|number|object} aValue
+ * Value to test.
+ * @param {boolean|string|number|object} aExpected
+ * Value to strictly compare with.
+ * @param {string} aMessage
+ * Message to show for the test result
+ * @throws {errors.AssertionError}
+ *
+ * @returns {boolean} Result of the test.
+ */
+ notEqual: function Assert_notEqual(aValue, aExpected, aMessage) {
+ let condition = (aValue !== aExpected);
+ let diagnosis = "'" + aValue + "' should not equal '" + aExpected + "'";
+
+ return this._test(condition, aMessage, diagnosis);
+ },
+
+ /**
+ * Test if an object equals another object
+ *
+ * @param {object} aValue
+ * The object to test.
+ * @param {object} aExpected
+ * The object to strictly compare with.
+ * @param {string} aMessage
+ * Message to show for the test result
+ * @throws {errors.AssertionError}
+ *
+ * @returns {boolean} Result of the test.
+ */
+ deepEqual: function equal(aValue, aExpected, aMessage) {
+ let condition = this._deepEqual(aValue, aExpected);
+ try {
+ var aValueString = JSON.stringify(aValue);
+ } catch (e) {
+ var aValueString = String(aValue);
+ }
+ try {
+ var aExpectedString = JSON.stringify(aExpected);
+ } catch (e) {
+ var aExpectedString = String(aExpected);
+ }
+
+ let diagnosis = "'" + aValueString + "' should equal '" +
+ aExpectedString + "'";
+
+ return this._test(condition, aMessage, diagnosis);
+ },
+
+ /**
+ * Test if an object does not equal another object
+ *
+ * @param {object} aValue
+ * The object to test.
+ * @param {object} aExpected
+ * The object to strictly compare with.
+ * @param {string} aMessage
+ * Message to show for the test result
+ * @throws {errors.AssertionError}
+ *
+ * @returns {boolean} Result of the test.
+ */
+ notDeepEqual: function notEqual(aValue, aExpected, aMessage) {
+ let condition = !this._deepEqual(aValue, aExpected);
+ try {
+ var aValueString = JSON.stringify(aValue);
+ } catch (e) {
+ var aValueString = String(aValue);
+ }
+ try {
+ var aExpectedString = JSON.stringify(aExpected);
+ } catch (e) {
+ var aExpectedString = String(aExpected);
+ }
+
+ let diagnosis = "'" + aValueString + "' should not equal '" +
+ aExpectedString + "'";
+
+ return this._test(condition, aMessage, diagnosis);
+ },
+
+ /**
+ * Test if the regular expression matches the string.
+ *
+ * @param {string} aString
+ * String to test.
+ * @param {RegEx} aRegex
+ * Regular expression to use for testing that a match exists.
+ * @param {string} aMessage
+ * Message to show for the test result
+ * @throws {errors.AssertionError}
+ *
+ * @returns {boolean} Result of the test.
+ */
+ match: function Assert_match(aString, aRegex, aMessage) {
+ // XXX Bug 634948
+ // Regex objects are transformed to strings when evaluated in a sandbox
+ // For now lets re-create the regex from its string representation
+ let pattern = flags = "";
+ try {
+ let matches = aRegex.toString().match(/\/(.*)\/(.*)/);
+
+ pattern = matches[1];
+ flags = matches[2];
+ } catch (e) {
+ }
+
+ let regex = new RegExp(pattern, flags);
+ let condition = (aString.match(regex) !== null);
+ let diagnosis = "'" + regex + "' matches for '" + aString + "'";
+
+ return this._test(condition, aMessage, diagnosis);
+ },
+
+ /**
+ * Test if the regular expression does not match the string.
+ *
+ * @param {string} aString
+ * String to test.
+ * @param {RegEx} aRegex
+ * Regular expression to use for testing that a match does not exist.
+ * @param {string} aMessage
+ * Message to show for the test result
+ * @throws {errors.AssertionError}
+ *
+ * @returns {boolean} Result of the test.
+ */
+ notMatch: function Assert_notMatch(aString, aRegex, aMessage) {
+ // XXX Bug 634948
+ // Regex objects are transformed to strings when evaluated in a sandbox
+ // For now lets re-create the regex from its string representation
+ let pattern = flags = "";
+ try {
+ let matches = aRegex.toString().match(/\/(.*)\/(.*)/);
+
+ pattern = matches[1];
+ flags = matches[2];
+ } catch (e) {
+ }
+
+ let regex = new RegExp(pattern, flags);
+ let condition = (aString.match(regex) === null);
+ let diagnosis = "'" + regex + "' doesn't match for '" + aString + "'";
+
+ return this._test(condition, aMessage, diagnosis);
+ },
+
+
+ /**
+ * Test if a code block throws an exception.
+ *
+ * @param {string} block
+ * function to call to test for exception
+ * @param {RegEx} error
+ * the expected error class
+ * @param {string} message
+ * message to present if assertion fails
+ * @throws {errors.AssertionError}
+ *
+ * @returns {boolean} Result of the test.
+ */
+ throws : function Assert_throws(block, /*optional*/error, /*optional*/message) {
+ return this._throws.apply(this, [true].concat(Array.prototype.slice.call(arguments)));
+ },
+
+ /**
+ * Test if a code block doesn't throw an exception.
+ *
+ * @param {string} block
+ * function to call to test for exception
+ * @param {RegEx} error
+ * the expected error class
+ * @param {string} message
+ * message to present if assertion fails
+ * @throws {errors.AssertionError}
+ *
+ * @returns {boolean} Result of the test.
+ */
+ doesNotThrow : function Assert_doesNotThrow(block, /*optional*/error, /*optional*/message) {
+ return this._throws.apply(this, [false].concat(Array.prototype.slice.call(arguments)));
+ },
+
+ /* Tests whether a code block throws the expected exception
+ class. helper for throws() and doesNotThrow()
+
+ adapted from node.js's assert._throws()
+ https://github.com/joyent/node/blob/master/lib/assert.js
+ */
+ _throws : function Assert__throws(shouldThrow, block, expected, message) {
+ var actual;
+
+ if (typeof expected === 'string') {
+ message = expected;
+ expected = null;
+ }
+
+ try {
+ block();
+ } catch (e) {
+ actual = e;
+ }
+
+ message = (expected && expected.name ? ' (' + expected.name + ').' : '.') +
+ (message ? ' ' + message : '.');
+
+ if (shouldThrow && !actual) {
+ return this._test(false, message, 'Missing expected exception');
+ }
+
+ if (!shouldThrow && this._expectedException(actual, expected)) {
+ return this._test(false, message, 'Got unwanted exception');
+ }
+
+ if ((shouldThrow && actual && expected &&
+ !this._expectedException(actual, expected)) || (!shouldThrow && actual)) {
+ throw actual;
+ }
+
+ return this._test(true, message);
+ },
+
+ /**
+ * Test if the string contains the pattern.
+ *
+ * @param {String} aString String to test.
+ * @param {String} aPattern Pattern to look for in the string
+ * @param {String} aMessage Message to show for the test result
+ * @throws {errors.AssertionError}
+ *
+ * @returns {Boolean} Result of the test.
+ */
+ contain: function Assert_contain(aString, aPattern, aMessage) {
+ let condition = (aString.indexOf(aPattern) !== -1);
+ let diagnosis = "'" + aString + "' should contain '" + aPattern + "'";
+
+ return this._test(condition, aMessage, diagnosis);
+ },
+
+ /**
+ * Test if the string does not contain the pattern.
+ *
+ * @param {String} aString String to test.
+ * @param {String} aPattern Pattern to look for in the string
+ * @param {String} aMessage Message to show for the test result
+ * @throws {errors.AssertionError}
+ *
+ * @returns {Boolean} Result of the test.
+ */
+ notContain: function Assert_notContain(aString, aPattern, aMessage) {
+ let condition = (aString.indexOf(aPattern) === -1);
+ let diagnosis = "'" + aString + "' should not contain '" + aPattern + "'";
+
+ return this._test(condition, aMessage, diagnosis);
+ },
+
+ /**
+ * Waits for the callback evaluates to true
+ *
+ * @param {Function} aCallback
+ * Callback for evaluation
+ * @param {String} aMessage
+ * Message to show for result
+ * @param {Number} aTimeout
+ * Timeout in waiting for evaluation
+ * @param {Number} aInterval
+ * Interval between evaluation attempts
+ * @param {Object} aThisObject
+ * this object
+ * @throws {errors.AssertionError}
+ *
+ * @returns {Boolean} Result of the test.
+ */
+ waitFor: function Assert_waitFor(aCallback, aMessage, aTimeout, aInterval, aThisObject) {
+ var timeout = aTimeout || 5000;
+ var interval = aInterval || 100;
+
+ var self = {
+ timeIsUp: false,
+ result: aCallback.call(aThisObject)
+ };
+ var deadline = Date.now() + timeout;
+
+ function wait() {
+ if (self.result !== true) {
+ self.result = aCallback.call(aThisObject);
+ self.timeIsUp = Date.now() > deadline;
+ }
+ }
+
+ var hwindow = Services.appShell.hiddenDOMWindow;
+ var timeoutInterval = hwindow.setInterval(wait, interval);
+ var thread = Services.tm.currentThread;
+
+ while (self.result !== true && !self.timeIsUp) {
+ thread.processNextEvent(true);
+
+ let type = typeof(self.result);
+ if (type !== 'boolean')
+ throw TypeError("waitFor() callback has to return a boolean" +
+ " instead of '" + type + "'");
+ }
+
+ hwindow.clearInterval(timeoutInterval);
+
+ if (self.result !== true && self.timeIsUp) {
+ aMessage = aMessage || arguments.callee.name + ": Timeout exceeded for '" + aCallback + "'";
+ throw new errors.TimeoutError(aMessage);
+ }
+
+ broker.pass({'function':'assert.waitFor()'});
+ return true;
+ }
+}
+
+/* non-fatal assertions */
+var Expect = function () {}
+
+Expect.prototype = new Assert();
+
+/**
+ * Log a test as failing by adding a fail frame.
+ *
+ * @param {object} aResult
+ * Test result details used for reporting.
+ * <dl>
+ * <dd>fileName</dd>
+ * <dt>Name of the file in which the assertion failed.</dt>
+ * <dd>functionName</dd>
+ * <dt>Function in which the assertion failed.</dt>
+ * <dd>lineNumber</dd>
+ * <dt>Line number of the file in which the assertion failed.</dt>
+ * <dd>message</dd>
+ * <dt>Message why the assertion failed.</dt>
+ * </dl>
+ */
+Expect.prototype._logFail = function Expect__logFail(aResult) {
+ broker.fail({fail: aResult});
+}
+
+/**
+ * Waits for the callback evaluates to true
+ *
+ * @param {Function} aCallback
+ * Callback for evaluation
+ * @param {String} aMessage
+ * Message to show for result
+ * @param {Number} aTimeout
+ * Timeout in waiting for evaluation
+ * @param {Number} aInterval
+ * Interval between evaluation attempts
+ * @param {Object} aThisObject
+ * this object
+ */
+Expect.prototype.waitFor = function Expect_waitFor(aCallback, aMessage, aTimeout, aInterval, aThisObject) {
+ let condition = true;
+ let message = aMessage;
+
+ try {
+ Assert.prototype.waitFor.apply(this, arguments);
+ }
+ catch (ex) {
+ if (!ex instanceof errors.AssertionError) {
+ throw ex;
+ }
+ message = ex.message;
+ condition = false;
+ }
+
+ return this._test(condition, message);
+}
diff --git a/services/sync/tps/extensions/mozmill/resource/modules/driver.js b/services/sync/tps/extensions/mozmill/resource/modules/driver.js
new file mode 100644
index 000000000..17fcfbde6
--- /dev/null
+++ b/services/sync/tps/extensions/mozmill/resource/modules/driver.js
@@ -0,0 +1,290 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, you can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * @namespace Defines the Mozmill driver for global actions
+ */
+var driver = exports;
+
+Cu.import("resource://gre/modules/Services.jsm");
+
+// Temporarily include utils module to re-use sleep
+var assertions = {}; Cu.import('resource://mozmill/modules/assertions.js', assertions);
+var mozmill = {}; Cu.import("resource://mozmill/driver/mozmill.js", mozmill);
+var utils = {}; Cu.import('resource://mozmill/stdlib/utils.js', utils);
+
+/**
+ * Gets the topmost browser window. If there are none at that time, optionally
+ * opens one. Otherwise will raise an exception if none are found.
+ *
+ * @memberOf driver
+ * @param {Boolean] [aOpenIfNone=true] Open a new browser window if none are found.
+ * @returns {DOMWindow}
+ */
+function getBrowserWindow(aOpenIfNone) {
+ // Set default
+ if (typeof aOpenIfNone === 'undefined') {
+ aOpenIfNone = true;
+ }
+
+ // If implicit open is off, turn on strict checking, and vice versa.
+ let win = getTopmostWindowByType("navigator:browser", !aOpenIfNone);
+
+ // Can just assume automatic open here. If we didn't want it and nothing found,
+ // we already raised above when getTopmostWindow was called.
+ if (!win)
+ win = openBrowserWindow();
+
+ return win;
+}
+
+
+/**
+ * Retrieves the hidden window on OS X
+ *
+ * @memberOf driver
+ * @returns {DOMWindow} The hidden window
+ */
+function getHiddenWindow() {
+ return Services.appShell.hiddenDOMWindow;
+}
+
+
+/**
+ * Opens a new browser window
+ *
+ * @memberOf driver
+ * @returns {DOMWindow}
+ */
+function openBrowserWindow() {
+ // On OS X we have to be able to create a new browser window even with no other
+ // window open. Therefore we have to use the hidden window. On other platforms
+ // at least one remaining browser window has to exist.
+ var win = mozmill.isMac ? getHiddenWindow() :
+ getTopmostWindowByType("navigator:browser", true);
+ return win.OpenBrowserWindow();
+}
+
+
+/**
+ * Pause the test execution for the given amount of time
+ *
+ * @type utils.sleep
+ * @memberOf driver
+ */
+var sleep = utils.sleep;
+
+/**
+ * Wait until the given condition via the callback returns true.
+ *
+ * @type utils.waitFor
+ * @memberOf driver
+ */
+var waitFor = assertions.Assert.waitFor;
+
+//
+// INTERNAL WINDOW ENUMERATIONS
+//
+
+/**
+ * Internal function to build a list of DOM windows using a given enumerator
+ * and filter.
+ *
+ * @private
+ * @memberOf driver
+ * @param {nsISimpleEnumerator} aEnumerator Window enumerator to use.
+ * @param {Function} [aFilterCallback] Function which is used to filter windows.
+ * @param {Boolean} [aStrict=true] Throw an error if no windows found
+ *
+ * @returns {DOMWindow[]} The windows found, in the same order as the enumerator.
+ */
+function _getWindows(aEnumerator, aFilterCallback, aStrict) {
+ // Set default
+ if (typeof aStrict === 'undefined')
+ aStrict = true;
+
+ let windows = [];
+
+ while (aEnumerator.hasMoreElements()) {
+ let window = aEnumerator.getNext();
+
+ if (!aFilterCallback || aFilterCallback(window)) {
+ windows.push(window);
+ }
+ }
+
+ // If this list is empty and we're strict, throw an error
+ if (windows.length === 0 && aStrict) {
+ var message = 'No windows were found';
+
+ // We'll throw a more detailed error if a filter was used.
+ if (aFilterCallback && aFilterCallback.name)
+ message += ' using filter "' + aFilterCallback.name + '"';
+
+ throw new Error(message);
+ }
+
+ return windows;
+}
+
+//
+// FILTER CALLBACKS
+//
+
+/**
+ * Generator of a closure to filter a window based by a method
+ *
+ * @memberOf driver
+ * @param {String} aName Name of the method in the window object.
+ * @returns {Boolean} True if the condition is met.
+ */
+function windowFilterByMethod(aName) {
+ return function byMethod(aWindow) { return (aName in aWindow); }
+}
+
+
+/**
+ * Generator of a closure to filter a window based by the its title
+ *
+ * @param {String} aTitle Title of the window.
+ * @returns {Boolean} True if the condition is met.
+ */
+function windowFilterByTitle(aTitle) {
+ return function byTitle(aWindow) { return (aWindow.document.title === aTitle); }
+}
+
+
+/**
+ * Generator of a closure to filter a window based by the its type
+ *
+ * @memberOf driver
+ * @param {String} aType Type of the window.
+ * @returns {Boolean} True if the condition is met.
+ */
+function windowFilterByType(aType) {
+ return function byType(aWindow) {
+ var type = aWindow.document.documentElement.getAttribute("windowtype");
+ return (type === aType);
+ }
+}
+
+//
+// WINDOW LIST RETRIEVAL FUNCTIONS
+//
+
+/**
+ * Retrieves a sorted list of open windows based on their age (newest to oldest),
+ * optionally matching filter criteria.
+ *
+ * @memberOf driver
+ * @param {Function} [aFilterCallback] Function which is used to filter windows.
+ * @param {Boolean} [aStrict=true] Throw an error if no windows found
+ *
+ * @returns {DOMWindow[]} List of windows.
+ */
+function getWindowsByAge(aFilterCallback, aStrict) {
+ var windows = _getWindows(Services.wm.getEnumerator(""),
+ aFilterCallback, aStrict);
+
+ // Reverse the list, since naturally comes back old->new
+ return windows.reverse();
+}
+
+
+/**
+ * Retrieves a sorted list of open windows based on their z order (topmost first),
+ * optionally matching filter criteria.
+ *
+ * @memberOf driver
+ * @param {Function} [aFilterCallback] Function which is used to filter windows.
+ * @param {Boolean} [aStrict=true] Throw an error if no windows found
+ *
+ * @returns {DOMWindow[]} List of windows.
+ */
+function getWindowsByZOrder(aFilterCallback, aStrict) {
+ return _getWindows(Services.wm.getZOrderDOMWindowEnumerator("", true),
+ aFilterCallback, aStrict);
+}
+
+//
+// SINGLE WINDOW RETRIEVAL FUNCTIONS
+//
+
+/**
+ * Retrieves the last opened window, optionally matching filter criteria.
+ *
+ * @memberOf driver
+ * @param {Function} [aFilterCallback] Function which is used to filter windows.
+ * @param {Boolean} [aStrict=true] If true, throws error if no window found.
+ *
+ * @returns {DOMWindow} The window, or null if none found and aStrict == false
+ */
+function getNewestWindow(aFilterCallback, aStrict) {
+ var windows = getWindowsByAge(aFilterCallback, aStrict);
+ return windows.length ? windows[0] : null;
+}
+
+/**
+ * Retrieves the topmost window, optionally matching filter criteria.
+ *
+ * @memberOf driver
+ * @param {Function} [aFilterCallback] Function which is used to filter windows.
+ * @param {Boolean} [aStrict=true] If true, throws error if no window found.
+ *
+ * @returns {DOMWindow} The window, or null if none found and aStrict == false
+ */
+function getTopmostWindow(aFilterCallback, aStrict) {
+ var windows = getWindowsByZOrder(aFilterCallback, aStrict);
+ return windows.length ? windows[0] : null;
+}
+
+
+/**
+ * Retrieves the topmost window given by the window type
+ *
+ * XXX: Bug 462222
+ * This function has to be used instead of getTopmostWindow until the
+ * underlying platform bug has been fixed.
+ *
+ * @memberOf driver
+ * @param {String} [aWindowType=null] Window type to query for
+ * @param {Boolean} [aStrict=true] Throw an error if no windows found
+ *
+ * @returns {DOMWindow} The window, or null if none found and aStrict == false
+ */
+function getTopmostWindowByType(aWindowType, aStrict) {
+ if (typeof aStrict === 'undefined')
+ aStrict = true;
+
+ var win = Services.wm.getMostRecentWindow(aWindowType);
+
+ if (win === null && aStrict) {
+ var message = 'No windows of type "' + aWindowType + '" were found';
+ throw new errors.UnexpectedError(message);
+ }
+
+ return win;
+}
+
+
+// Export of functions
+driver.getBrowserWindow = getBrowserWindow;
+driver.getHiddenWindow = getHiddenWindow;
+driver.openBrowserWindow = openBrowserWindow;
+driver.sleep = sleep;
+driver.waitFor = waitFor;
+
+driver.windowFilterByMethod = windowFilterByMethod;
+driver.windowFilterByTitle = windowFilterByTitle;
+driver.windowFilterByType = windowFilterByType;
+
+driver.getWindowsByAge = getWindowsByAge;
+driver.getNewestWindow = getNewestWindow;
+driver.getTopmostWindowByType = getTopmostWindowByType;
+
+
+// XXX Bug: 462222
+// Currently those functions cannot be used. So they shouldn't be exported.
+//driver.getWindowsByZOrder = getWindowsByZOrder;
+//driver.getTopmostWindow = getTopmostWindow;
diff --git a/services/sync/tps/extensions/mozmill/resource/modules/errors.js b/services/sync/tps/extensions/mozmill/resource/modules/errors.js
new file mode 100644
index 000000000..58d1a918a
--- /dev/null
+++ b/services/sync/tps/extensions/mozmill/resource/modules/errors.js
@@ -0,0 +1,102 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, you can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var EXPORTED_SYMBOLS = ['BaseError',
+ 'ApplicationQuitError',
+ 'AssertionError',
+ 'TimeoutError'];
+
+
+/**
+ * Creates a new instance of a base error
+ *
+ * @class Represents the base for custom errors
+ * @param {string} [aMessage=Error().message]
+ * The error message to show
+ * @param {string} [aFileName=Error().fileName]
+ * The file name where the error has been raised
+ * @param {string} [aLineNumber=Error().lineNumber]
+ * The line number of the file where the error has been raised
+ * @param {string} [aFunctionName=undefined]
+ * The function name in which the error has been raised
+ */
+function BaseError(aMessage, aFileName, aLineNumber, aFunctionName) {
+ this.name = this.constructor.name;
+
+ var err = new Error();
+ if (err.stack) {
+ this.stack = err.stack;
+ }
+
+ this.message = aMessage || err.message;
+ this.fileName = aFileName || err.fileName;
+ this.lineNumber = aLineNumber || err.lineNumber;
+ this.functionName = aFunctionName;
+}
+
+
+/**
+ * Creates a new instance of an application quit error used by Mozmill to
+ * indicate that the application is going to shutdown
+ *
+ * @class Represents an error object thrown when the application is going to shutdown
+ * @param {string} [aMessage=Error().message]
+ * The error message to show
+ * @param {string} [aFileName=Error().fileName]
+ * The file name where the error has been raised
+ * @param {string} [aLineNumber=Error().lineNumber]
+ * The line number of the file where the error has been raised
+ * @param {string} [aFunctionName=undefined]
+ * The function name in which the error has been raised
+ */
+function ApplicationQuitError(aMessage, aFileName, aLineNumber, aFunctionName) {
+ BaseError.apply(this, arguments);
+}
+
+ApplicationQuitError.prototype = Object.create(BaseError.prototype, {
+ constructor : { value : ApplicationQuitError }
+});
+
+
+/**
+ * Creates a new instance of an assertion error
+ *
+ * @class Represents an error object thrown by failing assertions
+ * @param {string} [aMessage=Error().message]
+ * The error message to show
+ * @param {string} [aFileName=Error().fileName]
+ * The file name where the error has been raised
+ * @param {string} [aLineNumber=Error().lineNumber]
+ * The line number of the file where the error has been raised
+ * @param {string} [aFunctionName=undefined]
+ * The function name in which the error has been raised
+ */
+function AssertionError(aMessage, aFileName, aLineNumber, aFunctionName) {
+ BaseError.apply(this, arguments);
+}
+
+AssertionError.prototype = Object.create(BaseError.prototype, {
+ constructor : { value : AssertionError }
+});
+
+/**
+ * Creates a new instance of a timeout error
+ *
+ * @class Represents an error object thrown by failing assertions
+ * @param {string} [aMessage=Error().message]
+ * The error message to show
+ * @param {string} [aFileName=Error().fileName]
+ * The file name where the error has been raised
+ * @param {string} [aLineNumber=Error().lineNumber]
+ * The line number of the file where the error has been raised
+ * @param {string} [aFunctionName=undefined]
+ * The function name in which the error has been raised
+ */
+function TimeoutError(aMessage, aFileName, aLineNumber, aFunctionName) {
+ AssertionError.apply(this, arguments);
+}
+
+TimeoutError.prototype = Object.create(AssertionError.prototype, {
+ constructor : { value : TimeoutError }
+});
diff --git a/services/sync/tps/extensions/mozmill/resource/modules/frame.js b/services/sync/tps/extensions/mozmill/resource/modules/frame.js
new file mode 100644
index 000000000..dae8276b6
--- /dev/null
+++ b/services/sync/tps/extensions/mozmill/resource/modules/frame.js
@@ -0,0 +1,788 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, you can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var EXPORTED_SYMBOLS = ['Collector','Runner','events', 'runTestFile', 'log',
+ 'timers', 'persisted', 'shutdownApplication'];
+
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+var Cu = Components.utils;
+
+const TIMEOUT_SHUTDOWN_HTTPD = 15000;
+
+Cu.import("resource://gre/modules/Services.jsm");
+
+Cu.import('resource://mozmill/stdlib/httpd.js');
+
+var broker = {}; Cu.import('resource://mozmill/driver/msgbroker.js', broker);
+var assertions = {}; Cu.import('resource://mozmill/modules/assertions.js', assertions);
+var errors = {}; Cu.import('resource://mozmill/modules/errors.js', errors);
+var os = {}; Cu.import('resource://mozmill/stdlib/os.js', os);
+var strings = {}; Cu.import('resource://mozmill/stdlib/strings.js', strings);
+var arrays = {}; Cu.import('resource://mozmill/stdlib/arrays.js', arrays);
+var withs = {}; Cu.import('resource://mozmill/stdlib/withs.js', withs);
+var utils = {}; Cu.import('resource://mozmill/stdlib/utils.js', utils);
+
+var securableModule = {};
+Cu.import('resource://mozmill/stdlib/securable-module.js', securableModule);
+
+var uuidgen = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
+
+var httpd = null;
+var persisted = {};
+
+var assert = new assertions.Assert();
+var expect = new assertions.Expect();
+
+var mozmill = undefined;
+var mozelement = undefined;
+var modules = undefined;
+
+var timers = [];
+
+
+/**
+ * Shutdown or restart the application
+ *
+ * @param {boolean} [aFlags=undefined]
+ * Additional flags how to handle the shutdown or restart. The attributes
+ * eRestarti386 and eRestartx86_64 have not been documented yet.
+ * @see https://developer.mozilla.org/nsIAppStartup#Attributes
+ */
+function shutdownApplication(aFlags) {
+ var flags = Ci.nsIAppStartup.eForceQuit;
+
+ if (aFlags) {
+ flags |= aFlags;
+ }
+
+ // Send a request to shutdown the application. That will allow us and other
+ // components to finish up with any shutdown code. Please note that we don't
+ // care if other components or add-ons want to prevent this via cancelQuit,
+ // we really force the shutdown.
+ let cancelQuit = Components.classes["@mozilla.org/supports-PRBool;1"].
+ createInstance(Components.interfaces.nsISupportsPRBool);
+ Services.obs.notifyObservers(cancelQuit, "quit-application-requested", null);
+
+ // Use a timer to trigger the application restart, which will allow us to
+ // send an ACK packet via jsbridge if the method has been called via Python.
+ var event = {
+ notify: function(timer) {
+ Services.startup.quit(flags);
+ }
+ }
+
+ var timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ timer.initWithCallback(event, 100, Ci.nsITimer.TYPE_ONE_SHOT);
+}
+
+function stateChangeBase(possibilties, restrictions, target, cmeta, v) {
+ if (possibilties) {
+ if (!arrays.inArray(possibilties, v)) {
+ // TODO Error value not in this.poss
+ return;
+ }
+ }
+
+ if (restrictions) {
+ for (var i in restrictions) {
+ var r = restrictions[i];
+ if (!r(v)) {
+ // TODO error value did not pass restriction
+ return;
+ }
+ }
+ }
+
+ // Fire jsbridge notification, logging notification, listener notifications
+ events[target] = v;
+ events.fireEvent(cmeta, target);
+}
+
+
+var events = {
+ appQuit : false,
+ currentModule : null,
+ currentState : null,
+ currentTest : null,
+ shutdownRequested : false,
+ userShutdown : null,
+ userShutdownTimer : null,
+
+ listeners : {},
+ globalListeners : []
+}
+
+events.setState = function (v) {
+ return stateChangeBase(['dependencies', 'setupModule', 'teardownModule',
+ 'test', 'setupTest', 'teardownTest', 'collection'],
+ null, 'currentState', 'setState', v);
+}
+
+events.toggleUserShutdown = function (obj){
+ if (!this.userShutdown) {
+ this.userShutdown = obj;
+
+ var event = {
+ notify: function(timer) {
+ events.toggleUserShutdown(obj);
+ }
+ }
+
+ this.userShutdownTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ this.userShutdownTimer.initWithCallback(event, obj.timeout, Ci.nsITimer.TYPE_ONE_SHOT);
+
+ } else {
+ this.userShutdownTimer.cancel();
+
+ // If the application is not going to shutdown, the user shutdown failed and
+ // we have to force a shutdown.
+ if (!events.appQuit) {
+ this.fail({'function':'events.toggleUserShutdown',
+ 'message':'Shutdown expected but none detected before timeout',
+ 'userShutdown': obj});
+
+ var flags = Ci.nsIAppStartup.eAttemptQuit;
+ if (events.isRestartShutdown()) {
+ flags |= Ci.nsIAppStartup.eRestart;
+ }
+
+ shutdownApplication(flags);
+ }
+ }
+}
+
+events.isUserShutdown = function () {
+ return this.userShutdown ? this.userShutdown["user"] : false;
+}
+
+events.isRestartShutdown = function () {
+ return this.userShutdown.restart;
+}
+
+events.startShutdown = function (obj) {
+ events.fireEvent('shutdown', obj);
+
+ if (obj["user"]) {
+ events.toggleUserShutdown(obj);
+ } else {
+ shutdownApplication(obj.flags);
+ }
+}
+
+events.setTest = function (test) {
+ test.__start__ = Date.now();
+ test.__passes__ = [];
+ test.__fails__ = [];
+
+ events.currentTest = test;
+
+ var obj = {'filename': events.currentModule.__file__,
+ 'name': test.__name__}
+ events.fireEvent('setTest', obj);
+}
+
+events.endTest = function (test) {
+ // use the current test unless specified
+ if (test === undefined) {
+ test = events.currentTest;
+ }
+
+ // If no test is set it has already been reported. Beside that we don't want
+ // to report it a second time.
+ if (!test || test.status === 'done')
+ return;
+
+ // report the end of a test
+ test.__end__ = Date.now();
+ test.status = 'done';
+
+ var obj = {'filename': events.currentModule.__file__,
+ 'passed': test.__passes__.length,
+ 'failed': test.__fails__.length,
+ 'passes': test.__passes__,
+ 'fails' : test.__fails__,
+ 'name' : test.__name__,
+ 'time_start': test.__start__,
+ 'time_end': test.__end__}
+
+ if (test.skipped) {
+ obj['skipped'] = true;
+ obj.skipped_reason = test.skipped_reason;
+ }
+
+ if (test.meta) {
+ obj.meta = test.meta;
+ }
+
+ // Report the test result only if the test is a true test or if it is failing
+ if (withs.startsWith(test.__name__, "test") || test.__fails__.length > 0) {
+ events.fireEvent('endTest', obj);
+ }
+}
+
+events.setModule = function (aModule) {
+ aModule.__start__ = Date.now();
+ aModule.__status__ = 'running';
+
+ var result = stateChangeBase(null,
+ [function (aModule) {return (aModule.__file__ != undefined)}],
+ 'currentModule', 'setModule', aModule);
+
+ return result;
+}
+
+events.endModule = function (aModule) {
+ // It should only reported once, so check if it already has been done
+ if (aModule.__status__ === 'done')
+ return;
+
+ aModule.__end__ = Date.now();
+ aModule.__status__ = 'done';
+
+ var obj = {
+ 'filename': aModule.__file__,
+ 'time_start': aModule.__start__,
+ 'time_end': aModule.__end__
+ }
+
+ events.fireEvent('endModule', obj);
+}
+
+events.pass = function (obj) {
+ // a low level event, such as a keystroke, succeeds
+ if (events.currentTest) {
+ events.currentTest.__passes__.push(obj);
+ }
+
+ for (var timer of timers) {
+ timer.actions.push(
+ {"currentTest": events.currentModule.__file__ + "::" + events.currentTest.__name__,
+ "obj": obj,
+ "result": "pass"}
+ );
+ }
+
+ events.fireEvent('pass', obj);
+}
+
+events.fail = function (obj) {
+ var error = obj.exception;
+
+ if (error) {
+ // Error objects aren't enumerable https://bugzilla.mozilla.org/show_bug.cgi?id=637207
+ obj.exception = {
+ name: error.name,
+ message: error.message,
+ lineNumber: error.lineNumber,
+ fileName: error.fileName,
+ stack: error.stack
+ };
+ }
+
+ // a low level event, such as a keystroke, fails
+ if (events.currentTest) {
+ events.currentTest.__fails__.push(obj);
+ }
+
+ for (var time of timers) {
+ timer.actions.push(
+ {"currentTest": events.currentModule.__file__ + "::" + events.currentTest.__name__,
+ "obj": obj,
+ "result": "fail"}
+ );
+ }
+
+ events.fireEvent('fail', obj);
+}
+
+events.skip = function (reason) {
+ // this is used to report skips associated with setupModule and nothing else
+ events.currentTest.skipped = true;
+ events.currentTest.skipped_reason = reason;
+
+ for (var timer of timers) {
+ timer.actions.push(
+ {"currentTest": events.currentModule.__file__ + "::" + events.currentTest.__name__,
+ "obj": reason,
+ "result": "skip"}
+ );
+ }
+
+ events.fireEvent('skip', reason);
+}
+
+events.fireEvent = function (name, obj) {
+ if (events.appQuit) {
+ // dump('* Event discarded: ' + name + ' ' + JSON.stringify(obj) + '\n');
+ return;
+ }
+
+ if (this.listeners[name]) {
+ for (var i in this.listeners[name]) {
+ this.listeners[name][i](obj);
+ }
+ }
+
+ for (var listener of this.globalListeners) {
+ listener(name, obj);
+ }
+}
+
+events.addListener = function (name, listener) {
+ if (this.listeners[name]) {
+ this.listeners[name].push(listener);
+ } else if (name == '') {
+ this.globalListeners.push(listener)
+ } else {
+ this.listeners[name] = [listener];
+ }
+}
+
+events.removeListener = function (listener) {
+ for (var listenerIndex in this.listeners) {
+ var e = this.listeners[listenerIndex];
+
+ for (var i in e){
+ if (e[i] == listener) {
+ this.listeners[listenerIndex] = arrays.remove(e, i);
+ }
+ }
+ }
+
+ for (var i in this.globalListeners) {
+ if (this.globalListeners[i] == listener) {
+ this.globalListeners = arrays.remove(this.globalListeners, i);
+ }
+ }
+}
+
+events.persist = function () {
+ try {
+ events.fireEvent('persist', persisted);
+ } catch (e) {
+ events.fireEvent('error', "persist serialization failed.")
+ }
+}
+
+events.firePythonCallback = function (obj) {
+ obj['test'] = events.currentModule.__file__;
+ events.fireEvent('firePythonCallback', obj);
+}
+
+events.screenshot = function (obj) {
+ // Find the name of the test function
+ for (var attr in events.currentModule) {
+ if (events.currentModule[attr] == events.currentTest) {
+ var testName = attr;
+ break;
+ }
+ }
+
+ obj['test_file'] = events.currentModule.__file__;
+ obj['test_name'] = testName;
+ events.fireEvent('screenshot', obj);
+}
+
+var log = function (obj) {
+ events.fireEvent('log', obj);
+}
+
+// Register the listeners
+broker.addObject({'endTest': events.endTest,
+ 'fail': events.fail,
+ 'firePythonCallback': events.firePythonCallback,
+ 'log': log,
+ 'pass': events.pass,
+ 'persist': events.persist,
+ 'screenshot': events.screenshot,
+ 'shutdown': events.startShutdown,
+ });
+
+try {
+ Cu.import('resource://jsbridge/modules/Events.jsm');
+
+ events.addListener('', function (name, obj) {
+ Events.fireEvent('mozmill.' + name, obj);
+ });
+} catch (e) {
+ Services.console.logStringMessage("Event module of JSBridge not available.");
+}
+
+
+/**
+ * Observer for notifications when the application is going to shutdown
+ */
+function AppQuitObserver() {
+ this.runner = null;
+
+ Services.obs.addObserver(this, "quit-application-requested", false);
+}
+
+AppQuitObserver.prototype = {
+ observe: function (aSubject, aTopic, aData) {
+ switch (aTopic) {
+ case "quit-application-requested":
+ Services.obs.removeObserver(this, "quit-application-requested");
+
+ // If we observe a quit notification make sure to send the
+ // results of the current test. In those cases we don't reach
+ // the equivalent code in runTestModule()
+ events.pass({'message': 'AppQuitObserver: ' + JSON.stringify(aData),
+ 'userShutdown': events.userShutdown});
+
+ if (this.runner) {
+ this.runner.end();
+ }
+
+ if (httpd) {
+ httpd.stop();
+ }
+
+ events.appQuit = true;
+
+ break;
+ }
+ }
+}
+
+var appQuitObserver = new AppQuitObserver();
+
+/**
+ * The collector handles HTTPd.js and initilizing the module
+ */
+function Collector() {
+ this.test_modules_by_filename = {};
+ this.testing = [];
+}
+
+Collector.prototype.addHttpResource = function (aDirectory, aPath) {
+ var fp = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
+ fp.initWithPath(os.abspath(aDirectory, this.current_file));
+
+ return httpd.addHttpResource(fp, aPath);
+}
+
+Collector.prototype.initTestModule = function (filename, testname) {
+ var test_module = this.loadFile(filename, this);
+ var has_restarted = !(testname == null);
+ test_module.__tests__ = [];
+
+ for (var i in test_module) {
+ if (typeof(test_module[i]) == "function") {
+ test_module[i].__name__ = i;
+
+ // Only run setupModule if we are a single test OR if we are the first
+ // test of a restart chain (don't run it prior to members in a restart
+ // chain)
+ if (i == "setupModule" && !has_restarted) {
+ test_module.__setupModule__ = test_module[i];
+ } else if (i == "setupTest") {
+ test_module.__setupTest__ = test_module[i];
+ } else if (i == "teardownTest") {
+ test_module.__teardownTest__ = test_module[i];
+ } else if (i == "teardownModule") {
+ test_module.__teardownModule__ = test_module[i];
+ } else if (withs.startsWith(i, "test")) {
+ if (testname && (i != testname)) {
+ continue;
+ }
+
+ testname = null;
+ test_module.__tests__.push(test_module[i]);
+ }
+ }
+ }
+
+ test_module.collector = this;
+ test_module.status = 'loaded';
+
+ this.test_modules_by_filename[filename] = test_module;
+
+ return test_module;
+}
+
+Collector.prototype.loadFile = function (path, collector) {
+ var moduleLoader = new securableModule.Loader({
+ rootPaths: ["resource://mozmill/modules/"],
+ defaultPrincipal: "system",
+ globals : { Cc: Cc,
+ Ci: Ci,
+ Cu: Cu,
+ Cr: Components.results}
+ });
+
+ // load a test module from a file and add some candy
+ var file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
+ file.initWithPath(path);
+ var uri = Services.io.newFileURI(file).spec;
+
+ this.loadTestResources();
+
+ var systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal();
+ var module = new Components.utils.Sandbox(systemPrincipal);
+ module.assert = assert;
+ module.Cc = Cc;
+ module.Ci = Ci;
+ module.Cr = Components.results;
+ module.Cu = Cu;
+ module.collector = collector;
+ module.driver = moduleLoader.require("driver");
+ module.elementslib = mozelement;
+ module.errors = errors;
+ module.expect = expect;
+ module.findElement = mozelement;
+ module.log = log;
+ module.mozmill = mozmill;
+ module.persisted = persisted;
+
+ module.require = function (mod) {
+ var loader = new securableModule.Loader({
+ rootPaths: [Services.io.newFileURI(file.parent).spec,
+ "resource://mozmill/modules/"],
+ defaultPrincipal: "system",
+ globals : { assert: assert,
+ expect: expect,
+ mozmill: mozmill,
+ elementslib: mozelement, // This a quick hack to maintain backwards compatibility with 1.5.x
+ findElement: mozelement,
+ persisted: persisted,
+ Cc: Cc,
+ Ci: Ci,
+ Cu: Cu,
+ log: log }
+ });
+
+ if (modules != undefined) {
+ loader.modules = modules;
+ }
+
+ var retval = loader.require(mod);
+ modules = loader.modules;
+
+ return retval;
+ }
+
+ if (collector != undefined) {
+ collector.current_file = file;
+ collector.current_path = path;
+ }
+
+ try {
+ Services.scriptloader.loadSubScript(uri, module, "UTF-8");
+ } catch (e) {
+ var obj = {
+ 'filename': path,
+ 'passed': 0,
+ 'failed': 1,
+ 'passes': [],
+ 'fails' : [{'exception' : {
+ message: e.message,
+ filename: e.filename,
+ lineNumber: e.lineNumber}}],
+ 'name' :'<TOP_LEVEL>'
+ };
+
+ events.fail({'exception': e});
+ events.fireEvent('endTest', obj);
+ }
+
+ module.__file__ = path;
+ module.__uri__ = uri;
+
+ return module;
+}
+
+Collector.prototype.loadTestResources = function () {
+ // load resources we want in our tests
+ if (mozmill === undefined) {
+ mozmill = {};
+ Cu.import("resource://mozmill/driver/mozmill.js", mozmill);
+ }
+ if (mozelement === undefined) {
+ mozelement = {};
+ Cu.import("resource://mozmill/driver/mozelement.js", mozelement);
+ }
+}
+
+
+/**
+ *
+ */
+function Httpd(aPort) {
+ this.http_port = aPort;
+
+ while (true) {
+ try {
+ var srv = new HttpServer();
+ srv.registerContentType("sjs", "sjs");
+ srv.identity.setPrimary("http", "localhost", this.http_port);
+ srv.start(this.http_port);
+
+ this._httpd = srv;
+ break;
+ }
+ catch (e) {
+ // Failure most likely due to port conflict
+ this.http_port++;
+ }
+ }
+}
+
+Httpd.prototype.addHttpResource = function (aDir, aPath) {
+ var path = aPath ? ("/" + aPath + "/") : "/";
+
+ try {
+ this._httpd.registerDirectory(path, aDir);
+ return 'http://localhost:' + this.http_port + path;
+ }
+ catch (e) {
+ throw Error("Failure to register directory: " + aDir.path);
+ }
+};
+
+Httpd.prototype.stop = function () {
+ if (!this._httpd) {
+ return;
+ }
+
+ var shutdown = false;
+ this._httpd.stop(function () { shutdown = true; });
+
+ assert.waitFor(function () {
+ return shutdown;
+ }, "Local HTTP server has been stopped", TIMEOUT_SHUTDOWN_HTTPD);
+
+ this._httpd = null;
+};
+
+function startHTTPd() {
+ if (!httpd) {
+ // Ensure that we start the HTTP server only once during a session
+ httpd = new Httpd(43336);
+ }
+}
+
+
+function Runner() {
+ this.collector = new Collector();
+ this.ended = false;
+
+ var m = {}; Cu.import('resource://mozmill/driver/mozmill.js', m);
+ this.platform = m.platform;
+
+ events.fireEvent('startRunner', true);
+}
+
+Runner.prototype.end = function () {
+ if (!this.ended) {
+ this.ended = true;
+
+ appQuitObserver.runner = null;
+
+ events.endTest();
+ events.endModule(events.currentModule);
+ events.fireEvent('endRunner', true);
+ events.persist();
+ }
+};
+
+Runner.prototype.runTestFile = function (filename, name) {
+ var module = this.collector.initTestModule(filename, name);
+ this.runTestModule(module);
+};
+
+Runner.prototype.runTestModule = function (module) {
+ appQuitObserver.runner = this;
+ events.setModule(module);
+
+ // If setupModule passes, run all the tests. Otherwise mark them as skipped.
+ if (this.execFunction(module.__setupModule__, module)) {
+ for (var test of module.__tests__) {
+ if (events.shutdownRequested) {
+ break;
+ }
+
+ // If setupTest passes, run the test. Otherwise mark it as skipped.
+ if (this.execFunction(module.__setupTest__, module)) {
+ this.execFunction(test);
+ } else {
+ this.skipFunction(test, module.__setupTest__.__name__ + " failed");
+ }
+
+ this.execFunction(module.__teardownTest__, module);
+ }
+
+ } else {
+ for (var test of module.__tests__) {
+ this.skipFunction(test, module.__setupModule__.__name__ + " failed");
+ }
+ }
+
+ this.execFunction(module.__teardownModule__, module);
+ events.endModule(module);
+};
+
+Runner.prototype.execFunction = function (func, arg) {
+ if (typeof func !== "function" || events.shutdownRequested) {
+ return true;
+ }
+
+ var isTest = withs.startsWith(func.__name__, "test");
+
+ events.setState(isTest ? "test" : func.__name);
+ events.setTest(func);
+
+ // skip excluded platforms
+ if (func.EXCLUDED_PLATFORMS != undefined) {
+ if (arrays.inArray(func.EXCLUDED_PLATFORMS, this.platform)) {
+ events.skip("Platform exclusion");
+ events.endTest(func);
+ return false;
+ }
+ }
+
+ // skip function if requested
+ if (func.__force_skip__ != undefined) {
+ events.skip(func.__force_skip__);
+ events.endTest(func);
+ return false;
+ }
+
+ // execute the test function
+ try {
+ func(arg);
+ } catch (e) {
+ if (e instanceof errors.ApplicationQuitError) {
+ events.shutdownRequested = true;
+ } else {
+ events.fail({'exception': e, 'test': func})
+ }
+ }
+
+ // If a user shutdown has been requested and the function already returned,
+ // we can assume that a shutdown will not happen anymore. We should force a
+ // shutdown then, to prevent the next test from being executed.
+ if (events.isUserShutdown()) {
+ events.shutdownRequested = true;
+ events.toggleUserShutdown(events.userShutdown);
+ }
+
+ events.endTest(func);
+ return events.currentTest.__fails__.length == 0;
+};
+
+function runTestFile(filename, name) {
+ var runner = new Runner();
+ runner.runTestFile(filename, name);
+ runner.end();
+
+ return true;
+}
+
+Runner.prototype.skipFunction = function (func, message) {
+ events.setTest(func);
+ events.skip(message);
+ events.endTest(func);
+};
diff --git a/services/sync/tps/extensions/mozmill/resource/modules/l10n.js b/services/sync/tps/extensions/mozmill/resource/modules/l10n.js
new file mode 100644
index 000000000..63a355421
--- /dev/null
+++ b/services/sync/tps/extensions/mozmill/resource/modules/l10n.js
@@ -0,0 +1,71 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, you can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * @namespace Defines useful methods to work with localized content
+ */
+var l10n = exports;
+
+Cu.import("resource://gre/modules/Services.jsm");
+
+/**
+ * Retrieve the localized content for a given DTD entity
+ *
+ * @memberOf l10n
+ * @param {String[]} aDTDs Array of URLs for DTD files.
+ * @param {String} aEntityId ID of the entity to get the localized content of.
+ *
+ * @returns {String} Localized content
+ */
+function getEntity(aDTDs, aEntityId) {
+ // Add xhtml11.dtd to prevent missing entity errors with XHTML files
+ aDTDs.push("resource:///res/dtd/xhtml11.dtd");
+
+ // Build a string of external entities
+ var references = "";
+ for (i = 0; i < aDTDs.length; i++) {
+ var id = 'dtd' + i;
+ references += '<!ENTITY % ' + id + ' SYSTEM "' + aDTDs[i] + '">%' + id + ';';
+ }
+
+ var header = '<?xml version="1.0"?><!DOCTYPE elem [' + references + ']>';
+ var element = '<elem id="entity">&' + aEntityId + ';</elem>';
+ var content = header + element;
+
+ var parser = Cc["@mozilla.org/xmlextras/domparser;1"].
+ createInstance(Ci.nsIDOMParser);
+ var doc = parser.parseFromString(content, 'text/xml');
+ var node = doc.querySelector('elem[id="entity"]');
+
+ if (!node) {
+ throw new Error("Unkown entity '" + aEntityId + "'");
+ }
+
+ return node.textContent;
+}
+
+
+/**
+ * Retrieve the localized content for a given property
+ *
+ * @memberOf l10n
+ * @param {String} aURL URL of the .properties file.
+ * @param {String} aProperty The property to get the value of.
+ *
+ * @returns {String} Value of the requested property
+ */
+function getProperty(aURL, aProperty) {
+ var bundle = Services.strings.createBundle(aURL);
+
+ try {
+ return bundle.GetStringFromName(aProperty);
+ } catch (ex) {
+ throw new Error("Unkown property '" + aProperty + "'");
+ }
+}
+
+
+// Export of functions
+l10n.getEntity = getEntity;
+l10n.getProperty = getProperty;
diff --git a/services/sync/tps/extensions/mozmill/resource/modules/stack.js b/services/sync/tps/extensions/mozmill/resource/modules/stack.js
new file mode 100644
index 000000000..889316bf1
--- /dev/null
+++ b/services/sync/tps/extensions/mozmill/resource/modules/stack.js
@@ -0,0 +1,43 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, you can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var EXPORTED_SYMBOLS = ['findCallerFrame'];
+
+
+/**
+ * @namespace Defines utility methods for handling stack frames
+ */
+
+/**
+ * Find the frame to use for logging the test result. If a start frame has
+ * been specified, we walk down the stack until a frame with the same filename
+ * as the start frame has been found. The next file in the stack will be the
+ * frame to use for logging the result.
+ *
+ * @memberOf stack
+ * @param {Object} [aStartFrame=Components.stack] Frame to start from walking up the stack.
+ * @returns {Object} Frame of the stack to use for logging the result.
+ */
+function findCallerFrame(aStartFrame) {
+ let frame = Components.stack;
+ let filename = frame.filename.replace(/(.*)-> /, "");
+
+ // If a start frame has been specified, walk up the stack until we have
+ // found the corresponding file
+ if (aStartFrame) {
+ filename = aStartFrame.filename.replace(/(.*)-> /, "");
+
+ while (frame.caller &&
+ frame.filename && (frame.filename.indexOf(filename) == -1)) {
+ frame = frame.caller;
+ }
+ }
+
+ // Walk even up more until the next file has been found
+ while (frame.caller &&
+ (!frame.filename || (frame.filename.indexOf(filename) != -1)))
+ frame = frame.caller;
+
+ return frame;
+}
diff --git a/services/sync/tps/extensions/mozmill/resource/modules/windows.js b/services/sync/tps/extensions/mozmill/resource/modules/windows.js
new file mode 100644
index 000000000..1c75a2d3d
--- /dev/null
+++ b/services/sync/tps/extensions/mozmill/resource/modules/windows.js
@@ -0,0 +1,292 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, you can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var EXPORTED_SYMBOLS = ["init", "map"];
+
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+var Cu = Components.utils;
+
+// imports
+var utils = {}; Cu.import('resource://mozmill/stdlib/utils.js', utils);
+
+var uuidgen = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
+
+/**
+ * The window map is used to store information about the current state of
+ * open windows, e.g. loaded state
+ */
+var map = {
+ _windows : { },
+
+ /**
+ * Check if a given window id is contained in the map of windows
+ *
+ * @param {Number} aWindowId
+ * Outer ID of the window to check.
+ * @returns {Boolean} True if the window is part of the map, otherwise false.
+ */
+ contains : function (aWindowId) {
+ return (aWindowId in this._windows);
+ },
+
+ /**
+ * Retrieve the value of the specified window's property.
+ *
+ * @param {Number} aWindowId
+ * Outer ID of the window to check.
+ * @param {String} aProperty
+ * Property to retrieve the value from
+ * @return {Object} Value of the window's property
+ */
+ getValue : function (aWindowId, aProperty) {
+ if (!this.contains(aWindowId)) {
+ return undefined;
+ } else {
+ var win = this._windows[aWindowId];
+
+ return (aProperty in win) ? win[aProperty]
+ : undefined;
+ }
+ },
+
+ /**
+ * Remove the entry for a given window
+ *
+ * @param {Number} aWindowId
+ * Outer ID of the window to check.
+ */
+ remove : function (aWindowId) {
+ if (this.contains(aWindowId)) {
+ delete this._windows[aWindowId];
+ }
+
+ // dump("* current map: " + JSON.stringify(this._windows) + "\n");
+ },
+
+ /**
+ * Update the property value of a given window
+ *
+ * @param {Number} aWindowId
+ * Outer ID of the window to check.
+ * @param {String} aProperty
+ * Property to update the value for
+ * @param {Object}
+ * Value to set
+ */
+ update : function (aWindowId, aProperty, aValue) {
+ if (!this.contains(aWindowId)) {
+ this._windows[aWindowId] = { };
+ }
+
+ this._windows[aWindowId][aProperty] = aValue;
+ // dump("* current map: " + JSON.stringify(this._windows) + "\n");
+ },
+
+ /**
+ * Update the internal loaded state of the given content window. To identify
+ * an active (re)load action we make use of an uuid.
+ *
+ * @param {Window} aId - The outer id of the window to update
+ * @param {Boolean} aIsLoaded - Has the window been loaded
+ */
+ updatePageLoadStatus : function (aId, aIsLoaded) {
+ this.update(aId, "loaded", aIsLoaded);
+
+ var uuid = this.getValue(aId, "id_load_in_transition");
+
+ // If no uuid has been set yet or when the page gets unloaded create a new id
+ if (!uuid || !aIsLoaded) {
+ uuid = uuidgen.generateUUID();
+ this.update(aId, "id_load_in_transition", uuid);
+ }
+
+ // dump("*** Page status updated: id=" + aId + ", loaded=" + aIsLoaded + ", uuid=" + uuid + "\n");
+ },
+
+ /**
+ * This method only applies to content windows, where we have to check if it has
+ * been successfully loaded or reloaded. An uuid allows us to wait for the next
+ * load action triggered by e.g. controller.open().
+ *
+ * @param {Window} aId - The outer id of the content window to check
+ *
+ * @returns {Boolean} True if the content window has been loaded
+ */
+ hasPageLoaded : function (aId) {
+ var load_current = this.getValue(aId, "id_load_in_transition");
+ var load_handled = this.getValue(aId, "id_load_handled");
+
+ var isLoaded = this.contains(aId) && this.getValue(aId, "loaded") &&
+ (load_current !== load_handled);
+
+ if (isLoaded) {
+ // Backup the current uuid so we can check later if another page load happened.
+ this.update(aId, "id_load_handled", load_current);
+ }
+
+ // dump("** Page has been finished loading: id=" + aId + ", status=" + isLoaded + ", uuid=" + load_current + "\n");
+
+ return isLoaded;
+ }
+};
+
+
+// Observer when a new top-level window is ready
+var windowReadyObserver = {
+ observe: function (aSubject, aTopic, aData) {
+ // Not in all cases we get a ChromeWindow. So ensure we really operate
+ // on such an instance. Otherwise load events will not be handled.
+ var win = utils.getChromeWindow(aSubject);
+
+ // var id = utils.getWindowId(win);
+ // dump("*** 'toplevel-window-ready' observer notification: id=" + id + "\n");
+ attachEventListeners(win);
+ }
+};
+
+
+// Observer when a top-level window is closed
+var windowCloseObserver = {
+ observe: function (aSubject, aTopic, aData) {
+ var id = utils.getWindowId(aSubject);
+ // dump("*** 'outer-window-destroyed' observer notification: id=" + id + "\n");
+
+ map.remove(id);
+ }
+};
+
+// Bug 915554
+// Support for the old Private Browsing Mode (eg. ESR17)
+// TODO: remove once ESR17 is no longer supported
+var enterLeavePrivateBrowsingObserver = {
+ observe: function (aSubject, aTopic, aData) {
+ handleAttachEventListeners();
+ }
+};
+
+/**
+ * Attach event listeners
+ *
+ * @param {ChromeWindow} aWindow
+ * Window to attach listeners on.
+ */
+function attachEventListeners(aWindow) {
+ // These are the event handlers
+ var pageShowHandler = function (aEvent) {
+ var doc = aEvent.originalTarget;
+
+ // Only update the flag if we have a document as target
+ // see https://bugzilla.mozilla.org/show_bug.cgi?id=690829
+ if ("defaultView" in doc) {
+ var id = utils.getWindowId(doc.defaultView);
+ // dump("*** 'pageshow' event: id=" + id + ", baseURI=" + doc.baseURI + "\n");
+ map.updatePageLoadStatus(id, true);
+ }
+
+ // We need to add/remove the unload/pagehide event listeners to preserve caching.
+ aWindow.addEventListener("beforeunload", beforeUnloadHandler, true);
+ aWindow.addEventListener("pagehide", pageHideHandler, true);
+ };
+
+ var DOMContentLoadedHandler = function (aEvent) {
+ var doc = aEvent.originalTarget;
+
+ // Only update the flag if we have a document as target
+ if ("defaultView" in doc) {
+ var id = utils.getWindowId(doc.defaultView);
+ // dump("*** 'DOMContentLoaded' event: id=" + id + ", baseURI=" + doc.baseURI + "\n");
+
+ // We only care about error pages for DOMContentLoaded
+ var errorRegex = /about:.+(error)|(blocked)\?/;
+ if (errorRegex.exec(doc.baseURI)) {
+ // Wait about 1s to be sure the DOM is ready
+ utils.sleep(1000);
+
+ map.updatePageLoadStatus(id, true);
+ }
+
+ // We need to add/remove the unload event listener to preserve caching.
+ aWindow.addEventListener("beforeunload", beforeUnloadHandler, true);
+ }
+ };
+
+ // beforeunload is still needed because pagehide doesn't fire before the page is unloaded.
+ // still use pagehide for cases when beforeunload doesn't get fired
+ var beforeUnloadHandler = function (aEvent) {
+ var doc = aEvent.originalTarget;
+
+ // Only update the flag if we have a document as target
+ if ("defaultView" in doc) {
+ var id = utils.getWindowId(doc.defaultView);
+ // dump("*** 'beforeunload' event: id=" + id + ", baseURI=" + doc.baseURI + "\n");
+ map.updatePageLoadStatus(id, false);
+ }
+
+ aWindow.removeEventListener("beforeunload", beforeUnloadHandler, true);
+ };
+
+ var pageHideHandler = function (aEvent) {
+ var doc = aEvent.originalTarget;
+
+ // Only update the flag if we have a document as target
+ if ("defaultView" in doc) {
+ var id = utils.getWindowId(doc.defaultView);
+ // dump("*** 'pagehide' event: id=" + id + ", baseURI=" + doc.baseURI + "\n");
+ map.updatePageLoadStatus(id, false);
+ }
+ // If event.persisted is true the beforeUnloadHandler would never fire
+ // and we have to remove the event handler here to avoid memory leaks.
+ if (aEvent.persisted)
+ aWindow.removeEventListener("beforeunload", beforeUnloadHandler, true);
+ };
+
+ var onWindowLoaded = function (aEvent) {
+ var id = utils.getWindowId(aWindow);
+ // dump("*** 'load' event: id=" + id + ", baseURI=" + aWindow.document.baseURI + "\n");
+
+ map.update(id, "loaded", true);
+
+ // Note: Error pages will never fire a "pageshow" event. For those we
+ // have to wait for the "DOMContentLoaded" event. That's the final state.
+ // Error pages will always have a baseURI starting with
+ // "about:" followed by "error" or "blocked".
+ aWindow.addEventListener("DOMContentLoaded", DOMContentLoadedHandler, true);
+
+ // Page is ready
+ aWindow.addEventListener("pageshow", pageShowHandler, true);
+
+ // Leave page (use caching)
+ aWindow.addEventListener("pagehide", pageHideHandler, true);
+ };
+
+ // If the window has already been finished loading, call the load handler
+ // directly. Otherwise attach it to the current window.
+ if (aWindow.document.readyState === 'complete') {
+ onWindowLoaded();
+ } else {
+ aWindow.addEventListener("load", onWindowLoaded, false);
+ }
+}
+
+// Attach event listeners to all already open top-level windows
+function handleAttachEventListeners() {
+ var enumerator = Cc["@mozilla.org/appshell/window-mediator;1"].
+ getService(Ci.nsIWindowMediator).getEnumerator("");
+ while (enumerator.hasMoreElements()) {
+ var win = enumerator.getNext();
+ attachEventListeners(win);
+ }
+}
+
+function init() {
+ // Activate observer for new top level windows
+ var observerService = Cc["@mozilla.org/observer-service;1"].
+ getService(Ci.nsIObserverService);
+ observerService.addObserver(windowReadyObserver, "toplevel-window-ready", false);
+ observerService.addObserver(windowCloseObserver, "outer-window-destroyed", false);
+ observerService.addObserver(enterLeavePrivateBrowsingObserver, "private-browsing", false);
+
+ handleAttachEventListeners();
+}