From ac46df8daea09899ce30dc8fd70986e258c746bf Mon Sep 17 00:00:00 2001 From: "Matt A. Tobin" Date: Fri, 9 Feb 2018 06:46:43 -0500 Subject: Move Add-on SDK source to toolkit/jetpack --- toolkit/jetpack/sdk/deprecated/api-utils.js | 197 +++++++ toolkit/jetpack/sdk/deprecated/events/assembler.js | 54 ++ toolkit/jetpack/sdk/deprecated/sync-worker.js | 288 ++++++++++ toolkit/jetpack/sdk/deprecated/unit-test-finder.js | 199 +++++++ toolkit/jetpack/sdk/deprecated/unit-test.js | 584 +++++++++++++++++++++ toolkit/jetpack/sdk/deprecated/window-utils.js | 193 +++++++ 6 files changed, 1515 insertions(+) create mode 100644 toolkit/jetpack/sdk/deprecated/api-utils.js create mode 100644 toolkit/jetpack/sdk/deprecated/events/assembler.js create mode 100644 toolkit/jetpack/sdk/deprecated/sync-worker.js create mode 100644 toolkit/jetpack/sdk/deprecated/unit-test-finder.js create mode 100644 toolkit/jetpack/sdk/deprecated/unit-test.js create mode 100644 toolkit/jetpack/sdk/deprecated/window-utils.js (limited to 'toolkit/jetpack/sdk/deprecated') diff --git a/toolkit/jetpack/sdk/deprecated/api-utils.js b/toolkit/jetpack/sdk/deprecated/api-utils.js new file mode 100644 index 000000000..856fc50cb --- /dev/null +++ b/toolkit/jetpack/sdk/deprecated/api-utils.js @@ -0,0 +1,197 @@ +/* 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/. */ +"use strict"; + +module.metadata = { + "stability": "deprecated" +}; + +const { merge } = require("../util/object"); +const { union } = require("../util/array"); +const { isNil, isRegExp } = require("../lang/type"); + +// The possible return values of getTypeOf. +const VALID_TYPES = [ + "array", + "boolean", + "function", + "null", + "number", + "object", + "string", + "undefined", + "regexp" +]; + +const { isArray } = Array; + +/** + * Returns a validated options dictionary given some requirements. If any of + * the requirements are not met, an exception is thrown. + * + * @param options + * An object, the options dictionary to validate. It's not modified. + * If it's null or otherwise falsey, an empty object is assumed. + * @param requirements + * An object whose keys are the expected keys in options. Any key in + * options that is not present in requirements is ignored. Each value + * in requirements is itself an object describing the requirements of + * its key. There are four optional keys in this object: + * map: A function that's passed the value of the key in options. + * map's return value is taken as the key's value in the final + * validated options, is, and ok. If map throws an exception + * it's caught and discarded, and the key's value is its value in + * options. + * is: An array containing any number of the typeof type names. If + * the key's value is none of these types, it fails validation. + * Arrays, null and regexps are identified by the special type names + * "array", "null", "regexp"; "object" will not match either. No type + * coercion is done. + * ok: A function that's passed the key's value. If it returns + * false, the value fails validation. + * msg: If the key's value fails validation, an exception is thrown. + * This string will be used as its message. If undefined, a + * generic message is used, unless is is defined, in which case + * the message will state that the value needs to be one of the + * given types. + * @return An object whose keys are those keys in requirements that are also in + * options and whose values are the corresponding return values of map + * or the corresponding values in options. Note that any keys not + * shared by both requirements and options are not in the returned + * object. + */ +exports.validateOptions = function validateOptions(options, requirements) { + options = options || {}; + let validatedOptions = {}; + + for (let key in requirements) { + let isOptional = false; + let mapThrew = false; + let req = requirements[key]; + let [optsVal, keyInOpts] = (key in options) ? + [options[key], true] : + [undefined, false]; + if (req.map) { + try { + optsVal = req.map(optsVal); + } + catch (err) { + if (err instanceof RequirementError) + throw err; + + mapThrew = true; + } + } + if (req.is) { + let types = req.is; + + if (!isArray(types) && isArray(types.is)) + types = types.is; + + if (isArray(types)) { + isOptional = ['undefined', 'null'].every(v => ~types.indexOf(v)); + + // Sanity check the caller's type names. + types.forEach(function (typ) { + if (VALID_TYPES.indexOf(typ) < 0) { + let msg = 'Internal error: invalid requirement type "' + typ + '".'; + throw new Error(msg); + } + }); + if (types.indexOf(getTypeOf(optsVal)) < 0) + throw new RequirementError(key, req); + } + } + + if (req.ok && ((!isOptional || !isNil(optsVal)) && !req.ok(optsVal))) + throw new RequirementError(key, req); + + if (keyInOpts || (req.map && !mapThrew && optsVal !== undefined)) + validatedOptions[key] = optsVal; + } + + return validatedOptions; +}; + +exports.addIterator = function addIterator(obj, keysValsGenerator) { + obj.__iterator__ = function(keysOnly, keysVals) { + let keysValsIterator = keysValsGenerator.call(this); + + // "for (.. in ..)" gets only keys, "for each (.. in ..)" gets values, + // and "for (.. in Iterator(..))" gets [key, value] pairs. + let index = keysOnly ? 0 : 1; + while (true) + yield keysVals ? keysValsIterator.next() : keysValsIterator.next()[index]; + }; +}; + +// Similar to typeof, except arrays, null and regexps are identified by "array" and +// "null" and "regexp", not "object". +var getTypeOf = exports.getTypeOf = function getTypeOf(val) { + let typ = typeof(val); + if (typ === "object") { + if (!val) + return "null"; + if (isArray(val)) + return "array"; + if (isRegExp(val)) + return "regexp"; + } + return typ; +} + +function RequirementError(key, requirement) { + Error.call(this); + + this.name = "RequirementError"; + + let msg = requirement.msg; + if (!msg) { + msg = 'The option "' + key + '" '; + msg += requirement.is ? + "must be one of the following types: " + requirement.is.join(", ") : + "is invalid."; + } + + this.message = msg; +} +RequirementError.prototype = Object.create(Error.prototype); + +var string = { is: ['string', 'undefined', 'null'] }; +exports.string = string; + +var number = { is: ['number', 'undefined', 'null'] }; +exports.number = number; + +var boolean = { is: ['boolean', 'undefined', 'null'] }; +exports.boolean = boolean; + +var object = { is: ['object', 'undefined', 'null'] }; +exports.object = object; + +var array = { is: ['array', 'undefined', 'null'] }; +exports.array = array; + +var isTruthyType = type => !(type === 'undefined' || type === 'null'); +var findTypes = v => { while (!isArray(v) && v.is) v = v.is; return v }; + +function required(req) { + let types = (findTypes(req) || VALID_TYPES).filter(isTruthyType); + + return merge({}, req, {is: types}); +} +exports.required = required; + +function optional(req) { + req = merge({is: []}, req); + req.is = findTypes(req).filter(isTruthyType).concat('undefined', 'null'); + + return req; +} +exports.optional = optional; + +function either(...types) { + return union.apply(null, types.map(findTypes)); +} +exports.either = either; diff --git a/toolkit/jetpack/sdk/deprecated/events/assembler.js b/toolkit/jetpack/sdk/deprecated/events/assembler.js new file mode 100644 index 000000000..bb297c24f --- /dev/null +++ b/toolkit/jetpack/sdk/deprecated/events/assembler.js @@ -0,0 +1,54 @@ +/* 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/. */ +"use strict"; + +const { Class } = require("../../core/heritage"); +const { removeListener, on } = require("../../dom/events"); + +/** + * Event targets + * can be added / removed by calling `observe / ignore` methods. Composer should + * provide array of event types it wishes to handle as property + * `supportedEventsTypes` and function for handling all those events as + * `handleEvent` property. + */ +exports.DOMEventAssembler = Class({ + /** + * Function that is supposed to handle all the supported events (that are + * present in the `supportedEventsTypes`) from all the observed + * `eventTargets`. + * @param {Event} event + * Event being dispatched. + */ + handleEvent() { + throw new TypeError("Instance of DOMEventAssembler must implement `handleEvent` method"); + }, + /** + * Array of supported event names. + * @type {String[]} + */ + get supportedEventsTypes() { + throw new TypeError("Instance of DOMEventAssembler must implement `handleEvent` field"); + }, + /** + * Adds `eventTarget` to the list of observed `eventTarget`s. Listeners for + * supported events will be registered on the given `eventTarget`. + * @param {EventTarget} eventTarget + */ + observe: function observe(eventTarget) { + this.supportedEventsTypes.forEach(function(eventType) { + on(eventTarget, eventType, this); + }, this); + }, + /** + * Removes `eventTarget` from the list of observed `eventTarget`s. Listeners + * for all supported events will be unregistered from the given `eventTarget`. + * @param {EventTarget} eventTarget + */ + ignore: function ignore(eventTarget) { + this.supportedEventsTypes.forEach(function(eventType) { + removeListener(eventTarget, eventType, this); + }, this); + } +}); diff --git a/toolkit/jetpack/sdk/deprecated/sync-worker.js b/toolkit/jetpack/sdk/deprecated/sync-worker.js new file mode 100644 index 000000000..71cadac36 --- /dev/null +++ b/toolkit/jetpack/sdk/deprecated/sync-worker.js @@ -0,0 +1,288 @@ +/* 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/. */ + +/** + * + * `deprecated/sync-worker` was previously `content/worker`, that was + * incompatible with e10s. we are in the process of switching to the new + * asynchronous `Worker`, which behaves slightly differently in some edge + * cases, so we are keeping this one around for a short period. + * try to switch to the new one as soon as possible.. + * + */ + +"use strict"; + +module.metadata = { + "stability": "unstable" +}; + +const { Class } = require('../core/heritage'); +const { EventTarget } = require('../event/target'); +const { on, off, emit, setListeners } = require('../event/core'); +const { + attach, detach, destroy +} = require('../content/utils'); +const { method } = require('../lang/functional'); +const { Ci, Cu, Cc } = require('chrome'); +const unload = require('../system/unload'); +const events = require('../system/events'); +const { getInnerId } = require("../window/utils"); +const { WorkerSandbox } = require('../content/sandbox'); +const { isPrivate } = require('../private-browsing/utils'); + +// A weak map of workers to hold private attributes that +// should not be exposed +const workers = new WeakMap(); + +var modelFor = (worker) => workers.get(worker); + +const ERR_DESTROYED = + "Couldn't find the worker to receive this message. " + + "The script may not be initialized yet, or may already have been unloaded."; + +const ERR_FROZEN = "The page is currently hidden and can no longer be used " + + "until it is visible again."; + +/** + * Message-passing facility for communication between code running + * in the content and add-on process. + * @see https://developer.mozilla.org/en-US/Add-ons/SDK/Low-Level_APIs/content_worker + */ +const Worker = Class({ + implements: [EventTarget], + initialize: function WorkerConstructor (options) { + // Save model in weak map to not expose properties + let model = createModel(); + workers.set(this, model); + + options = options || {}; + + if ('contentScriptFile' in options) + this.contentScriptFile = options.contentScriptFile; + if ('contentScriptOptions' in options) + this.contentScriptOptions = options.contentScriptOptions; + if ('contentScript' in options) + this.contentScript = options.contentScript; + if ('injectInDocument' in options) + this.injectInDocument = !!options.injectInDocument; + + setListeners(this, options); + + unload.ensure(this, "destroy"); + + // Ensure that worker.port is initialized for contentWorker to be able + // to send events during worker initialization. + this.port = createPort(this); + + model.documentUnload = documentUnload.bind(this); + model.pageShow = pageShow.bind(this); + model.pageHide = pageHide.bind(this); + + if ('window' in options) + attach(this, options.window); + }, + + /** + * Sends a message to the worker's global scope. Method takes single + * argument, which represents data to be sent to the worker. The data may + * be any primitive type value or `JSON`. Call of this method asynchronously + * emits `message` event with data value in the global scope of this + * worker. + * + * `message` event listeners can be set either by calling + * `self.on` with a first argument string `"message"` or by + * implementing `onMessage` function in the global scope of this worker. + * @param {Number|String|JSON} data + */ + postMessage: function (...data) { + let model = modelFor(this); + let args = ['message'].concat(data); + if (!model.inited) { + model.earlyEvents.push(args); + return; + } + processMessage.apply(null, [this].concat(args)); + }, + + get url () { + let model = modelFor(this); + // model.window will be null after detach + return model.window ? model.window.document.location.href : null; + }, + + get contentURL () { + let model = modelFor(this); + return model.window ? model.window.document.URL : null; + }, + + // Implemented to provide some of the previous features of exposing sandbox + // so that Worker can be extended + getSandbox: function () { + return modelFor(this).contentWorker; + }, + + toString: function () { return '[object Worker]'; }, + attach: method(attach), + detach: method(detach), + destroy: method(destroy) +}); +exports.Worker = Worker; + +attach.define(Worker, function (worker, window) { + let model = modelFor(worker); + model.window = window; + // Track document unload to destroy this worker. + // We can't watch for unload event on page's window object as it + // prevents bfcache from working: + // https://developer.mozilla.org/En/Working_with_BFCache + model.windowID = getInnerId(model.window); + events.on("inner-window-destroyed", model.documentUnload); + + // will set model.contentWorker pointing to the private API: + model.contentWorker = WorkerSandbox(worker, model.window); + + // Listen to pagehide event in order to freeze the content script + // while the document is frozen in bfcache: + model.window.addEventListener("pageshow", model.pageShow, true); + model.window.addEventListener("pagehide", model.pageHide, true); + + // Mainly enable worker.port.emit to send event to the content worker + model.inited = true; + model.frozen = false; + + // Fire off `attach` event + emit(worker, 'attach', window); + + // Process all events and messages that were fired before the + // worker was initialized. + model.earlyEvents.forEach(args => processMessage.apply(null, [worker].concat(args))); +}); + +/** + * Remove all internal references to the attached document + * Tells _port to unload itself and removes all the references from itself. + */ +detach.define(Worker, function (worker, reason) { + let model = modelFor(worker); + + // maybe unloaded before content side is created + if (model.contentWorker) { + model.contentWorker.destroy(reason); + } + + model.contentWorker = null; + if (model.window) { + model.window.removeEventListener("pageshow", model.pageShow, true); + model.window.removeEventListener("pagehide", model.pageHide, true); + } + model.window = null; + // This method may be called multiple times, + // avoid dispatching `detach` event more than once + if (model.windowID) { + model.windowID = null; + events.off("inner-window-destroyed", model.documentUnload); + model.earlyEvents.length = 0; + emit(worker, 'detach'); + } + model.inited = false; +}); + +isPrivate.define(Worker, ({ tab }) => isPrivate(tab)); + +/** + * Tells content worker to unload itself and + * removes all the references from itself. + */ +destroy.define(Worker, function (worker, reason) { + detach(worker, reason); + modelFor(worker).inited = true; + // Specifying no type or listener removes all listeners + // from target + off(worker); + off(worker.port); +}); + +/** + * Events fired by workers + */ +function documentUnload ({ subject, data }) { + let model = modelFor(this); + let innerWinID = subject.QueryInterface(Ci.nsISupportsPRUint64).data; + if (innerWinID != model.windowID) return false; + detach(this); + return true; +} + +function pageShow () { + let model = modelFor(this); + model.contentWorker.emitSync('pageshow'); + emit(this, 'pageshow'); + model.frozen = false; +} + +function pageHide () { + let model = modelFor(this); + model.contentWorker.emitSync('pagehide'); + emit(this, 'pagehide'); + model.frozen = true; +} + +/** + * Fired from postMessage and emitEventToContent, or from the earlyMessage + * queue when fired before the content is loaded. Sends arguments to + * contentWorker if able + */ + +function processMessage (worker, ...args) { + let model = modelFor(worker) || {}; + if (!model.contentWorker) + throw new Error(ERR_DESTROYED); + if (model.frozen) + throw new Error(ERR_FROZEN); + model.contentWorker.emit.apply(null, args); +} + +function createModel () { + return { + // List of messages fired before worker is initialized + earlyEvents: [], + // Is worker connected to the content worker sandbox ? + inited: false, + // Is worker being frozen? i.e related document is frozen in bfcache. + // Content script should not be reachable if frozen. + frozen: true, + /** + * Reference to the content side of the worker. + * @type {WorkerGlobalScope} + */ + contentWorker: null, + /** + * Reference to the window that is accessible from + * the content scripts. + * @type {Object} + */ + window: null + }; +} + +function createPort (worker) { + let port = EventTarget(); + port.emit = emitEventToContent.bind(null, worker); + return port; +} + +/** + * Emit a custom event to the content script, + * i.e. emit this event on `self.port` + */ +function emitEventToContent (worker, ...eventArgs) { + let model = modelFor(worker); + let args = ['event'].concat(eventArgs); + if (!model.inited) { + model.earlyEvents.push(args); + return; + } + processMessage.apply(null, [worker].concat(args)); +} diff --git a/toolkit/jetpack/sdk/deprecated/unit-test-finder.js b/toolkit/jetpack/sdk/deprecated/unit-test-finder.js new file mode 100644 index 000000000..e38629f45 --- /dev/null +++ b/toolkit/jetpack/sdk/deprecated/unit-test-finder.js @@ -0,0 +1,199 @@ +/* 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/. */ +"use strict"; + +module.metadata = { + "stability": "deprecated" +}; + +const file = require("../io/file"); +const { Loader } = require("../test/loader"); + +const { isNative } = require('@loader/options'); + +const cuddlefish = isNative ? require("toolkit/loader") : require("../loader/cuddlefish"); + +const { defer, resolve } = require("../core/promise"); +const { getAddon } = require("../addon/installer"); +const { id } = require("sdk/self"); +const { newURI } = require('sdk/url/utils'); +const { getZipReader } = require("../zip/utils"); + +const { Cc, Ci, Cu } = require("chrome"); +const { AddonManager } = Cu.import("resource://gre/modules/AddonManager.jsm", {}); +var ios = Cc['@mozilla.org/network/io-service;1'] + .getService(Ci.nsIIOService); + +const CFX_TEST_REGEX = /(([^\/]+\/)(?:lib\/)?)?(tests?\/test-[^\.\/]+)\.js$/; +const JPM_TEST_REGEX = /^()(tests?\/test-[^\.\/]+)\.js$/; + +const { mapcat, map, filter, fromEnumerator } = require("sdk/util/sequence"); + +const toFile = x => x.QueryInterface(Ci.nsIFile); +const isTestFile = ({leafName}) => leafName.substr(0, 5) == "test-" && leafName.substr(-3, 3) == ".js"; +const getFileURI = x => ios.newFileURI(x).spec; + +const getDirectoryEntries = file => map(toFile, fromEnumerator(_ => file.directoryEntries)); +const getTestFiles = directory => filter(isTestFile, getDirectoryEntries(directory)); +const getTestURIs = directory => map(getFileURI, getTestFiles(directory)); + +const isDirectory = x => x.isDirectory(); +const getTestEntries = directory => mapcat(entry => + /^tests?$/.test(entry.leafName) ? getTestURIs(entry) : getTestEntries(entry), + filter(isDirectory, getDirectoryEntries(directory))); + +const removeDups = (array) => array.reduce((result, value) => { + if (value != result[result.length - 1]) { + result.push(value); + } + return result; +}, []); + +const getSuites = function getSuites({ id, filter }) { + const TEST_REGEX = isNative ? JPM_TEST_REGEX : CFX_TEST_REGEX; + + return getAddon(id).then(addon => { + let fileURI = addon.getResourceURI("tests/"); + let isPacked = fileURI.scheme == "jar"; + let xpiURI = addon.getResourceURI(); + let file = xpiURI.QueryInterface(Ci.nsIFileURL).file; + let suites = []; + let addEntry = (entry) => { + if (filter(entry) && TEST_REGEX.test(entry)) { + let suite = (isNative ? "./" : "") + (RegExp.$2 || "") + RegExp.$3; + suites.push(suite); + } + } + + if (isPacked) { + return getZipReader(file).then(zip => { + let entries = zip.findEntries(null); + while (entries.hasMore()) { + let entry = entries.getNext(); + addEntry(entry); + } + zip.close(); + + // sort and remove dups + suites = removeDups(suites.sort()); + return suites; + }) + } + else { + let tests = [...getTestEntries(file)]; + let rootURI = addon.getResourceURI("/"); + tests.forEach((entry) => { + addEntry(entry.replace(rootURI.spec, "")); + }); + } + + // sort and remove dups + suites = removeDups(suites.sort()); + return suites; + }); +} +exports.getSuites = getSuites; + +const makeFilters = function makeFilters(options) { + options = options || {}; + + // A filter string is {fileNameRegex}[:{testNameRegex}] - ie, a colon + // optionally separates a regex for the test fileName from a regex for the + // testName. + if (options.filter) { + let colonPos = options.filter.indexOf(':'); + let filterFileRegex, filterNameRegex; + + if (colonPos === -1) { + filterFileRegex = new RegExp(options.filter); + filterNameRegex = { test: () => true } + } + else { + filterFileRegex = new RegExp(options.filter.substr(0, colonPos)); + filterNameRegex = new RegExp(options.filter.substr(colonPos + 1)); + } + + return { + fileFilter: (name) => filterFileRegex.test(name), + testFilter: (name) => filterNameRegex.test(name) + } + } + + return { + fileFilter: () => true, + testFilter: () => true + }; +} +exports.makeFilters = makeFilters; + +var loader = Loader(module); +const NOT_TESTS = ['setup', 'teardown']; + +var TestFinder = exports.TestFinder = function TestFinder(options) { + this.filter = options.filter; + this.testInProcess = options.testInProcess === false ? false : true; + this.testOutOfProcess = options.testOutOfProcess === true ? true : false; +}; + +TestFinder.prototype = { + findTests: function findTests() { + let { fileFilter, testFilter } = makeFilters({ filter: this.filter }); + + return getSuites({ id: id, filter: fileFilter }).then(suites => { + let testsRemaining = []; + + let getNextTest = () => { + if (testsRemaining.length) { + return testsRemaining.shift(); + } + + if (!suites.length) { + return null; + } + + let suite = suites.shift(); + + // Load each test file as a main module in its own loader instance + // `suite` is defined by cuddlefish/manifest.py:ManifestBuilder.build + let suiteModule; + + try { + suiteModule = cuddlefish.main(loader, suite); + } + catch (e) { + if (/Unsupported Application/i.test(e.message)) { + // If `Unsupported Application` error thrown during test, + // skip the test suite + suiteModule = { + 'test suite skipped': assert => assert.pass(e.message) + }; + } + else { + console.exception(e); + throw e; + } + } + + if (this.testInProcess) { + for (let name of Object.keys(suiteModule).sort()) { + if (NOT_TESTS.indexOf(name) === -1 && testFilter(name)) { + testsRemaining.push({ + setup: suiteModule.setup, + teardown: suiteModule.teardown, + testFunction: suiteModule[name], + name: suite + "." + name + }); + } + } + } + + return getNextTest(); + }; + + return { + getNext: () => resolve(getNextTest()) + }; + }); + } +}; diff --git a/toolkit/jetpack/sdk/deprecated/unit-test.js b/toolkit/jetpack/sdk/deprecated/unit-test.js new file mode 100644 index 000000000..32bba8f6b --- /dev/null +++ b/toolkit/jetpack/sdk/deprecated/unit-test.js @@ -0,0 +1,584 @@ +/* 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/. */ +"use strict"; + +module.metadata = { + "stability": "deprecated" +}; + +const timer = require("../timers"); +const cfxArgs = require("../test/options"); +const { getTabs, closeTab, getURI, getTabId, getSelectedTab } = require("../tabs/utils"); +const { windows, isBrowser, getMostRecentBrowserWindow } = require("../window/utils"); +const { defer, all, Debugging: PromiseDebugging, resolve } = require("../core/promise"); +const { getInnerId } = require("../window/utils"); +const { cleanUI } = require("../test/utils"); + +const findAndRunTests = function findAndRunTests(options) { + var TestFinder = require("./unit-test-finder").TestFinder; + var finder = new TestFinder({ + filter: options.filter, + testInProcess: options.testInProcess, + testOutOfProcess: options.testOutOfProcess + }); + var runner = new TestRunner({fs: options.fs}); + finder.findTests().then(tests => { + runner.startMany({ + tests: tests, + stopOnError: options.stopOnError, + onDone: options.onDone + }); + }); +}; +exports.findAndRunTests = findAndRunTests; + +var runnerWindows = new WeakMap(); +var runnerTabs = new WeakMap(); + +const TestRunner = function TestRunner(options) { + options = options || {}; + + // remember the id's for the open window and tab + let window = getMostRecentBrowserWindow(); + runnerWindows.set(this, getInnerId(window)); + runnerTabs.set(this, getTabId(getSelectedTab(window))); + + this.fs = options.fs; + this.console = options.console || console; + this.passed = 0; + this.failed = 0; + this.testRunSummary = []; + this.expectFailNesting = 0; + this.done = TestRunner.prototype.done.bind(this); +}; + +TestRunner.prototype = { + toString: function toString() { + return "[object TestRunner]"; + }, + + DEFAULT_PAUSE_TIMEOUT: (cfxArgs.parseable ? 300000 : 15000), //Five minutes (5*60*1000ms) + PAUSE_DELAY: 500, + + _logTestFailed: function _logTestFailed(why) { + if (!(why in this.test.errors)) + this.test.errors[why] = 0; + this.test.errors[why]++; + }, + + _uncaughtErrorObserver: function({message, date, fileName, stack, lineNumber}) { + this.fail("There was an uncaught Promise rejection: " + message + " @ " + + fileName + ":" + lineNumber + "\n" + stack); + }, + + pass: function pass(message) { + if(!this.expectFailure) { + if ("testMessage" in this.console) + this.console.testMessage(true, true, this.test.name, message); + else + this.console.info("pass:", message); + this.passed++; + this.test.passed++; + this.test.last = message; + } + else { + this.expectFailure = false; + this._logTestFailed("failure"); + if ("testMessage" in this.console) { + this.console.testMessage(true, false, this.test.name, message); + } + else { + this.console.error("fail:", 'Failure Expected: ' + message) + this.console.trace(); + } + this.failed++; + this.test.failed++; + } + }, + + fail: function fail(message) { + if(!this.expectFailure) { + this._logTestFailed("failure"); + if ("testMessage" in this.console) { + this.console.testMessage(false, false, this.test.name, message); + } + else { + this.console.error("fail:", message) + this.console.trace(); + } + this.failed++; + this.test.failed++; + } + else { + this.expectFailure = false; + if ("testMessage" in this.console) + this.console.testMessage(false, true, this.test.name, message); + else + this.console.info("pass:", message); + this.passed++; + this.test.passed++; + this.test.last = message; + } + }, + + expectFail: function(callback) { + this.expectFailure = true; + callback(); + this.expectFailure = false; + }, + + exception: function exception(e) { + this._logTestFailed("exception"); + if (cfxArgs.parseable) + this.console.print("TEST-UNEXPECTED-FAIL | " + this.test.name + " | " + e + "\n"); + this.console.exception(e); + this.failed++; + this.test.failed++; + }, + + assertMatches: function assertMatches(string, regexp, message) { + if (regexp.test(string)) { + if (!message) + message = uneval(string) + " matches " + uneval(regexp); + this.pass(message); + } else { + var no = uneval(string) + " doesn't match " + uneval(regexp); + if (!message) + message = no; + else + message = message + " (" + no + ")"; + this.fail(message); + } + }, + + assertRaises: function assertRaises(func, predicate, message) { + try { + func(); + if (message) + this.fail(message + " (no exception thrown)"); + else + this.fail("function failed to throw exception"); + } catch (e) { + var errorMessage; + if (typeof(e) == "string") + errorMessage = e; + else + errorMessage = e.message; + if (typeof(predicate) == "string") + this.assertEqual(errorMessage, predicate, message); + else + this.assertMatches(errorMessage, predicate, message); + } + }, + + assert: function assert(a, message) { + if (!a) { + if (!message) + message = "assertion failed, value is " + a; + this.fail(message); + } else + this.pass(message || "assertion successful"); + }, + + assertNotEqual: function assertNotEqual(a, b, message) { + if (a != b) { + if (!message) + message = "a != b != " + uneval(a); + this.pass(message); + } else { + var equality = uneval(a) + " == " + uneval(b); + if (!message) + message = equality; + else + message += " (" + equality + ")"; + this.fail(message); + } + }, + + assertEqual: function assertEqual(a, b, message) { + if (a == b) { + if (!message) + message = "a == b == " + uneval(a); + this.pass(message); + } else { + var inequality = uneval(a) + " != " + uneval(b); + if (!message) + message = inequality; + else + message += " (" + inequality + ")"; + this.fail(message); + } + }, + + assertNotStrictEqual: function assertNotStrictEqual(a, b, message) { + if (a !== b) { + if (!message) + message = "a !== b !== " + uneval(a); + this.pass(message); + } else { + var equality = uneval(a) + " === " + uneval(b); + if (!message) + message = equality; + else + message += " (" + equality + ")"; + this.fail(message); + } + }, + + assertStrictEqual: function assertStrictEqual(a, b, message) { + if (a === b) { + if (!message) + message = "a === b === " + uneval(a); + this.pass(message); + } else { + var inequality = uneval(a) + " !== " + uneval(b); + if (!message) + message = inequality; + else + message += " (" + inequality + ")"; + this.fail(message); + } + }, + + assertFunction: function assertFunction(a, message) { + this.assertStrictEqual('function', typeof a, message); + }, + + assertUndefined: function(a, message) { + this.assertStrictEqual('undefined', typeof a, message); + }, + + assertNotUndefined: function(a, message) { + this.assertNotStrictEqual('undefined', typeof a, message); + }, + + assertNull: function(a, message) { + this.assertStrictEqual(null, a, message); + }, + + assertNotNull: function(a, message) { + this.assertNotStrictEqual(null, a, message); + }, + + assertObject: function(a, message) { + this.assertStrictEqual('[object Object]', Object.prototype.toString.apply(a), message); + }, + + assertString: function(a, message) { + this.assertStrictEqual('[object String]', Object.prototype.toString.apply(a), message); + }, + + assertArray: function(a, message) { + this.assertStrictEqual('[object Array]', Object.prototype.toString.apply(a), message); + }, + + assertNumber: function(a, message) { + this.assertStrictEqual('[object Number]', Object.prototype.toString.apply(a), message); + }, + + done: function done() { + if (this.isDone) { + return resolve(); + } + + this.isDone = true; + this.pass("This test is done."); + + if (this.test.teardown) { + this.test.teardown(this); + } + + if (this.waitTimeout !== null) { + timer.clearTimeout(this.waitTimeout); + this.waitTimeout = null; + } + + // Do not leave any callback set when calling to `waitUntil` + this.waitUntilCallback = null; + if (this.test.passed == 0 && this.test.failed == 0) { + this._logTestFailed("empty test"); + + if ("testMessage" in this.console) { + this.console.testMessage(false, false, this.test.name, "Empty test"); + } + else { + this.console.error("fail:", "Empty test") + } + + this.failed++; + this.test.failed++; + } + + let wins = windows(null, { includePrivate: true }); + let winPromises = wins.map(win => { + return new Promise(resolve => { + if (["interactive", "complete"].indexOf(win.document.readyState) >= 0) { + resolve() + } + else { + win.addEventListener("DOMContentLoaded", function onLoad() { + win.removeEventListener("DOMContentLoaded", onLoad, false); + resolve(); + }, false); + } + }); + }); + + PromiseDebugging.flushUncaughtErrors(); + PromiseDebugging.removeUncaughtErrorObserver(this._uncaughtErrorObserver); + + + return all(winPromises).then(() => { + let browserWins = wins.filter(isBrowser); + let tabs = browserWins.reduce((tabs, window) => tabs.concat(getTabs(window)), []); + let newTabID = getTabId(getSelectedTab(wins[0])); + let oldTabID = runnerTabs.get(this); + let hasMoreTabsOpen = browserWins.length && tabs.length != 1; + let failure = false; + + if (wins.length != 1 || getInnerId(wins[0]) !== runnerWindows.get(this)) { + failure = true; + this.fail("Should not be any unexpected windows open"); + } + else if (hasMoreTabsOpen) { + failure = true; + this.fail("Should not be any unexpected tabs open"); + } + else if (oldTabID != newTabID) { + failure = true; + runnerTabs.set(this, newTabID); + this.fail("Should not be any new tabs left open, old id: " + oldTabID + " new id: " + newTabID); + } + + if (failure) { + console.log("Windows open:"); + for (let win of wins) { + if (isBrowser(win)) { + tabs = getTabs(win); + console.log(win.location + " - " + tabs.map(getURI).join(", ")); + } + else { + console.log(win.location); + } + } + } + + return failure; + }). + then(failure => { + if (!failure) { + this.pass("There was a clean UI."); + return null; + } + return cleanUI().then(() => { + this.pass("There is a clean UI."); + }); + }). + then(() => { + this.testRunSummary.push({ + name: this.test.name, + passed: this.test.passed, + failed: this.test.failed, + errors: Object.keys(this.test.errors).join(", ") + }); + + if (this.onDone !== null) { + let onDone = this.onDone; + this.onDone = null; + timer.setTimeout(_ => onDone(this)); + } + }). + catch(console.exception); + }, + + // Set of assertion functions to wait for an assertion to become true + // These functions take the same arguments as the TestRunner.assert* methods. + waitUntil: function waitUntil() { + return this._waitUntil(this.assert, arguments); + }, + + waitUntilNotEqual: function waitUntilNotEqual() { + return this._waitUntil(this.assertNotEqual, arguments); + }, + + waitUntilEqual: function waitUntilEqual() { + return this._waitUntil(this.assertEqual, arguments); + }, + + waitUntilMatches: function waitUntilMatches() { + return this._waitUntil(this.assertMatches, arguments); + }, + + /** + * Internal function that waits for an assertion to become true. + * @param {Function} assertionMethod + * Reference to a TestRunner assertion method like test.assert, + * test.assertEqual, ... + * @param {Array} args + * List of arguments to give to the previous assertion method. + * All functions in this list are going to be called to retrieve current + * assertion values. + */ + _waitUntil: function waitUntil(assertionMethod, args) { + let { promise, resolve } = defer(); + let count = 0; + let maxCount = this.DEFAULT_PAUSE_TIMEOUT / this.PAUSE_DELAY; + + // We need to ensure that test is asynchronous + if (!this.waitTimeout) + this.waitUntilDone(this.DEFAULT_PAUSE_TIMEOUT); + + let finished = false; + let test = this; + + // capture a traceback before we go async. + let traceback = require("../console/traceback"); + let stack = traceback.get(); + stack.splice(-2, 2); + let currentWaitStack = traceback.format(stack); + let timeout = null; + + function loop(stopIt) { + timeout = null; + + // Build a mockup object to fake TestRunner API and intercept calls to + // pass and fail methods, in order to retrieve nice error messages + // and assertion result + let mock = { + pass: function (msg) { + test.pass(msg); + test.waitUntilCallback = null; + if (!stopIt) + resolve(); + }, + fail: function (msg) { + // If we are called on test timeout, we stop the loop + // and print which test keeps failing: + if (stopIt) { + test.console.error("test assertion never became true:\n", + msg + "\n", + currentWaitStack); + if (timeout) + timer.clearTimeout(timeout); + return; + } + timeout = timer.setTimeout(loop, test.PAUSE_DELAY); + } + }; + + // Automatically call args closures in order to build arguments for + // assertion function + let appliedArgs = []; + for (let i = 0, l = args.length; i < l; i++) { + let a = args[i]; + if (typeof a == "function") { + try { + a = a(); + } + catch(e) { + test.fail("Exception when calling asynchronous assertion: " + e + + "\n" + e.stack); + return resolve(); + } + } + appliedArgs.push(a); + } + + // Finally call assertion function with current assertion values + assertionMethod.apply(mock, appliedArgs); + } + loop(); + this.waitUntilCallback = loop; + + return promise; + }, + + waitUntilDone: function waitUntilDone(ms) { + if (ms === undefined) + ms = this.DEFAULT_PAUSE_TIMEOUT; + + var self = this; + + function tiredOfWaiting() { + self._logTestFailed("timed out"); + if ("testMessage" in self.console) { + self.console.testMessage(false, false, self.test.name, + `Test timed out (after: ${self.test.last})`); + } + else { + self.console.error("fail:", `Timed out (after: ${self.test.last})`) + } + if (self.waitUntilCallback) { + self.waitUntilCallback(true); + self.waitUntilCallback = null; + } + self.failed++; + self.test.failed++; + self.done(); + } + + // We may already have registered a timeout callback + if (this.waitTimeout) + timer.clearTimeout(this.waitTimeout); + + this.waitTimeout = timer.setTimeout(tiredOfWaiting, ms); + }, + + startMany: function startMany(options) { + function runNextTest(self) { + let { tests, onDone } = options; + + return tests.getNext().then((test) => { + if (options.stopOnError && self.test && self.test.failed) { + self.console.error("aborted: test failed and --stop-on-error was specified"); + onDone(self); + } + else if (test) { + self.start({test: test, onDone: runNextTest}); + } + else { + onDone(self); + } + }); + } + + return runNextTest(this).catch(console.exception); + }, + + start: function start(options) { + this.test = options.test; + this.test.passed = 0; + this.test.failed = 0; + this.test.errors = {}; + this.test.last = 'START'; + PromiseDebugging.clearUncaughtErrorObservers(); + this._uncaughtErrorObserver = this._uncaughtErrorObserver.bind(this); + PromiseDebugging.addUncaughtErrorObserver(this._uncaughtErrorObserver); + + this.isDone = false; + this.onDone = function(self) { + if (cfxArgs.parseable) + self.console.print("TEST-END | " + self.test.name + "\n"); + options.onDone(self); + } + this.waitTimeout = null; + + try { + if (cfxArgs.parseable) + this.console.print("TEST-START | " + this.test.name + "\n"); + else + this.console.info("executing '" + this.test.name + "'"); + + if(this.test.setup) { + this.test.setup(this); + } + this.test.testFunction(this); + } catch (e) { + this.exception(e); + } + if (this.waitTimeout === null) + this.done(); + } +}; +exports.TestRunner = TestRunner; diff --git a/toolkit/jetpack/sdk/deprecated/window-utils.js b/toolkit/jetpack/sdk/deprecated/window-utils.js new file mode 100644 index 000000000..93c0ab7b8 --- /dev/null +++ b/toolkit/jetpack/sdk/deprecated/window-utils.js @@ -0,0 +1,193 @@ +/* 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/. */ +'use strict'; + +module.metadata = { + 'stability': 'deprecated' +}; + +const { Cc, Ci } = require('chrome'); +const events = require('../system/events'); +const { getInnerId, getOuterId, windows, isDocumentLoaded, isBrowser, + getMostRecentBrowserWindow, getToplevelWindow, getMostRecentWindow } = require('../window/utils'); +const { deprecateFunction } = require('../util/deprecate'); +const { ignoreWindow } = require('sdk/private-browsing/utils'); +const { isPrivateBrowsingSupported } = require('../self'); + +const windowWatcher = Cc['@mozilla.org/embedcomp/window-watcher;1']. + getService(Ci.nsIWindowWatcher); +const appShellService = Cc['@mozilla.org/appshell/appShellService;1']. + getService(Ci.nsIAppShellService); + +// Bug 834961: ignore private windows when they are not supported +function getWindows() { + return windows(null, { includePrivate: isPrivateBrowsingSupported }); +} + +/** + * An iterator for XUL windows currently in the application. + * + * @return A generator that yields XUL windows exposing the + * nsIDOMWindow interface. + */ +function windowIterator() { + // Bug 752631: We only pass already loaded window in order to avoid + // breaking XUL windows DOM. DOM is broken when some JS code try + // to access DOM during "uninitialized" state of the related document. + let list = getWindows().filter(isDocumentLoaded); + for (let i = 0, l = list.length; i < l; i++) { + yield list[i]; + } +}; +exports.windowIterator = windowIterator; + +/** + * An iterator for browser windows currently open in the application. + * @returns {Function} + * A generator that yields browser windows exposing the `nsIDOMWindow` + * interface. + */ +function browserWindowIterator() { + for (let window of windowIterator()) { + if (isBrowser(window)) + yield window; + } +} +exports.browserWindowIterator = browserWindowIterator; + +function WindowTracker(delegate) { + if (!(this instanceof WindowTracker)) { + return new WindowTracker(delegate); + } + + this._delegate = delegate; + + for (let window of getWindows()) + this._regWindow(window); + windowWatcher.registerNotification(this); + this._onToplevelWindowReady = this._onToplevelWindowReady.bind(this); + events.on('toplevel-window-ready', this._onToplevelWindowReady); + + require('../system/unload').ensure(this); + + return this; +}; + +WindowTracker.prototype = { + _regLoadingWindow: function _regLoadingWindow(window) { + // Bug 834961: ignore private windows when they are not supported + if (ignoreWindow(window)) + return; + + window.addEventListener('load', this, true); + }, + + _unregLoadingWindow: function _unregLoadingWindow(window) { + // This may have no effect if we ignored the window in _regLoadingWindow(). + window.removeEventListener('load', this, true); + }, + + _regWindow: function _regWindow(window) { + // Bug 834961: ignore private windows when they are not supported + if (ignoreWindow(window)) + return; + + if (window.document.readyState == 'complete') { + this._unregLoadingWindow(window); + this._delegate.onTrack(window); + } else + this._regLoadingWindow(window); + }, + + _unregWindow: function _unregWindow(window) { + if (window.document.readyState == 'complete') { + if (this._delegate.onUntrack) + this._delegate.onUntrack(window); + } else { + this._unregLoadingWindow(window); + } + }, + + unload: function unload() { + windowWatcher.unregisterNotification(this); + events.off('toplevel-window-ready', this._onToplevelWindowReady); + for (let window of getWindows()) + this._unregWindow(window); + }, + + handleEvent: function handleEvent(event) { + try { + if (event.type == 'load' && event.target) { + var window = event.target.defaultView; + if (window) + this._regWindow(getToplevelWindow(window)); + } + } + catch(e) { + console.exception(e); + } + }, + + _onToplevelWindowReady: function _onToplevelWindowReady({subject}) { + let window = getToplevelWindow(subject); + // ignore private windows if they are not supported + if (ignoreWindow(window)) + return; + this._regWindow(window); + }, + + observe: function observe(subject, topic, data) { + try { + var window = subject.QueryInterface(Ci.nsIDOMWindow); + // ignore private windows if they are not supported + if (ignoreWindow(window)) + return; + if (topic == 'domwindowclosed') + this._unregWindow(window); + } + catch(e) { + console.exception(e); + } + } +}; +exports.WindowTracker = WindowTracker; + +Object.defineProperties(exports, { + activeWindow: { + enumerable: true, + get: function() { + return getMostRecentWindow(null); + }, + set: function(window) { + try { + window.focus(); + } catch (e) {} + } + }, + activeBrowserWindow: { + enumerable: true, + get: getMostRecentBrowserWindow + } +}); + + +/** + * Returns the ID of the window's current inner window. + */ +exports.getInnerId = deprecateFunction(getInnerId, + 'require("window-utils").getInnerId is deprecated, ' + + 'please use require("sdk/window/utils").getInnerId instead' +); + +exports.getOuterId = deprecateFunction(getOuterId, + 'require("window-utils").getOuterId is deprecated, ' + + 'please use require("sdk/window/utils").getOuterId instead' +); + +exports.isBrowser = deprecateFunction(isBrowser, + 'require("window-utils").isBrowser is deprecated, ' + + 'please use require("sdk/window/utils").isBrowser instead' +); + +exports.hiddenWindow = appShellService.hiddenDOMWindow; -- cgit v1.2.3