diff options
Diffstat (limited to 'services/sync/tps/extensions/mozmill/resource/modules')
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(); +} |