From 1081b45e497c01b00b09d2e68b0fe93f1a8154d2 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Wed, 18 Dec 2013 12:48:38 -0600 Subject: [PATCH 01/78] Bug 946813 - Part 0: Backout ef84114446cc (bug 941012) for breaking browser debugger. r=bustage --HG-- extra : rebase_source : d2f9506adc15b5f72c0dda2fd4f28deabbf874ec --- toolkit/devtools/Loader.jsm | 1 - toolkit/devtools/server/dbg-server.jsm | 30 ++++++++++-- toolkit/devtools/server/main.js | 64 ++++++++++++++------------ 3 files changed, 60 insertions(+), 35 deletions(-) diff --git a/toolkit/devtools/Loader.jsm b/toolkit/devtools/Loader.jsm index 530831976ae7..d2c8bb034730 100644 --- a/toolkit/devtools/Loader.jsm +++ b/toolkit/devtools/Loader.jsm @@ -35,7 +35,6 @@ let loaderGlobals = { btoa: btoa, console: console, _Iterator: Iterator, - ChromeWorker: ChromeWorker, loader: { lazyGetter: XPCOMUtils.defineLazyGetter.bind(XPCOMUtils), lazyImporter: XPCOMUtils.defineLazyModuleGetter.bind(XPCOMUtils), diff --git a/toolkit/devtools/server/dbg-server.jsm b/toolkit/devtools/server/dbg-server.jsm index 4087d6ab4ec6..44d88f3c3195 100644 --- a/toolkit/devtools/server/dbg-server.jsm +++ b/toolkit/devtools/server/dbg-server.jsm @@ -15,11 +15,31 @@ const Ci = Components.interfaces; const Cc = Components.classes; const Cu = Components.utils; -const { devtools } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}); - this.EXPORTED_SYMBOLS = ["DebuggerServer", "ActorPool"]; -let server = devtools.require("devtools/server/main"); +var loadSubScript = + "function loadSubScript(aURL)\n" + + "{\n" + + "const Ci = Components.interfaces;\n" + + "const Cc = Components.classes;\n" + + " try {\n" + + " let loader = Cc[\"@mozilla.org/moz/jssubscript-loader;1\"]\n" + + " .getService(Ci.mozIJSSubScriptLoader);\n" + + " loader.loadSubScript(aURL, this);\n" + + " } catch(e) {\n" + + " dump(\"Error loading: \" + aURL + \": \" + e + \" - \" + e.stack + \"\\n\");\n" + + " throw e;\n" + + " }\n" + + "}"; -this.DebuggerServer = server.DebuggerServer; -this.ActorPool = server.ActorPool; +// Load the debugging server in a sandbox with its own compartment. +var systemPrincipal = Cc["@mozilla.org/systemprincipal;1"] + .createInstance(Ci.nsIPrincipal); + +var gGlobal = Cu.Sandbox(systemPrincipal); +gGlobal.ChromeWorker = ChromeWorker; +Cu.evalInSandbox(loadSubScript, gGlobal, "1.8"); +gGlobal.loadSubScript("resource://gre/modules/devtools/server/main.js"); + +this.DebuggerServer = gGlobal.DebuggerServer; +this.ActorPool = gGlobal.ActorPool; diff --git a/toolkit/devtools/server/main.js b/toolkit/devtools/server/main.js index 70c82f0e6183..cf55cb119c57 100644 --- a/toolkit/devtools/server/main.js +++ b/toolkit/devtools/server/main.js @@ -10,22 +10,36 @@ * debugging global. */ -// Until all Debugger server code is converted to SDK modules, -// imports Components.* alias from chrome module. -var { Ci, Cc, CC, Cu, Cr } = require("chrome"); -// On B2G, `this` != Global scope, so `Ci` won't be binded on `this` -// (i.e. this.Ci is undefined) Then later, when using loadSubScript, -// Ci,... won't be defined for sub scripts. -this.Ci = Ci; -this.Cc = Cc; -this.CC = CC; -this.Cu = Cu; -this.Cr = Cr; -// Overload `Components` to prevent SDK loader exception on Components -// object usage -Object.defineProperty(this, "Components", { - get: function () require("chrome").components -}); +// |this.require| is used to test if this file was loaded via the devtools +// loader (as it is in DebuggerProcess.jsm) or via loadSubScript (as it is from +// dbg-server.jsm). Note that testing |require| is not safe in either +// situation, as it causes a ReferenceError. +var Ci, Cc, CC, Cu, Cr, Components; +if (this.require) { + ({ Ci, Cc, CC, Cu, Cr, components: Components }) = require("chrome"); +} else { + ({ + interfaces: Ci, + classes: Cc, + Constructor: CC, + utils: Cu, + results: Cr + }) = Components; +} + +// On B2G, if |this.require| is undefined at this point, it remains undefined +// later on when |DebuggerServer.registerModule| is called. On desktop (and +// perhaps other places), if |this.require| starts out undefined, it ends up +// being set to some native code by the time we get to |registerModule|. Here +// we perform a test early on, and then cache the correct require function for +// later use. +var localRequire; +if (this.require) { + localRequire = id => require(id); +} else { + let { devtools } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}); + localRequire = id => devtools.require(id); +} const DBG_STRINGS_URI = "chrome://global/locale/devtools/debugger.properties"; @@ -54,12 +68,10 @@ function loadSubScript(aURL) } } -let {defer, resolve, reject, promised, all} = require("sdk/core/promise"); -this.defer = defer; -this.resolve = resolve; -this.reject = reject; -this.promised = promised; -this.all = all; +let loaderRequire = this.require; +this.require = null; +loadSubScript.call(this, "resource://gre/modules/commonjs/sdk/core/promise.js"); +this.require = loaderRequire; Cu.import("resource://gre/modules/devtools/SourceMap.jsm"); @@ -70,14 +82,12 @@ function dumpn(str) { dump("DBG-SERVER: " + str + "\n"); } } -this.dumpn = dumpn; function dbg_assert(cond, e) { if (!cond) { return e; } } -this.dbg_assert = dbg_assert; loadSubScript.call(this, "resource://gre/modules/devtools/server/transport.js"); @@ -314,7 +324,7 @@ var DebuggerServer = { } let moduleAPI = ModuleAPI(); - let mod = require(id); + let mod = localRequire(id); mod.register(moduleAPI); gRegisteredModules[id] = { module: mod, api: moduleAPI }; }, @@ -678,8 +688,6 @@ var DebuggerServer = { if (this.exports) { exports.DebuggerServer = DebuggerServer; } -// Needed on B2G (See header note) -this.DebuggerServer = DebuggerServer; /** * Construct an ActorPool. @@ -698,8 +706,6 @@ function ActorPool(aConnection) if (this.exports) { exports.ActorPool = ActorPool; } -// Needed on B2G (See header note) -this.ActorPool = ActorPool; ActorPool.prototype = { /** From 887cbb50afcb1788b979d62662d7497b1d0f11f6 Mon Sep 17 00:00:00 2001 From: Victor Porof Date: Wed, 18 Dec 2013 19:10:16 +0200 Subject: [PATCH 02/78] Bug 951674 - Vertical splitter isn't shown for the Events tab, r=fitzgen --- .../devtools/shared/widgets/VariablesView.jsm | 2 -- browser/themes/linux/devtools/debugger.css | 4 ++-- browser/themes/linux/devtools/widgets.css | 6 +++--- browser/themes/osx/devtools/debugger.css | 4 ++-- browser/themes/osx/devtools/widgets.css | 6 +++--- browser/themes/shared/devtools/common.css | 16 ++++++++-------- browser/themes/shared/devtools/profiler.inc.css | 6 +++--- .../themes/shared/devtools/shadereditor.inc.css | 4 ++-- browser/themes/windows/devtools/debugger.css | 4 ++-- browser/themes/windows/devtools/widgets.css | 6 +++--- 10 files changed, 28 insertions(+), 30 deletions(-) diff --git a/browser/devtools/shared/widgets/VariablesView.jsm b/browser/devtools/shared/widgets/VariablesView.jsm index 78b9731472dc..9ab1bcc9aa46 100644 --- a/browser/devtools/shared/widgets/VariablesView.jsm +++ b/browser/devtools/shared/widgets/VariablesView.jsm @@ -907,7 +907,6 @@ VariablesView.prototype = { label.className = "variables-view-empty-notice"; label.setAttribute("value", this._emptyTextValue); - this._parent.setAttribute("empty", ""); this._parent.appendChild(label); this._emptyTextNode = label; }, @@ -920,7 +919,6 @@ VariablesView.prototype = { return; } - this._parent.removeAttribute("empty"); this._parent.removeChild(this._emptyTextNode); this._emptyTextNode = null; }, diff --git a/browser/themes/linux/devtools/debugger.css b/browser/themes/linux/devtools/debugger.css index ed1d9552ec10..2b06bb3a5b37 100644 --- a/browser/themes/linux/devtools/debugger.css +++ b/browser/themes/linux/devtools/debugger.css @@ -14,7 +14,7 @@ } #sources-and-editor-splitter { - -moz-border-start-color: transparent; + border-color: transparent; } /* Sources toolbar */ @@ -485,7 +485,7 @@ } #globalsearch + .devtools-horizontal-splitter { - -moz-border-top-colors: #bfbfbf; + border-color: #bfbfbf; } .dbg-source-results { diff --git a/browser/themes/linux/devtools/widgets.css b/browser/themes/linux/devtools/widgets.css index 8342525997d9..a4b9ca72baab 100644 --- a/browser/themes/linux/devtools/widgets.css +++ b/browser/themes/linux/devtools/widgets.css @@ -271,7 +271,7 @@ .side-menu-widget-container { /* Hack: force hardware acceleration */ - transform: translateX(0px); + transform: translateZ(1px); } .side-menu-widget-container[theme="dark"] { @@ -437,9 +437,9 @@ /* VariablesView */ -.variables-view-container:not([empty]) { +.variables-view-container { /* Hack: force hardware acceleration */ - transform: translateX(1px); + transform: translateZ(1px); } .variables-view-empty-notice { diff --git a/browser/themes/osx/devtools/debugger.css b/browser/themes/osx/devtools/debugger.css index 09419851d2c1..1898c1d60be7 100644 --- a/browser/themes/osx/devtools/debugger.css +++ b/browser/themes/osx/devtools/debugger.css @@ -16,7 +16,7 @@ } #sources-and-editor-splitter { - -moz-border-start-color: transparent; + border-color: transparent; } /* Sources toolbar */ @@ -487,7 +487,7 @@ } #globalsearch + .devtools-horizontal-splitter { - -moz-border-top-colors: #bfbfbf; + border-color: #bfbfbf; } .dbg-source-results { diff --git a/browser/themes/osx/devtools/widgets.css b/browser/themes/osx/devtools/widgets.css index 6b8b33893f60..cd7b48ce2d4f 100644 --- a/browser/themes/osx/devtools/widgets.css +++ b/browser/themes/osx/devtools/widgets.css @@ -271,7 +271,7 @@ .side-menu-widget-container { /* Hack: force hardware acceleration */ - transform: translateX(0px); + transform: translateZ(1px); } .side-menu-widget-container[theme="dark"] { @@ -431,9 +431,9 @@ /* VariablesView */ -.variables-view-container:not([empty]) { +.variables-view-container { /* Hack: force hardware acceleration */ - transform: translateX(1px); + transform: translateZ(1px); } .variables-view-empty-notice { diff --git a/browser/themes/shared/devtools/common.css b/browser/themes/shared/devtools/common.css index df70486fbb63..96f160dfdba2 100644 --- a/browser/themes/shared/devtools/common.css +++ b/browser/themes/shared/devtools/common.css @@ -26,23 +26,23 @@ -moz-appearance: none; background-image: none; background-color: transparent; - border: 1px solid black; - border-width: 1px 0 0 0; + border: 0; + border-bottom: 1px solid black; min-height: 3px; height: 3px; - margin-bottom: -3px; + margin-top: -3px; position: relative; } .devtools-side-splitter { -moz-appearance: none; background-image: none; - border: 0; - -moz-border-start: 1px solid black; - min-width: 0; - width: 3px; background-color: transparent; - -moz-margin-end: -3px; + border: 0; + -moz-border-end: 1px solid black; + min-width: 3px; + width: 3px; + -moz-margin-start: -3px; position: relative; cursor: e-resize; } diff --git a/browser/themes/shared/devtools/profiler.inc.css b/browser/themes/shared/devtools/profiler.inc.css index ed6c994bbc07..4ef3ca3ca713 100644 --- a/browser/themes/shared/devtools/profiler.inc.css +++ b/browser/themes/shared/devtools/profiler.inc.css @@ -13,7 +13,7 @@ } .devtools-toolbar { - min-height: 33px; + min-height: 33px; } .profiler-sidebar { @@ -21,7 +21,7 @@ } .profiler-sidebar + .devtools-side-splitter { - -moz-border-start-color: transparent; + border-color: transparent; } .profiler-sidebar-item { @@ -86,4 +86,4 @@ #profiler-start[checked] { -moz-image-region: rect(0px,32px,16px,16px); -} \ No newline at end of file +} diff --git a/browser/themes/shared/devtools/shadereditor.inc.css b/browser/themes/shared/devtools/shadereditor.inc.css index ef93a59b399d..5dc226ffe46a 100644 --- a/browser/themes/shared/devtools/shadereditor.inc.css +++ b/browser/themes/shared/devtools/shadereditor.inc.css @@ -46,7 +46,7 @@ } #shaders-pane + .devtools-side-splitter { - -moz-border-start-color: transparent; + border-color: transparent; } .side-menu-widget-item-checkbox { @@ -88,7 +88,7 @@ /* Shader source editors */ #editors-splitter { - -moz-border-start-color: rgb(61,69,76); + border-color: rgb(61,69,76); } .editor-label { diff --git a/browser/themes/windows/devtools/debugger.css b/browser/themes/windows/devtools/debugger.css index 1aea58d0f690..e184947c6e62 100644 --- a/browser/themes/windows/devtools/debugger.css +++ b/browser/themes/windows/devtools/debugger.css @@ -14,7 +14,7 @@ } #sources-and-editor-splitter { - -moz-border-start-color: transparent; + border-color: transparent; } /* Sources toolbar */ @@ -485,7 +485,7 @@ } #globalsearch + .devtools-horizontal-splitter { - -moz-border-top-colors: #bfbfbf; + border-color: #bfbfbf; } .dbg-source-results { diff --git a/browser/themes/windows/devtools/widgets.css b/browser/themes/windows/devtools/widgets.css index e154432dd754..1565cfeddb8c 100644 --- a/browser/themes/windows/devtools/widgets.css +++ b/browser/themes/windows/devtools/widgets.css @@ -275,7 +275,7 @@ .side-menu-widget-container { /* Hack: force hardware acceleration */ - transform: translateX(0px); + transform: translateZ(1px); } .side-menu-widget-container[theme="dark"] { @@ -434,9 +434,9 @@ /* VariablesView */ -.variables-view-container:not([empty]) { +.variables-view-container { /* Hack: force hardware acceleration */ - transform: translateX(1px); + transform: translateZ(1px); } .variables-view-empty-notice { From d66455620a5fd680192d346698069081cf04f234 Mon Sep 17 00:00:00 2001 From: Victor Porof Date: Wed, 18 Dec 2013 19:10:17 +0200 Subject: [PATCH 03/78] Bug 951694 - Sources pane can overlap content over the source editor, r=fitzgen --- browser/themes/linux/devtools/debugger.css | 7 ++----- browser/themes/linux/devtools/widgets.css | 1 - browser/themes/osx/devtools/debugger.css | 7 ++----- browser/themes/osx/devtools/widgets.css | 1 - browser/themes/windows/devtools/debugger.css | 7 ++----- browser/themes/windows/devtools/widgets.css | 1 - 6 files changed, 6 insertions(+), 18 deletions(-) diff --git a/browser/themes/linux/devtools/debugger.css b/browser/themes/linux/devtools/debugger.css index 2b06bb3a5b37..c68934f613e4 100644 --- a/browser/themes/linux/devtools/debugger.css +++ b/browser/themes/linux/devtools/debugger.css @@ -5,10 +5,6 @@ /* Sources and breakpoints pane */ -#sources-pane { - min-width: 50px; -} - #sources-pane > tabs { -moz-border-end: 1px solid #222426; /* Match the sources list's dark margin. */ } @@ -72,7 +68,8 @@ #black-boxed-message, #source-progress-container { background: url(background-noise-toolbar.png) rgb(61,69,76); - /* Prevent the container deck from aquiring the height from this message. */ + /* Prevent the container deck from aquiring the size from this message. */ + min-width: 1px; min-height: 1px; color: white; } diff --git a/browser/themes/linux/devtools/widgets.css b/browser/themes/linux/devtools/widgets.css index a4b9ca72baab..2d8d1a11bb3b 100644 --- a/browser/themes/linux/devtools/widgets.css +++ b/browser/themes/linux/devtools/widgets.css @@ -6,7 +6,6 @@ /* Generic pane helpers */ .generic-toggled-side-pane { - min-width: 50px; -moz-margin-start: 0px !important; /* Unfortunately, transitions don't work properly with locale-aware properties, so both the left and right margins are set via js, while the start margin diff --git a/browser/themes/osx/devtools/debugger.css b/browser/themes/osx/devtools/debugger.css index 1898c1d60be7..545033ab400b 100644 --- a/browser/themes/osx/devtools/debugger.css +++ b/browser/themes/osx/devtools/debugger.css @@ -7,10 +7,6 @@ /* Sources and breakpoints pane */ -#sources-pane { - min-width: 50px; -} - #sources-pane > tabs { -moz-border-end: 1px solid #222426; /* Match the sources list's dark margin. */ } @@ -74,7 +70,8 @@ #black-boxed-message, #source-progress-container { background: url(background-noise-toolbar.png) rgb(61,69,76); - /* Prevent the container deck from aquiring the height from this message. */ + /* Prevent the container deck from aquiring the size from this message. */ + min-width: 1px; min-height: 1px; color: white; } diff --git a/browser/themes/osx/devtools/widgets.css b/browser/themes/osx/devtools/widgets.css index cd7b48ce2d4f..592387f3ba1f 100644 --- a/browser/themes/osx/devtools/widgets.css +++ b/browser/themes/osx/devtools/widgets.css @@ -6,7 +6,6 @@ /* Generic pane helpers */ .generic-toggled-side-pane { - min-width: 50px; -moz-margin-start: 0px !important; /* Unfortunately, transitions don't work properly with locale-aware properties, so both the left and right margins are set via js, while the start margin diff --git a/browser/themes/windows/devtools/debugger.css b/browser/themes/windows/devtools/debugger.css index e184947c6e62..c8d8cb09c3cb 100644 --- a/browser/themes/windows/devtools/debugger.css +++ b/browser/themes/windows/devtools/debugger.css @@ -5,10 +5,6 @@ /* Sources and breakpoints pane */ -#sources-pane { - min-width: 50px; -} - #sources-pane > tabs { -moz-border-end: 1px solid #222426; /* Match the sources list's dark margin. */ } @@ -72,7 +68,8 @@ #black-boxed-message, #source-progress-container { background: url(background-noise-toolbar.png) rgb(61,69,76); - /* Prevent the container deck from aquiring the height from this message. */ + /* Prevent the container deck from aquiring the size from this message. */ + min-width: 1px; min-height: 1px; color: white; } diff --git a/browser/themes/windows/devtools/widgets.css b/browser/themes/windows/devtools/widgets.css index 1565cfeddb8c..9df4907700c0 100644 --- a/browser/themes/windows/devtools/widgets.css +++ b/browser/themes/windows/devtools/widgets.css @@ -6,7 +6,6 @@ /* Generic pane helpers */ .generic-toggled-side-pane { - min-width: 50px; -moz-margin-start: 0px !important; /* Unfortunately, transitions don't work properly with locale-aware properties, so both the left and right margins are set via js, while the start margin From e081cadad51a0b1b0696a42206a7c368adb837f2 Mon Sep 17 00:00:00 2001 From: Victor Porof Date: Thu, 19 Dec 2013 13:41:46 +0200 Subject: [PATCH 04/78] Bug 951828 - Always select the variables tab when pausing, r=fitzgen --- browser/devtools/debugger/debugger-view.js | 2 +- browser/devtools/debugger/test/browser.ini | 1 + .../test/browser_dbg_break-on-dom-08.js | 57 +++++++++++++++++++ .../debugger/test/doc_event-listeners-02.html | 4 ++ 4 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 browser/devtools/debugger/test/browser_dbg_break-on-dom-08.js diff --git a/browser/devtools/debugger/debugger-view.js b/browser/devtools/debugger/debugger-view.js index f6f87f4aaa3a..9b4467318e20 100644 --- a/browser/devtools/debugger/debugger-view.js +++ b/browser/devtools/debugger/debugger-view.js @@ -537,7 +537,7 @@ let DebuggerView = { animated: true, delayed: true, callback: aCallback - }); + }, 0); }, /** diff --git a/browser/devtools/debugger/test/browser.ini b/browser/devtools/debugger/test/browser.ini index 377d59d5b3b4..373ff46ac700 100644 --- a/browser/devtools/debugger/test/browser.ini +++ b/browser/devtools/debugger/test/browser.ini @@ -80,6 +80,7 @@ support-files = [browser_dbg_break-on-dom-05.js] [browser_dbg_break-on-dom-06.js] [browser_dbg_break-on-dom-07.js] +[browser_dbg_break-on-dom-08.js] [browser_dbg_breakpoints-actual-location.js] [browser_dbg_breakpoints-button-01.js] [browser_dbg_breakpoints-button-02.js] diff --git a/browser/devtools/debugger/test/browser_dbg_break-on-dom-08.js b/browser/devtools/debugger/test/browser_dbg_break-on-dom-08.js new file mode 100644 index 000000000000..4e15e5098fd9 --- /dev/null +++ b/browser/devtools/debugger/test/browser_dbg_break-on-dom-08.js @@ -0,0 +1,57 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that breaking on an event selects the variables view tab. + */ + +const TAB_URL = EXAMPLE_URL + "doc_event-listeners-02.html"; + +function test() { + initDebugger(TAB_URL).then(([aTab, aDebuggee, aPanel]) => { + let gDebugger = aPanel.panelWin; + let gView = gDebugger.DebuggerView; + let gEvents = gView.EventListeners; + + Task.spawn(function() { + yield waitForSourceShown(aPanel, ".html"); + aDebuggee.addBodyClickEventListener(); + + let fetched = waitForDebuggerEvents(aPanel, gDebugger.EVENTS.EVENT_LISTENERS_FETCHED); + gView.toggleInstrumentsPane({ visible: true, animated: false }, 1); + yield fetched; + yield ensureThreadClientState(aPanel, "resumed"); + + is(gView.instrumentsPaneHidden, false, + "The instruments pane should be visible."); + is(gView.instrumentsPaneTab, "events-tab", + "The events tab should be selected."); + + let updated = waitForDebuggerEvents(aPanel, gDebugger.EVENTS.EVENT_BREAKPOINTS_UPDATED); + EventUtils.sendMouseEvent({ type: "click" }, getItemCheckboxNode(1), gDebugger); + yield updated; + yield ensureThreadClientState(aPanel, "resumed"); + + let paused = waitForCaretAndScopes(aPanel, 48); + // Spin the event loop before causing the debuggee to pause, to allow + // this function to yield first. + executeSoon(() => { + EventUtils.sendMouseEvent({ type: "click" }, aDebuggee.document.body, aDebuggee); + }); + yield paused; + yield ensureThreadClientState(aPanel, "paused"); + + is(gView.instrumentsPaneHidden, false, + "The instruments pane should be visible."); + is(gView.instrumentsPaneTab, "variables-tab", + "The variables tab should be selected."); + + yield resumeDebuggerThenCloseAndFinish(aPanel); + }); + + function getItemCheckboxNode(index) { + return gEvents.items[index].target.parentNode + .querySelector(".side-menu-widget-item-checkbox"); + } + }); +} diff --git a/browser/devtools/debugger/test/doc_event-listeners-02.html b/browser/devtools/debugger/test/doc_event-listeners-02.html index fbcc8c5d9aa0..6a4649de9df8 100644 --- a/browser/devtools/debugger/test/doc_event-listeners-02.html +++ b/browser/devtools/debugger/test/doc_event-listeners-02.html @@ -43,6 +43,10 @@ window.changeHandler = changeHandler; }); + + function addBodyClickEventListener() { + document.body.addEventListener("click", function() { debugger; }); + } From 675d61df9d2ffb1a7d93a1ffa60a7afc8e77a700 Mon Sep 17 00:00:00 2001 From: Manish Goregaokar Date: Thu, 19 Dec 2013 11:32:09 -0500 Subject: [PATCH 05/78] Bug 425145 - Add a preference to override autocomplete=off for password manager. r=gavin --- modules/libpref/src/init/all.js | 2 ++ toolkit/components/passwordmgr/LoginManagerContent.jsm | 8 ++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/modules/libpref/src/init/all.js b/modules/libpref/src/init/all.js index 72eea7ca0bb4..3f91d837809c 100644 --- a/modules/libpref/src/init/all.js +++ b/modules/libpref/src/init/all.js @@ -3898,6 +3898,8 @@ pref("signon.rememberSignons", true); pref("signon.autofillForms", true); pref("signon.autologin.proxy", false); pref("signon.debug", false); +// Override autocomplete=false for password manager +pref("signon.overrideAutocomplete", false); // Satchel (Form Manager) prefs pref("browser.formfill.debug", false); diff --git a/toolkit/components/passwordmgr/LoginManagerContent.jsm b/toolkit/components/passwordmgr/LoginManagerContent.jsm index 50ac2b42ca0e..51d85a265d82 100644 --- a/toolkit/components/passwordmgr/LoginManagerContent.jsm +++ b/toolkit/components/passwordmgr/LoginManagerContent.jsm @@ -13,7 +13,7 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/PrivateBrowsingUtils.jsm"); -var gEnabled = false, gDebug = false, gAutofillForms = true; // these mirror signon.* prefs +var gEnabled = false, gDebug = false, gAutofillForms = true , gOverrideAutocompleteAttribute = false; // these mirror signon.* prefs function log(...pieces) { function generateLogMessage(args) { @@ -71,6 +71,7 @@ var observer = { gDebug = Services.prefs.getBoolPref("signon.debug"); gEnabled = Services.prefs.getBoolPref("signon.rememberSignons"); gAutofillForms = Services.prefs.getBoolPref("signon.autofillForms"); + gOverrideAutocompleteAttribute = Services.prefs.getBoolPref("signon.overrideAutocomplete"); }, }; @@ -356,10 +357,13 @@ var LoginManagerContent = { * specified form input. */ _isAutocompleteDisabled : function (element) { + if (gOverrideAutocompleteAttribute) + return false; + if (element && element.hasAttribute("autocomplete") && element.getAttribute("autocomplete").toLowerCase() == "off") return true; - + return false; }, From 9e5e3e01c85710fcda159ca8fc163b3bdb2b5141 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Mon, 16 Dec 2013 14:55:37 -0500 Subject: [PATCH 06/78] Bug 803188 - Support converting between file paths and file:/// uris in OS.File. r=Yoric --- .../components/osfile/modules/ospath_unix.jsm | 36 +++++- .../components/osfile/modules/ospath_win.jsm | 56 ++++++++- .../xpcshell/test_file_URL_conversion.js | 111 ++++++++++++++++++ .../osfile/tests/xpcshell/xpcshell.ini | 1 + 4 files changed, 202 insertions(+), 2 deletions(-) create mode 100644 toolkit/components/osfile/tests/xpcshell/test_file_URL_conversion.js diff --git a/toolkit/components/osfile/modules/ospath_unix.jsm b/toolkit/components/osfile/modules/ospath_unix.jsm index 35d19b103611..a040459549ed 100644 --- a/toolkit/components/osfile/modules/ospath_unix.jsm +++ b/toolkit/components/osfile/modules/ospath_unix.jsm @@ -20,6 +20,7 @@ // Boilerplate used to be able to import this module both from the main // thread and from worker threads. if (typeof Components != "undefined") { + Components.utils.importGlobalProperties(["URL"]); // Global definition of |exports|, to keep everybody happy. // In non-main thread, |exports| is provided by the module // loader. @@ -33,7 +34,9 @@ let EXPORTED_SYMBOLS = [ "dirname", "join", "normalize", - "split" + "split", + "toFileURI", + "fromFileURI", ]; /** @@ -148,6 +151,37 @@ let split = function(path) { }; exports.split = split; +/** + * Returns the file:// URI file path of the given local file path. + */ +// The case of %3b is designed to match Services.io, but fundamentally doesn't matter. +let toFileURIExtraEncodings = {';': '%3b', '?': '%3F', "'": '%27', '#': '%23'}; +let toFileURI = function toFileURI(path) { + let uri = encodeURI(this.normalize(path)); + + // add a prefix, and encodeURI doesn't escape a few characters that we do + // want to escape, so fix that up + let prefix = "file://"; + uri = prefix + uri.replace(/[;?'#]/g, match => toFileURIExtraEncodings[match]); + + return uri; +}; +exports.toFileURI = toFileURI; + +/** + * Returns the local file path from a given file URI. + */ +let fromFileURI = function fromFileURI(uri) { + let url = new URL(uri); + if (url.protocol != 'file:') { + throw new Error("fromFileURI expects a file URI"); + } + let path = this.normalize(decodeURIComponent(url.pathname)); + return path; +}; +exports.fromFileURI = fromFileURI; + + //////////// Boilerplate if (typeof Components != "undefined") { this.EXPORTED_SYMBOLS = EXPORTED_SYMBOLS; diff --git a/toolkit/components/osfile/modules/ospath_win.jsm b/toolkit/components/osfile/modules/ospath_win.jsm index 45cc7bd507a2..bf3079638442 100644 --- a/toolkit/components/osfile/modules/ospath_win.jsm +++ b/toolkit/components/osfile/modules/ospath_win.jsm @@ -29,6 +29,7 @@ // Boilerplate used to be able to import this module both from the main // thread and from worker threads. if (typeof Components != "undefined") { + Components.utils.importGlobalProperties(["URL"]); // Global definition of |exports|, to keep everybody happy. // In non-main thread, |exports| is provided by the module // loader. @@ -44,7 +45,9 @@ let EXPORTED_SYMBOLS = [ "normalize", "split", "winGetDrive", - "winIsAbsolute" + "winIsAbsolute", + "toFileURI", + "fromFileURI", ]; /** @@ -287,6 +290,57 @@ let split = function(path) { }; exports.split = split; +/** + * Return the file:// URI file path of the given local file path. + */ +// The case of %3b is designed to match Services.io, but fundamentally doesn't matter. +let toFileURIExtraEncodings = {';': '%3b', '?': '%3F', "'": '%27', '#': '%23'}; +let toFileURI = function toFileURI(path) { + // URI-escape forward slashes and convert backward slashes to forward + path = this.normalize(path).replace(/[\\\/]/g, m => (m=='\\')? '/' : '%2F'); + let uri = encodeURI(path); + + // add a prefix, and encodeURI doesn't escape a few characters that we do + // want to escape, so fix that up + let prefix = "file:///"; + uri = prefix + uri.replace(/[;?'#]/g, match => toFileURIExtraEncodings[match]); + + // turn e.g., file:///C: into file:///C:/ + if (uri.charAt(uri.length - 1) === ':') { + uri += "/" + } + + return uri; +}; +exports.toFileURI = toFileURI; + +/** + * Returns the local file path from a given file URI. + */ +let fromFileURI = function fromFileURI(uri) { + let url = new URL(uri); + if (url.protocol != 'file:') { + throw new Error("fromFileURI expects a file URI"); + } + + // strip leading slash, since Windows paths don't start with one + uri = url.pathname.substr(1); + + let path = decodeURI(uri); + // decode a few characters where URL's parsing is overzealous + path = path.replace(/%(3b|3f|23)/gi, + match => decodeURIComponent(match)); + path = this.normalize(path); + + // this.normalize() does not remove the trailing slash if the path + // component is a drive letter. eg. 'C:\'' will not get normalized. + if (path.endsWith(":\\")) { + path = path.substr(0, path.length - 1); + } + return this.normalize(path); +}; +exports.fromFileURI = fromFileURI; + /** * Utility function: Remove any leading/trailing backslashes * from a string. diff --git a/toolkit/components/osfile/tests/xpcshell/test_file_URL_conversion.js b/toolkit/components/osfile/tests/xpcshell/test_file_URL_conversion.js new file mode 100644 index 000000000000..9802a2a9d9ea --- /dev/null +++ b/toolkit/components/osfile/tests/xpcshell/test_file_URL_conversion.js @@ -0,0 +1,111 @@ +/* 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/. */ + +function run_test() { + Components.utils.import("resource://gre/modules/Services.jsm"); + Components.utils.import("resource://gre/modules/osfile.jsm"); + Components.utils.import("resource://gre/modules/FileUtils.jsm"); + + let isWindows = ("@mozilla.org/windows-registry-key;1" in Components.classes); + + // Test cases for filePathToURI + let paths = isWindows ? [ + 'C:\\', + 'C:\\test', + 'C:\\test\\', + 'C:\\test%2f', + 'C:\\test\\test\\test', + 'C:\\test;+%', + 'C:\\test?action=index\\', + 'C:\\test\ test', + '\\\\C:\\a\\b\\c', + '\\\\Server\\a\\b\\c', + + // note that per http://support.microsoft.com/kb/177506 (under more info), + // the following characters are allowed on Windows: + 'C:\\char^', + 'C:\\char&', + 'C:\\char\'', + 'C:\\char@', + 'C:\\char{', + 'C:\\char}', + 'C:\\char[', + 'C:\\char]', + 'C:\\char,', + 'C:\\char$', + 'C:\\char=', + 'C:\\char!', + 'C:\\char-', + 'C:\\char#', + 'C:\\char(', + 'C:\\char)', + 'C:\\char%', + 'C:\\char.', + 'C:\\char+', + 'C:\\char~', + 'C:\\char_' + ] : [ + '/', + '/test', + '/test/', + '/test%2f', + '/test/test/test', + '/test;+%', + '/test?action=index/', + '/test\ test', + '/punctuation/;,/?:@&=+$-_.!~*\'()"#', + '/CasePreserving' + ]; + + // some additional URIs to test, beyond those generated from paths + let uris = isWindows ? [ + 'file:///C:/test/', + 'file://localhost/C:/test', + 'file:///c:/test/test.txt', + //'file:///C:/foo%2f', // trailing, encoded slash + 'file:///C:/%3f%3F', + 'file:///C:/%3b%3B', + 'file:///C:/%3c%3C', // not one of the special-cased ? or ; + 'file:///C:/%78', // 'x', not usually uri encoded + 'file:///C:/test#frag', // a fragment identifier + 'file:///C:/test?action=index' // an actual query component + ] : [ + 'file:///test/', + 'file://localhost/test', + 'file:///test/test.txt', + 'file:///foo%2f', // trailing, encoded slash + 'file:///%3f%3F', + 'file:///%3b%3B', + 'file:///%3c%3C', // not one of the special-cased ? or ; + 'file:///%78', // 'x', not usually uri encoded + 'file:///test#frag', // a fragment identifier + 'file:///test?action=index' // an actual query component + ]; + + for (let path of paths) { + // convert that to a uri using FileUtils and Services, which toFileURI is trying to model + let file = FileUtils.File(path); + let uri = Services.io.newFileURI(file).spec; + do_check_eq(uri, OS.Path.toFileURI(path)); + + // keep the resulting URI to try the reverse + uris.push(uri) + } + + for (let uri of uris) { + // convert URIs to paths with nsIFileURI, which fromFileURI is trying to model + let path = Services.io.newURI(uri, null, null).QueryInterface(Components.interfaces.nsIFileURL).file.path; + do_check_eq(path, OS.Path.fromFileURI(uri)); + } + + // check that non-file URLs aren't allowed + let thrown = false; + try { + OS.Path.fromFileURI('http://test.com') + } catch (e) { + do_check_eq(e.message, "fromFileURI expects a file URI"); + thrown = true; + } + do_check_true(thrown); +} diff --git a/toolkit/components/osfile/tests/xpcshell/xpcshell.ini b/toolkit/components/osfile/tests/xpcshell/xpcshell.ini index 19646c017c28..29bc2b02e049 100644 --- a/toolkit/components/osfile/tests/xpcshell/xpcshell.ini +++ b/toolkit/components/osfile/tests/xpcshell/xpcshell.ini @@ -13,6 +13,7 @@ tail = [test_removeEmptyDir.js] [test_makeDir.js] [test_profiledir.js] +[test_file_URL_conversion.js] [test_logging.js] [test_creationDate.js] [test_exception.js] From 920887ea39fc6fac5e64ed21bed5673414c34484 Mon Sep 17 00:00:00 2001 From: Tim Abraldes Date: Thu, 19 Dec 2013 11:32:12 -0500 Subject: [PATCH 07/78] Bug 850372 - Delete session store backup during process suspend so that termination-from-suspended-state is treated as a normal shutdown. r=mbrubeck --- browser/metro/components/SessionStore.js | 58 ++++++++++++++++++++---- widget/nsIWinMetroUtils.idl | 9 +++- widget/windows/winrt/FrameworkView.cpp | 15 ++++-- widget/windows/winrt/FrameworkView.h | 2 + widget/windows/winrt/nsWinMetroUtils.cpp | 11 +++++ 5 files changed, 80 insertions(+), 15 deletions(-) diff --git a/browser/metro/components/SessionStore.js b/browser/metro/components/SessionStore.js index 0ce923ed2447..1353e5fdc684 100644 --- a/browser/metro/components/SessionStore.js +++ b/browser/metro/components/SessionStore.js @@ -62,24 +62,64 @@ SessionStore.prototype = { this._loadState = STATE_STOPPED; try { + let shutdownWasUnclean = false; + if (this._sessionFileBackup.exists()) { - this._shouldRestore = true; this._sessionFileBackup.remove(false); + shutdownWasUnclean = true; } if (this._sessionFile.exists()) { - // Disable crash recovery if we have exceeded the timeout - this._lastSessionTime = this._sessionFile.lastModifiedTime; - let delta = Date.now() - this._lastSessionTime; - let timeout = Services.prefs.getIntPref("browser.sessionstore.resume_from_crash_timeout"); - if (delta > (timeout * 60000)) - this._shouldRestore = false; - this._sessionFile.copyTo(null, this._sessionFileBackup.leafName); + + switch(Services.metro.previousExecutionState) { + // 0 == NotRunning + case 0: + // Disable crash recovery if we have exceeded the timeout + this._lastSessionTime = this._sessionFile.lastModifiedTime; + let delta = Date.now() - this._lastSessionTime; + let timeout = + Services.prefs.getIntPref( + "browser.sessionstore.resume_from_crash_timeout"); + this._shouldRestore = shutdownWasUnclean + && (delta < (timeout * 60000)); + break; + // 1 == Running + case 1: + // We should never encounter this situation + Components.utils.reportError("SessionRestore.init called with " + + "previous execution state 'Running'"); + this._shouldRestore = true; + break; + // 2 == Suspended + case 2: + // We should never encounter this situation + Components.utils.reportError("SessionRestore.init called with " + + "previous execution state 'Suspended'"); + this._shouldRestore = true; + break; + // 3 == Terminated + case 3: + // Terminated means that Windows terminated our already-suspended + // process to get back some resources. When we re-launch, we want + // to provide the illusion that our process was suspended the + // whole time, and never terminated. + this._shouldRestore = true; + break; + // 4 == ClosedByUser + case 4: + // ClosedByUser indicates that the user performed a "close" gesture + // on our tile. We should act as if the browser closed normally, + // even if we were closed from a suspended state (in which case + // we'll have determined that it was an unclean shtudown) + this._shouldRestore = false; + break; + } } - if (!this._sessionCache.exists() || !this._sessionCache.isDirectory()) + if (!this._sessionCache.exists() || !this._sessionCache.isDirectory()) { this._sessionCache.create(Ci.nsIFile.DIRECTORY_TYPE, 0700); + } } catch (ex) { Cu.reportError(ex); // file was write-locked? } diff --git a/widget/nsIWinMetroUtils.idl b/widget/nsIWinMetroUtils.idl index 94d44909f879..211c23d3d596 100644 --- a/widget/nsIWinMetroUtils.idl +++ b/widget/nsIWinMetroUtils.idl @@ -12,7 +12,7 @@ * implementation of this interface for non-Windows systems, for testing and * development purposes only. */ -[scriptable, uuid(d30daa27-ce2b-4503-80cc-b162f4c24e93)] +[scriptable, uuid(148f57d3-51df-4e5c-a40b-7ae5ee76188e)] interface nsIWinMetroUtils : nsISupports { /** @@ -26,6 +26,13 @@ interface nsIWinMetroUtils : nsISupports */ readonly attribute AString activationURI; + /** + * Determine the previous execution state. The possible values of this + * attribute are exactly those values in the + * Windows::ApplicationModel::Activation enumeration. + */ + readonly attribute long previousExecutionState; + /** * Show the settings flyout */ diff --git a/widget/windows/winrt/FrameworkView.cpp b/widget/windows/winrt/FrameworkView.cpp index 1c3cf270313c..405a409115b9 100644 --- a/widget/windows/winrt/FrameworkView.cpp +++ b/widget/windows/winrt/FrameworkView.cpp @@ -352,15 +352,20 @@ FrameworkView::OnActivated(ICoreApplicationView* aApplicationView, { LogFunction(); - ApplicationExecutionState state; - aArgs->get_PreviousExecutionState(&state); - bool startup = state == ApplicationExecutionState::ApplicationExecutionState_Terminated || - state == ApplicationExecutionState::ApplicationExecutionState_ClosedByUser || - state == ApplicationExecutionState::ApplicationExecutionState_NotRunning; + aArgs->get_PreviousExecutionState(&mPreviousExecutionState); + bool startup = mPreviousExecutionState == ApplicationExecutionState::ApplicationExecutionState_Terminated || + mPreviousExecutionState == ApplicationExecutionState::ApplicationExecutionState_ClosedByUser || + mPreviousExecutionState == ApplicationExecutionState::ApplicationExecutionState_NotRunning; ProcessActivationArgs(aArgs, startup); return S_OK; } +int +FrameworkView::GetPreviousExecutionState() +{ + return mPreviousExecutionState; +} + HRESULT FrameworkView::OnSoftkeyboardHidden(IInputPane* aSender, IInputPaneVisibilityEventArgs* aArgs) diff --git a/widget/windows/winrt/FrameworkView.h b/widget/windows/winrt/FrameworkView.h index ea0139160e3d..ba41092143a1 100644 --- a/widget/windows/winrt/FrameworkView.h +++ b/widget/windows/winrt/FrameworkView.h @@ -81,6 +81,7 @@ public: HRESULT ActivateView(); // Public apis for MetroWidget + int GetPreviousExecutionState(); void ShutdownXPCOM(); float GetDPI() { return mDPI; } ICoreWindow* GetCoreWindow() { return mWindow.Get(); } @@ -177,6 +178,7 @@ private: EventRegistrationToken mPrintManager; private: + ABI::Windows::ApplicationModel::Activation::ApplicationExecutionState mPreviousExecutionState; nsIntRect mWindowBounds; // in device-pixel coordinates float mDPI; bool mShuttingDown; diff --git a/widget/windows/winrt/nsWinMetroUtils.cpp b/widget/windows/winrt/nsWinMetroUtils.cpp index 1c4fb2c1dc89..2640a588d194 100644 --- a/widget/windows/winrt/nsWinMetroUtils.cpp +++ b/widget/windows/winrt/nsWinMetroUtils.cpp @@ -247,6 +247,17 @@ nsWinMetroUtils::GetActivationURI(nsAString &aActivationURI) return NS_OK; } +NS_IMETHODIMP +nsWinMetroUtils::GetPreviousExecutionState(int32_t *out) +{ + if (!sFrameworkView) { + NS_WARNING("GetPreviousExecutionState used before view is created!"); + return NS_OK; + } + *out = sFrameworkView->GetPreviousExecutionState(); + return NS_OK; +} + NS_IMETHODIMP nsWinMetroUtils::GetKeyboardVisible(bool *aImersive) { From d13f3cb8b33d9b1dcad8a2125b2a26ff1723574f Mon Sep 17 00:00:00 2001 From: Heather Arthur Date: Thu, 19 Dec 2013 11:32:12 -0500 Subject: [PATCH 08/78] Bug 949556 - Add Firefox 26-28 backwards compatibility to Style Editor. r=paul --- b2g/chrome/content/shell.js | 1 + .../devtools/styleeditor/styleeditor-panel.js | 17 +- toolkit/devtools/server/actors/styleeditor.js | 446 +++----- toolkit/devtools/server/actors/styles.js | 2 +- toolkit/devtools/server/actors/stylesheets.js | 986 ++++++++++++++++++ toolkit/devtools/server/main.js | 2 + 6 files changed, 1131 insertions(+), 323 deletions(-) create mode 100644 toolkit/devtools/server/actors/stylesheets.js diff --git a/b2g/chrome/content/shell.js b/b2g/chrome/content/shell.js index 07b64c7d54de..be0456240e58 100644 --- a/b2g/chrome/content/shell.js +++ b/b2g/chrome/content/shell.js @@ -1067,6 +1067,7 @@ let RemoteDebugger = { } DebuggerServer.registerModule("devtools/server/actors/inspector"); DebuggerServer.registerModule("devtools/server/actors/styleeditor"); + DebuggerServer.registerModule("devtools/server/actors/stylesheets"); DebuggerServer.enableWebappsContentActor = true; } DebuggerServer.addActors('chrome://browser/content/dbg-browser-actors.js'); diff --git a/browser/devtools/styleeditor/styleeditor-panel.js b/browser/devtools/styleeditor/styleeditor-panel.js index 9940fabd15db..ad13ad250a6c 100644 --- a/browser/devtools/styleeditor/styleeditor-panel.js +++ b/browser/devtools/styleeditor/styleeditor-panel.js @@ -16,7 +16,10 @@ Cu.import("resource:///modules/devtools/StyleEditorUI.jsm"); Cu.import("resource:///modules/devtools/StyleEditorUtil.jsm"); loader.lazyGetter(this, "StyleSheetsFront", - () => require("devtools/server/actors/styleeditor").StyleSheetsFront); + () => require("devtools/server/actors/stylesheets").StyleSheetsFront); + +loader.lazyGetter(this, "StyleEditorFront", + () => require("devtools/server/actors/styleeditor").StyleEditorFront); this.StyleEditorPanel = function StyleEditorPanel(panelWin, toolbox) { EventEmitter.decorate(this); @@ -54,14 +57,20 @@ StyleEditorPanel.prototype = { targetPromise.then(() => { this.target.on("close", this.destroy); - this._debuggee = StyleSheetsFront(this.target.client, this.target.form); - + if (this.target.form.styleSheetsActor) { + this._debuggee = StyleSheetsFront(this.target.client, this.target.form); + } + else { + /* We're talking to a pre-Firefox 29 server-side */ + this._debuggee = StyleEditorFront(this.target.client, this.target.form); + } this.UI = new StyleEditorUI(this._debuggee, this.target, this._panelDoc); this.UI.on("error", this._showError); this.isReady = true; + deferred.resolve(this); - }) + }, console.error); return deferred.promise; }, diff --git a/toolkit/devtools/server/actors/styleeditor.js b/toolkit/devtools/server/actors/styleeditor.js index dff2afb60492..d377912137cb 100644 --- a/toolkit/devtools/server/actors/styleeditor.js +++ b/toolkit/devtools/server/actors/styleeditor.js @@ -33,24 +33,23 @@ transition-property: all !important;\ let LOAD_ERROR = "error-load"; exports.register = function(handle) { - handle.addTabActor(StyleSheetsActor, "styleSheetsActor"); - handle.addGlobalActor(StyleSheetsActor, "styleSheetsActor"); + handle.addTabActor(StyleEditorActor, "styleEditorActor"); + handle.addGlobalActor(StyleEditorActor, "styleEditorActor"); }; exports.unregister = function(handle) { - handle.removeTabActor(StyleSheetsActor); - handle.removeGlobalActor(StyleSheetsActor); + handle.removeTabActor(StyleEditorActor); + handle.removeGlobalActor(StyleEditorActor); }; -types.addActorType("stylesheet"); -types.addActorType("originalsource"); +types.addActorType("old-stylesheet"); /** - * Creates a StyleSheetsActor. StyleSheetsActor provides remote access to the + * Creates a StyleEditorActor. StyleEditorActor provides remote access to the * stylesheets of a document. */ -let StyleSheetsActor = protocol.ActorClass({ - typeName: "stylesheets", +let StyleEditorActor = protocol.ActorClass({ + typeName: "styleeditor", /** * The window we work with, taken from the parent actor. @@ -62,6 +61,13 @@ let StyleSheetsActor = protocol.ActorClass({ */ get document() this.window.document, + events: { + "document-load" : { + type: "documentLoad", + styleSheets: Arg(0, "array:old-stylesheet") + } + }, + form: function() { return { actor: this.actorID }; @@ -77,7 +83,7 @@ let StyleSheetsActor = protocol.ActorClass({ }, /** - * Destroy the current StyleSheetsActor instance. + * Destroy the current StyleEditorActor instance. */ destroy: function() { @@ -85,40 +91,46 @@ let StyleSheetsActor = protocol.ActorClass({ }, /** - * Protocol method for getting a list of StyleSheetActors representing - * all the style sheets in this document. + * Called by client when target navigates to a new document. + * Adds load listeners to document. */ - getStyleSheets: method(function() { - let deferred = promise.defer(); + newDocument: method(function() { + // delete previous document's actors + this._clearStyleSheetActors(); - let window = this.window; - var domReady = () => { - window.removeEventListener("DOMContentLoaded", domReady, true); + // Note: listening for load won't be necessary once + // https://bugzilla.mozilla.org/show_bug.cgi?id=839103 is fixed + if (this.document.readyState == "complete") { + this._onDocumentLoaded(); + } + else { + this.window.addEventListener("load", this._onDocumentLoaded, false); + } + return {}; + }), - let documents = [this.document]; - let actors = []; - for (let doc of documents) { - let sheets = this._addStyleSheets(doc.styleSheets); - actors = actors.concat(sheets); - // Recursively handle style sheets of the documents in iframes. - for (let iframe of doc.getElementsByTagName("iframe")) { - documents.push(iframe.contentDocument); - } - } - deferred.resolve(actors); - }; - - if (window.document.readyState === "loading") { - window.addEventListener("DOMContentLoaded", domReady, true); - } else { - domReady(); + /** + * Event handler for document loaded event. Add actor for each stylesheet + * and send an event notifying of the load + */ + _onDocumentLoaded: function(event) { + if (event) { + this.window.removeEventListener("load", this._onDocumentLoaded, false); } - return deferred.promise; - }, { - request: {}, - response: { styleSheets: RetVal("array:stylesheet") } - }), + let documents = [this.document]; + var forms = []; + for (let doc of documents) { + let sheetForms = this._addStyleSheets(doc.styleSheets); + forms = forms.concat(sheetForms); + // Recursively handle style sheets of the documents in iframes. + for (let iframe of doc.getElementsByTagName("iframe")) { + documents.push(iframe.contentDocument); + } + } + + events.emit(this, "document-load", forms); + }, /** * Add all the stylesheets to the map and create an actor for each one @@ -145,6 +157,27 @@ let StyleSheetsActor = protocol.ActorClass({ return actors; }, + /** + * Create a new actor for a style sheet, if it hasn't already been created. + * + * @param {DOMStyleSheet} styleSheet + * The style sheet to create an actor for. + * @return {StyleSheetActor} + * The actor for this style sheet + */ + _createStyleSheetActor: function(styleSheet) + { + if (this._sheets.has(styleSheet)) { + return this._sheets.get(styleSheet); + } + let actor = new OldStyleSheetActor(styleSheet, this); + + this.manage(actor); + this._sheets.set(styleSheet, actor); + + return actor; + }, + /** * Get all the stylesheets @imported from a stylesheet. * @@ -177,27 +210,6 @@ let StyleSheetsActor = protocol.ActorClass({ return imported; }, - /** - * Create a new actor for a style sheet, if it hasn't already been created. - * - * @param {DOMStyleSheet} styleSheet - * The style sheet to create an actor for. - * @return {StyleSheetActor} - * The actor for this style sheet - */ - _createStyleSheetActor: function(styleSheet) - { - if (this._sheets.has(styleSheet)) { - return this._sheets.get(styleSheet); - } - let actor = new StyleSheetActor(styleSheet, this); - - this.manage(actor); - this._sheets.set(styleSheet, actor); - - return actor; - }, - /** * Clear all the current stylesheet actors in map. */ @@ -217,7 +229,7 @@ let StyleSheetsActor = protocol.ActorClass({ * @return {object} * Object with 'styelSheet' property for form on new actor. */ - addStyleSheet: method(function(text) { + newStyleSheet: method(function(text) { let parent = this.document.documentElement; let style = this.document.createElementNS("http://www.w3.org/1999/xhtml", "style"); style.setAttribute("type", "text/css"); @@ -231,28 +243,43 @@ let StyleSheetsActor = protocol.ActorClass({ return actor; }, { request: { text: Arg(0, "string") }, - response: { styleSheet: RetVal("stylesheet") } + response: { styleSheet: RetVal("old-stylesheet") } }) }); /** - * The corresponding Front object for the StyleSheetsActor. + * The corresponding Front object for the StyleEditorActor. */ -let StyleSheetsFront = protocol.FrontClass(StyleSheetsActor, { +let StyleEditorFront = protocol.FrontClass(StyleEditorActor, { initialize: function(client, tabForm) { protocol.Front.prototype.initialize.call(this, client); - this.actorID = tabForm.styleSheetsActor; + this.actorID = tabForm.styleEditorActor; client.addActorPool(this); this.manage(this); + }, + + getStyleSheets: function() { + let deferred = promise.defer(); + + events.once(this, "document-load", (styleSheets) => { + deferred.resolve(styleSheets); + }); + this.newDocument(); + + return deferred.promise; + }, + + addStyleSheet: function(text) { + return this.newStyleSheet(text); } }); /** * A StyleSheetActor represents a stylesheet on the server. */ -let StyleSheetActor = protocol.ActorClass({ - typeName: "stylesheet", +let OldStyleSheetActor = protocol.ActorClass({ + typeName: "old-stylesheet", events: { "property-change" : { @@ -260,16 +287,17 @@ let StyleSheetActor = protocol.ActorClass({ property: Arg(0, "string"), value: Arg(1, "json") }, + "source-load" : { + type: "sourceLoad", + source: Arg(0, "string") + }, "style-applied" : { type: "styleApplied" } }, - /* List of original sources that generated this stylesheet */ - _originalSources: null, - toString: function() { - return "[StyleSheetActor " + this.actorID + "]"; + return "[OldStyleSheetActor " + this.actorID + "]"; }, /** @@ -399,17 +427,14 @@ let StyleSheetActor = protocol.ActorClass({ events.emit(this, "property-change", property, this.form()[property]); }, - /** - * Protocol method to get the text of this stylesheet. - */ - getText: method(function() { - return this._getText().then((text) => { - return new LongStringActor(this.conn, text || ""); + /** + * Fetch the source of the style sheet from its URL. Send a "sourceLoad" + * event when it's been fetched. + */ + fetchSource: method(function() { + this._getText().then((content) => { + events.emit(this, "source-load", this.text); }); - }, { - response: { - text: RetVal("longstring") - } }), /** @@ -442,163 +467,6 @@ let StyleSheetActor = protocol.ActorClass({ }); }, - /** - * Protocol method to get the original source (actors) for this - * stylesheet if it has uses source maps. - */ - getOriginalSources: method(function() { - if (this._originalSources) { - return promise.resolve(this._originalSources); - } - return this._fetchOriginalSources(); - }, { - request: {}, - response: { - originalSources: RetVal("nullable:array:originalsource") - } - }), - - /** - * Fetch the original sources (actors) for this style sheet using its - * source map. If they've already been fetched, returns cached array. - * - * @return {Promise} - * Promise that resolves with an array of OriginalSourceActors - */ - _fetchOriginalSources: function() { - this._clearOriginalSources(); - this._originalSources = []; - - return this.getSourceMap().then((sourceMap) => { - if (!sourceMap) { - return null; - } - for (let url of sourceMap.sources) { - let actor = new OriginalSourceActor(url, sourceMap, this); - - this.manage(actor); - this._originalSources.push(actor); - } - return this._originalSources; - }) - }, - - /** - * Get the SourceMapConsumer for this stylesheet's source map, if - * it exists. Saves the consumer for later queries. - * - * @return {Promise} - * A promise that resolves with a SourceMapConsumer, or null. - */ - getSourceMap: function() { - if (this._sourceMap) { - return this._sourceMap; - } - return this._fetchSourceMap(); - }, - - /** - * Fetch the source map for this stylesheet. - * - * @return {Promise} - * A promise that resolves with a SourceMapConsumer, or null. - */ - _fetchSourceMap: function() { - let deferred = promise.defer(); - - this._getText().then((content) => { - let url = this._extractSourceMapUrl(content); - if (!url) { - // no source map for this stylesheet - deferred.resolve(null); - return; - }; - - url = normalize(url, this.href); - - let map = fetch(url, { loadFromCache: false, window: this.window }) - .then(({content}) => { - let map = new SourceMapConsumer(content); - this._setSourceMapRoot(map, url, this.href); - this._sourceMap = promise.resolve(map); - - deferred.resolve(map); - return map; - }, deferred.reject); - - this._sourceMap = map; - }, deferred.reject); - - return deferred.promise; - }, - - /** - * Clear and unmanage the original source actors for this stylesheet. - */ - _clearOriginalSources: function() { - for (actor in this._originalSources) { - this.unmanage(actor); - } - this._originalSources = null; - }, - - /** - * Sets the source map's sourceRoot to be relative to the source map url. - */ - _setSourceMapRoot: function(aSourceMap, aAbsSourceMapURL, aScriptURL) { - const base = dirname( - aAbsSourceMapURL.indexOf("data:") === 0 - ? aScriptURL - : aAbsSourceMapURL); - aSourceMap.sourceRoot = aSourceMap.sourceRoot - ? normalize(aSourceMap.sourceRoot, base) - : base; - }, - - /** - * Get the source map url specified in the text of a stylesheet. - * - * @param {string} content - * The text of the style sheet. - * @return {string} - * Url of source map. - */ - _extractSourceMapUrl: function(content) { - var matches = /sourceMappingURL\=([^\s\*]*)/.exec(content); - if (matches) { - return matches[1]; - } - return null; - }, - - /** - * Protocol method that gets the location in the original source of a - * line, column pair in this stylesheet, if its source mapped, otherwise - * a promise of the same location. - */ - getOriginalLocation: method(function(line, column) { - return this.getSourceMap().then((sourceMap) => { - if (sourceMap) { - return sourceMap.originalPositionFor({ line: line, column: column }); - } - return { - source: this.href, - line: line, - column: column - } - }); - }, { - request: { - line: Arg(0, "number"), - column: Arg(1, "number") - }, - response: RetVal(types.addDictType("originallocationresponse", { - source: "string", - line: "number", - column: "number" - })) - }), - /** * Get the charset of the stylesheet according to the character set rules * defined in . @@ -685,7 +553,7 @@ let StyleSheetActor = protocol.ActorClass({ _insertTransistionRule: function() { // Insert the global transition rule // Use a ref count to make sure we do not add it multiple times.. and remove - // it only when all pending StyleSheets-generated transitions ended. + // it only when all pending StyleEditor-generated transitions ended. if (this._transitionRefCount == 0) { this.rawSheet.insertRule(TRANSITION_RULE, this.rawSheet.cssRules.length); this.document.documentElement.classList.add(TRANSITION_CLASS); @@ -717,7 +585,7 @@ let StyleSheetActor = protocol.ActorClass({ /** * StyleSheetFront is the client-side counterpart to a StyleSheetActor. */ -var StyleSheetFront = protocol.FrontClass(StyleSheetActor, { +var OldStyleSheetFront = protocol.FrontClass(OldStyleSheetActor, { initialize: function(conn, form, ctx, detail) { protocol.Front.prototype.initialize.call(this, conn, form, ctx, detail); @@ -744,6 +612,22 @@ var StyleSheetFront = protocol.FrontClass(StyleSheetActor, { this._form = form; }, + getText: function() { + let deferred = promise.defer(); + + events.once(this, "source-load", (source) => { + let longStr = new ShortLongString(source); + deferred.resolve(longStr); + }); + this.fetchSource(); + + return deferred.promise; + }, + + getOriginalSources: function() { + return promise.resolve([]); + }, + get href() this._form.href, get nodeHref() this._form.nodeHref, get disabled() !!this._form.disabled, @@ -753,89 +637,15 @@ var StyleSheetFront = protocol.FrontClass(StyleSheetActor, { get ruleCount() this._form.ruleCount }); -/** - * Actor representing an original source of a style sheet that was specified - * in a source map. - */ -let OriginalSourceActor = protocol.ActorClass({ - typeName: "originalsource", - - initialize: function(aUrl, aSourceMap, aParentActor) { - protocol.Actor.prototype.initialize.call(this, null); - - this.url = aUrl; - this.sourceMap = aSourceMap; - this.parentActor = aParentActor; - this.conn = this.parentActor.conn; - - this.text = null; - }, - - form: function() { - return { - actor: this.actorID, // actorID is set when it's added to a pool - url: this.url, - parentSource: this.parentActor.actorID - }; - }, - - _getText: function() { - if (this.text) { - return promise.resolve(this.text); - } - return fetch(this.url, { window: this.window }).then(({content}) => { - this.text = content; - return content; - }); - }, - - /** - * Protocol method to get the text of this source. - */ - getText: method(function() { - return this._getText().then((text) => { - return new LongStringActor(this.conn, text || ""); - }); - }, { - response: { - text: RetVal("longstring") - } - }) -}) - -/** - * The client-side counterpart for an OriginalSourceActor. - */ -let OriginalSourceFront = protocol.FrontClass(OriginalSourceActor, { - initialize: function(client, form) { - protocol.Front.prototype.initialize.call(this, client, form); - - this.isOriginalSource = true; - }, - - form: function(form, detail) { - if (detail === "actorid") { - this.actorID = form; - return; - } - this.actorID = form.actor; - this._form = form; - }, - - get href() this._form.url, - get url() this._form.url -}); - - XPCOMUtils.defineLazyGetter(this, "DOMUtils", function () { return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils); }); -exports.StyleSheetsActor = StyleSheetsActor; -exports.StyleSheetsFront = StyleSheetsFront; +exports.StyleEditorActor = StyleEditorActor; +exports.StyleEditorFront = StyleEditorFront; -exports.StyleSheetActor = StyleSheetActor; -exports.StyleSheetFront = StyleSheetFront; +exports.OldStyleSheetActor = OldStyleSheetActor; +exports.OldStyleSheetFront = OldStyleSheetFront; /** diff --git a/toolkit/devtools/server/actors/styles.js b/toolkit/devtools/server/actors/styles.js index e5ebbd292e91..b9d33ca92b7b 100644 --- a/toolkit/devtools/server/actors/styles.js +++ b/toolkit/devtools/server/actors/styles.js @@ -11,7 +11,7 @@ const {Arg, Option, method, RetVal, types} = protocol; const events = require("sdk/event/core"); const object = require("sdk/util/object"); const { Class } = require("sdk/core/heritage"); -const { StyleSheetActor } = require("devtools/server/actors/styleeditor"); +const { StyleSheetActor } = require("devtools/server/actors/stylesheets"); loader.lazyImporter(this, "Services", "resource://gre/modules/Services.jsm"); loader.lazyGetter(this, "CssLogic", () => require("devtools/styleinspector/css-logic").CssLogic); diff --git a/toolkit/devtools/server/actors/stylesheets.js b/toolkit/devtools/server/actors/stylesheets.js new file mode 100644 index 000000000000..dff2afb60492 --- /dev/null +++ b/toolkit/devtools/server/actors/stylesheets.js @@ -0,0 +1,986 @@ +/* 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"; + +let { components, Cc, Ci, Cu } = require('chrome'); + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/NetUtil.jsm"); +Cu.import("resource://gre/modules/FileUtils.jsm"); +Cu.import("resource://gre/modules/devtools/SourceMap.jsm"); + +const promise = require("sdk/core/promise"); +const events = require("sdk/event/core"); +const protocol = require("devtools/server/protocol"); +const {Arg, Option, method, RetVal, types} = protocol; +const {LongStringActor, ShortLongString} = require("devtools/server/actors/string"); + +loader.lazyGetter(this, "CssLogic", () => require("devtools/styleinspector/css-logic").CssLogic); + +let TRANSITION_CLASS = "moz-styleeditor-transitioning"; +let TRANSITION_DURATION_MS = 500; +let TRANSITION_RULE = "\ +:root.moz-styleeditor-transitioning, :root.moz-styleeditor-transitioning * {\ +transition-duration: " + TRANSITION_DURATION_MS + "ms !important; \ +transition-delay: 0ms !important;\ +transition-timing-function: ease-out !important;\ +transition-property: all !important;\ +}"; + +let LOAD_ERROR = "error-load"; + +exports.register = function(handle) { + handle.addTabActor(StyleSheetsActor, "styleSheetsActor"); + handle.addGlobalActor(StyleSheetsActor, "styleSheetsActor"); +}; + +exports.unregister = function(handle) { + handle.removeTabActor(StyleSheetsActor); + handle.removeGlobalActor(StyleSheetsActor); +}; + +types.addActorType("stylesheet"); +types.addActorType("originalsource"); + +/** + * Creates a StyleSheetsActor. StyleSheetsActor provides remote access to the + * stylesheets of a document. + */ +let StyleSheetsActor = protocol.ActorClass({ + typeName: "stylesheets", + + /** + * The window we work with, taken from the parent actor. + */ + get window() this.parentActor.window, + + /** + * The current content document of the window we work with. + */ + get document() this.window.document, + + form: function() + { + return { actor: this.actorID }; + }, + + initialize: function (conn, tabActor) { + protocol.Actor.prototype.initialize.call(this, null); + + this.parentActor = tabActor; + + // keep a map of sheets-to-actors so we don't create two actors for one sheet + this._sheets = new Map(); + }, + + /** + * Destroy the current StyleSheetsActor instance. + */ + destroy: function() + { + this._sheets.clear(); + }, + + /** + * Protocol method for getting a list of StyleSheetActors representing + * all the style sheets in this document. + */ + getStyleSheets: method(function() { + let deferred = promise.defer(); + + let window = this.window; + var domReady = () => { + window.removeEventListener("DOMContentLoaded", domReady, true); + + let documents = [this.document]; + let actors = []; + for (let doc of documents) { + let sheets = this._addStyleSheets(doc.styleSheets); + actors = actors.concat(sheets); + // Recursively handle style sheets of the documents in iframes. + for (let iframe of doc.getElementsByTagName("iframe")) { + documents.push(iframe.contentDocument); + } + } + deferred.resolve(actors); + }; + + if (window.document.readyState === "loading") { + window.addEventListener("DOMContentLoaded", domReady, true); + } else { + domReady(); + } + + return deferred.promise; + }, { + request: {}, + response: { styleSheets: RetVal("array:stylesheet") } + }), + + /** + * Add all the stylesheets to the map and create an actor for each one + * if not already created. Send event that there are new stylesheets. + * + * @param {[DOMStyleSheet]} styleSheets + * Stylesheets to add + * @return {[object]} + * Array of actors for each StyleSheetActor created + */ + _addStyleSheets: function(styleSheets) + { + let sheets = []; + for (let i = 0; i < styleSheets.length; i++) { + let styleSheet = styleSheets[i]; + sheets.push(styleSheet); + + // Get all sheets, including imported ones + let imports = this._getImported(styleSheet); + sheets = sheets.concat(imports); + } + let actors = sheets.map(this._createStyleSheetActor.bind(this)); + + return actors; + }, + + /** + * Get all the stylesheets @imported from a stylesheet. + * + * @param {DOMStyleSheet} styleSheet + * Style sheet to search + * @return {array} + * All the imported stylesheets + */ + _getImported: function(styleSheet) { + let imported = []; + + for (let i = 0; i < styleSheet.cssRules.length; i++) { + let rule = styleSheet.cssRules[i]; + if (rule.type == Ci.nsIDOMCSSRule.IMPORT_RULE) { + // Associated styleSheet may be null if it has already been seen due to + // duplicate @imports for the same URL. + if (!rule.styleSheet) { + continue; + } + imported.push(rule.styleSheet); + + // recurse imports in this stylesheet as well + imported = imported.concat(this._getImported(rule.styleSheet)); + } + else if (rule.type != Ci.nsIDOMCSSRule.CHARSET_RULE) { + // @import rules must precede all others except @charset + break; + } + } + return imported; + }, + + /** + * Create a new actor for a style sheet, if it hasn't already been created. + * + * @param {DOMStyleSheet} styleSheet + * The style sheet to create an actor for. + * @return {StyleSheetActor} + * The actor for this style sheet + */ + _createStyleSheetActor: function(styleSheet) + { + if (this._sheets.has(styleSheet)) { + return this._sheets.get(styleSheet); + } + let actor = new StyleSheetActor(styleSheet, this); + + this.manage(actor); + this._sheets.set(styleSheet, actor); + + return actor; + }, + + /** + * Clear all the current stylesheet actors in map. + */ + _clearStyleSheetActors: function() { + for (let actor in this._sheets) { + this.unmanage(this._sheets[actor]); + } + this._sheets.clear(); + }, + + /** + * Create a new style sheet in the document with the given text. + * Return an actor for it. + * + * @param {object} request + * Debugging protocol request object, with 'text property' + * @return {object} + * Object with 'styelSheet' property for form on new actor. + */ + addStyleSheet: method(function(text) { + let parent = this.document.documentElement; + let style = this.document.createElementNS("http://www.w3.org/1999/xhtml", "style"); + style.setAttribute("type", "text/css"); + + if (text) { + style.appendChild(this.document.createTextNode(text)); + } + parent.appendChild(style); + + let actor = this._createStyleSheetActor(style.sheet); + return actor; + }, { + request: { text: Arg(0, "string") }, + response: { styleSheet: RetVal("stylesheet") } + }) +}); + +/** + * The corresponding Front object for the StyleSheetsActor. + */ +let StyleSheetsFront = protocol.FrontClass(StyleSheetsActor, { + initialize: function(client, tabForm) { + protocol.Front.prototype.initialize.call(this, client); + this.actorID = tabForm.styleSheetsActor; + + client.addActorPool(this); + this.manage(this); + } +}); + +/** + * A StyleSheetActor represents a stylesheet on the server. + */ +let StyleSheetActor = protocol.ActorClass({ + typeName: "stylesheet", + + events: { + "property-change" : { + type: "propertyChange", + property: Arg(0, "string"), + value: Arg(1, "json") + }, + "style-applied" : { + type: "styleApplied" + } + }, + + /* List of original sources that generated this stylesheet */ + _originalSources: null, + + toString: function() { + return "[StyleSheetActor " + this.actorID + "]"; + }, + + /** + * Window of target + */ + get window() this._window || this.parentActor.window, + + /** + * Document of target. + */ + get document() this.window.document, + + /** + * URL of underlying stylesheet. + */ + get href() this.rawSheet.href, + + /** + * Retrieve the index (order) of stylesheet in the document. + * + * @return number + */ + get styleSheetIndex() + { + if (this._styleSheetIndex == -1) { + for (let i = 0; i < this.document.styleSheets.length; i++) { + if (this.document.styleSheets[i] == this.rawSheet) { + this._styleSheetIndex = i; + break; + } + } + } + return this._styleSheetIndex; + }, + + initialize: function(aStyleSheet, aParentActor, aWindow) { + protocol.Actor.prototype.initialize.call(this, null); + + this.rawSheet = aStyleSheet; + this.parentActor = aParentActor; + this.conn = this.parentActor.conn; + + this._window = aWindow; + + // text and index are unknown until source load + this.text = null; + this._styleSheetIndex = -1; + + this._transitionRefCount = 0; + + // if this sheet has an @import, then it's rules are loaded async + let ownerNode = this.rawSheet.ownerNode; + if (ownerNode) { + let onSheetLoaded = function(event) { + ownerNode.removeEventListener("load", onSheetLoaded, false); + this._notifyPropertyChanged("ruleCount"); + }.bind(this); + + ownerNode.addEventListener("load", onSheetLoaded, false); + } + }, + + /** + * Get the current state of the actor + * + * @return {object} + * With properties of the underlying stylesheet, plus 'text', + * 'styleSheetIndex' and 'parentActor' if it's @imported + */ + form: function(detail) { + if (detail === "actorid") { + return this.actorID; + } + + let docHref; + if (this.rawSheet.ownerNode) { + if (this.rawSheet.ownerNode instanceof Ci.nsIDOMHTMLDocument) { + docHref = this.rawSheet.ownerNode.location.href; + } + if (this.rawSheet.ownerNode.ownerDocument) { + docHref = this.rawSheet.ownerNode.ownerDocument.location.href; + } + } + + let form = { + actor: this.actorID, // actorID is set when this actor is added to a pool + href: this.href, + nodeHref: docHref, + disabled: this.rawSheet.disabled, + title: this.rawSheet.title, + system: !CssLogic.isContentStylesheet(this.rawSheet), + styleSheetIndex: this.styleSheetIndex + } + + try { + form.ruleCount = this.rawSheet.cssRules.length; + } + catch(e) { + // stylesheet had an @import rule that wasn't loaded yet + } + return form; + }, + + /** + * Toggle the disabled property of the style sheet + * + * @return {object} + * 'disabled' - the disabled state after toggling. + */ + toggleDisabled: method(function() { + this.rawSheet.disabled = !this.rawSheet.disabled; + this._notifyPropertyChanged("disabled"); + + return this.rawSheet.disabled; + }, { + response: { disabled: RetVal("boolean")} + }), + + /** + * Send an event notifying that a property of the stylesheet + * has changed. + * + * @param {string} property + * Name of the changed property + */ + _notifyPropertyChanged: function(property) { + events.emit(this, "property-change", property, this.form()[property]); + }, + + /** + * Protocol method to get the text of this stylesheet. + */ + getText: method(function() { + return this._getText().then((text) => { + return new LongStringActor(this.conn, text || ""); + }); + }, { + response: { + text: RetVal("longstring") + } + }), + + /** + * Fetch the text for this stylesheet from the cache or network. Return + * cached text if it's already been fetched. + * + * @return {Promise} + * Promise that resolves with a string text of the stylesheet. + */ + _getText: function() { + if (this.text) { + return promise.resolve(this.text); + } + + if (!this.href) { + // this is an inline

@@ -52,5 +55,27 @@

+ +

+ + A link with no ID and an anchor, used by PredicateContext tests. + +

+ +

+ +

+ +

+ +

+ +

+ +

+ +

+

This content is editable.

+

diff --git a/addon-sdk/source/test/test-context-menu.js b/addon-sdk/source/test/test-context-menu.js index 80b6df4f0ff5..f2b1c3c0ee3b 100644 --- a/addon-sdk/source/test/test-context-menu.js +++ b/addon-sdk/source/test/test-context-menu.js @@ -3135,6 +3135,488 @@ exports.testSelectionInOuterFrameNoMatch = function (assert, done) { }); }; + +// Test that the return value of the predicate function determines if +// item is shown +exports.testPredicateContextControl = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let itemTrue = loader.cm.Item({ + label: "visible", + context: loader.cm.PredicateContext(function () { return true; }) + }); + + let itemFalse = loader.cm.Item({ + label: "hidden", + context: loader.cm.PredicateContext(function () { return false; }) + }); + + test.showMenu(null, function (popup) { + test.checkMenu([itemTrue, itemFalse], [itemFalse], []); + test.done(); + }); +}; + +// Test that the data object has the correct document type +exports.testPredicateContextDocumentType = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let items = [loader.cm.Item({ + label: "item", + context: loader.cm.PredicateContext(function (data) { + assert.equal(data.documentType, 'text/html'); + return true; + }) + })]; + + test.withTestDoc(function (window, doc) { + test.showMenu(null, function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); + }); +}; + +// Test that the data object has the correct document URL +exports.testPredicateContextDocumentURL = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let items = [loader.cm.Item({ + label: "item", + context: loader.cm.PredicateContext(function (data) { + assert.equal(data.documentURL, TEST_DOC_URL); + return true; + }) + })]; + + test.withTestDoc(function (window, doc) { + test.showMenu(null, function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); + }); +}; + + +// Test that the data object has the correct element name +exports.testPredicateContextTargetName = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let items = [loader.cm.Item({ + label: "item", + context: loader.cm.PredicateContext(function (data) { + assert.strictEqual(data.targetName, "input"); + return true; + }) + })]; + + test.withTestDoc(function (window, doc) { + test.showMenu(doc.getElementById("button"), function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); + }); +}; + + +// Test that the data object has the correct ID +exports.testPredicateContextTargetIDSet = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let items = [loader.cm.Item({ + label: "item", + context: loader.cm.PredicateContext(function (data) { + assert.strictEqual(data.targetID, "button"); + return true; + }) + })]; + + test.withTestDoc(function (window, doc) { + test.showMenu(doc.getElementById("button"), function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); + }); +}; + +// Test that the data object has the correct ID +exports.testPredicateContextTargetIDNotSet = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let items = [loader.cm.Item({ + label: "item", + context: loader.cm.PredicateContext(function (data) { + assert.strictEqual(data.targetID, null); + return true; + }) + })]; + + test.withTestDoc(function (window, doc) { + test.showMenu(doc.getElementsByClassName("predicate-test-a")[0], function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); + }); +}; + +// Test that the data object is showing editable correctly for regular text inputs +exports.testPredicateContextTextBoxIsEditable = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let items = [loader.cm.Item({ + label: "item", + context: loader.cm.PredicateContext(function (data) { + assert.strictEqual(data.isEditable, true); + return true; + }) + })]; + + test.withTestDoc(function (window, doc) { + test.showMenu(doc.getElementById("textbox"), function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); + }); +}; + +// Test that the data object is showing editable correctly for readonly text inputs +exports.testPredicateContextReadonlyTextBoxIsNotEditable = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let items = [loader.cm.Item({ + label: "item", + context: loader.cm.PredicateContext(function (data) { + assert.strictEqual(data.isEditable, false); + return true; + }) + })]; + + test.withTestDoc(function (window, doc) { + test.showMenu(doc.getElementById("readonly-textbox"), function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); + }); +}; + +// Test that the data object is showing editable correctly for disabled text inputs +exports.testPredicateContextDisabledTextBoxIsNotEditable = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let items = [loader.cm.Item({ + label: "item", + context: loader.cm.PredicateContext(function (data) { + assert.strictEqual(data.isEditable, false); + return true; + }) + })]; + + test.withTestDoc(function (window, doc) { + test.showMenu(doc.getElementById("disabled-textbox"), function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); + }); +}; + +// Test that the data object is showing editable correctly for text areas +exports.testPredicateContextTextAreaIsEditable = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let items = [loader.cm.Item({ + label: "item", + context: loader.cm.PredicateContext(function (data) { + assert.strictEqual(data.isEditable, true); + return true; + }) + })]; + + test.withTestDoc(function (window, doc) { + test.showMenu(doc.getElementById("textfield"), function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); + }); +}; + +// Test that non-text inputs are not considered editable +exports.testPredicateContextButtonIsNotEditable = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let items = [loader.cm.Item({ + label: "item", + context: loader.cm.PredicateContext(function (data) { + assert.strictEqual(data.isEditable, false); + return true; + }) + })]; + + test.withTestDoc(function (window, doc) { + test.showMenu(doc.getElementById("button"), function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); + }); +}; + + +// Test that the data object is showing editable correctly +exports.testPredicateContextNonInputIsNotEditable = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let items = [loader.cm.Item({ + label: "item", + context: loader.cm.PredicateContext(function (data) { + assert.strictEqual(data.isEditable, false); + return true; + }) + })]; + + test.withTestDoc(function (window, doc) { + test.showMenu(doc.getElementById("image"), function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); + }); +}; + + +// Test that the data object is showing editable correctly for HTML contenteditable elements +exports.testPredicateContextEditableElement = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let items = [loader.cm.Item({ + label: "item", + context: loader.cm.PredicateContext(function (data) { + assert.strictEqual(data.isEditable, true); + return true; + }) + })]; + + test.withTestDoc(function (window, doc) { + test.showMenu(doc.getElementById("editable"), function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); + }); +}; + + +// Test that the data object does not have a selection when there is none +exports.testPredicateContextNoSelectionInPage = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let items = [loader.cm.Item({ + label: "item", + context: loader.cm.PredicateContext(function (data) { + assert.strictEqual(data.selectionText, null); + return true; + }) + })]; + + test.withTestDoc(function (window, doc) { + test.showMenu(null, function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); + }); +}; + +// Test that the data object includes the selected page text +exports.testPredicateContextSelectionInPage = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let items = [loader.cm.Item({ + label: "item", + context: loader.cm.PredicateContext(function (data) { + // since we might get whitespace + assert.ok(data.selectionText && data.selectionText.search(/^\s*Some text.\s*$/) != -1, + 'Expected "Some text.", got "' + data.selectionText + '"'); + return true; + }) + })]; + + test.withTestDoc(function (window, doc) { + window.getSelection().selectAllChildren(doc.getElementById("text")); + test.showMenu(null, function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); + }); +}; + +// Test that the data object includes the selected input text +exports.testPredicateContextSelectionInTextBox = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let items = [loader.cm.Item({ + label: "item", + context: loader.cm.PredicateContext(function (data) { + // since we might get whitespace + assert.strictEqual(data.selectionText, "t v"); + return true; + }) + })]; + + test.withTestDoc(function (window, doc) { + let textbox = doc.getElementById("textbox"); + textbox.focus(); + textbox.setSelectionRange(3, 6); + test.showMenu(textbox, function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); + }); +}; + +// Test that the data object has the correct src for an image +exports.testPredicateContextTargetSrcSet = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + let image; + + let items = [loader.cm.Item({ + label: "item", + context: loader.cm.PredicateContext(function (data) { + assert.strictEqual(data.srcURL, image.src); + return true; + }) + })]; + + test.withTestDoc(function (window, doc) { + image = doc.getElementById("image"); + test.showMenu(image, function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); + }); +}; + +// Test that the data object has no src for a link +exports.testPredicateContextTargetSrcNotSet = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let items = [loader.cm.Item({ + label: "item", + context: loader.cm.PredicateContext(function (data) { + assert.strictEqual(data.srcURL, null); + return true; + }) + })]; + + test.withTestDoc(function (window, doc) { + test.showMenu(doc.getElementById("link"), function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); + }); +}; + + +// Test that the data object has the correct link set +exports.testPredicateContextTargetLinkSet = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + let image; + + let items = [loader.cm.Item({ + label: "item", + context: loader.cm.PredicateContext(function (data) { + assert.strictEqual(data.linkURL, TEST_DOC_URL + "#test"); + return true; + }) + })]; + + test.withTestDoc(function (window, doc) { + test.showMenu(doc.getElementsByClassName("predicate-test-a")[0], function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); + }); +}; + +// Test that the data object has no link for an image +exports.testPredicateContextTargetLinkNotSet = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let items = [loader.cm.Item({ + label: "item", + context: loader.cm.PredicateContext(function (data) { + assert.strictEqual(data.linkURL, null); + return true; + }) + })]; + + test.withTestDoc(function (window, doc) { + test.showMenu(doc.getElementById("image"), function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); + }); +}; + +// Test that the data object has the value for an input textbox +exports.testPredicateContextTargetValueSet = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + let image; + + let items = [loader.cm.Item({ + label: "item", + context: loader.cm.PredicateContext(function (data) { + assert.strictEqual(data.value, "test value"); + return true; + }) + })]; + + test.withTestDoc(function (window, doc) { + test.showMenu(doc.getElementById("textbox"), function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); + }); +}; + +// Test that the data object has no value for an image +exports.testPredicateContextTargetValueNotSet = function (assert, done) { + let test = new TestHelper(assert, done); + let loader = test.newLoader(); + + let items = [loader.cm.Item({ + label: "item", + context: loader.cm.PredicateContext(function (data) { + assert.strictEqual(data.value, null); + return true; + }) + })]; + + test.withTestDoc(function (window, doc) { + test.showMenu(doc.getElementById("image"), function (popup) { + test.checkMenu(items, [], []); + test.done(); + }); + }); +}; + + // NO TESTS BELOW THIS LINE! /////////////////////////////////////////////////// // This makes it easier to run tests by handling things like opening the menu, diff --git a/addon-sdk/source/test/test-sequence.js b/addon-sdk/source/test/test-sequence.js new file mode 100644 index 000000000000..ec450a5c6a93 --- /dev/null +++ b/addon-sdk/source/test/test-sequence.js @@ -0,0 +1,1163 @@ +/* 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"; + +let { seq, iterate, filter, map, reductions, reduce, count, + isEmpty, every, isEvery, some, take, takeWhile, drop, + dropWhile, concat, first, rest, nth, last, dropLast, + distinct, remove, mapcat, fromEnumerator, string, + object, pairs, keys, values, each + } = require("sdk/util/sequence"); + +const boom = () => { throw new Error("Boom!"); }; +const broken = seq(function*() { + yield 1; + throw new Error("Boom!"); +}); + +exports["test seq"] = assert => { + let xs = seq(function*() { + yield 1; + yield 2; + yield 3; + }); + + assert.deepEqual([...seq(null)], [], "seq of null is empty"); + assert.deepEqual([...seq(void(0))], [], "seq of void is empty"); + assert.deepEqual([...xs], [1, 2, 3], "seq of 1 2 3"); + assert.deepEqual([...seq(xs)], [1, 2, 3], "seq of seq is seq"); + + assert.deepEqual([...seq([])], [], "seq of emtpy array is empty"); + assert.deepEqual([...seq([1])], [1], "seq of lonly array is single element"); + assert.deepEqual([...seq([1, 2, 3])], [1, 2, 3], "seq of array is it's elements"); + + assert.deepEqual([...seq("")], [], "seq of emtpy string is empty"); + assert.deepEqual([...seq("o")], ["o"], "seq of char is single char seq"); + assert.deepEqual([...seq("hello")], ["h", "e", "l", "l", "o"], + "seq of string are chars"); + + assert.deepEqual([...seq(new Set())], [], "seq of emtpy set is empty"); + assert.deepEqual([...seq(new Set([1]))], [1], "seq of lonely set is single"); + assert.deepEqual([...seq(new Set([1, 2, 3]))], [1, 2, 3], "seq of lonely set is single"); + + assert.deepEqual([...seq(new Map())], [], "seq of emtpy map is empty"); + assert.deepEqual([...seq(new Map([[1, 2]]))], [[1, 2]], "seq single mapping is that mapping"); + assert.deepEqual([...seq(new Map([[1, 2], [3, 4], [5, 6]]))], + [[1, 2], [3, 4], [5, 6]], + "seq of map is key value mappings"); + + [function(){}, 1, /foo/, true].forEach(x => { + assert.throws(() => seq(x), "Type is not seq-able"); + }); + + assert.throws(() => [...broken], + /Boom/, + "broken sequence errors propagate"); +}; + +exports["test seq casting"] = assert => { + const xs = seq(function*() { yield 1; yield 2; yield 3; }); + const ys = seq(function*() { yield 1; }); + const zs = seq(function*() {}); + const kvs = seq(function*() { yield ["a", 1]; yield ["b", 2]; }); + const kv = seq(function*() { yield ["a", 1]; }); + + assert.deepEqual([...xs], [1, 2, 3], "cast to array"); + assert.deepEqual([...ys], [1], "cast to of one element"); + assert.deepEqual([...zs], [], "cast empty array"); + + assert.deepEqual(string(...xs), "123", "cast to string"); + assert.deepEqual(string(...ys), "1", "cast to char"); + assert.deepEqual(string(...zs), "", "cast to empty string"); + + assert.deepEqual(new Set(xs), new Set([1, 2, 3]), + "cast to set of items"); + assert.deepEqual(new Set(ys), new Set([1]), + "cast to set of one item"); + assert.deepEqual(new Set(zs), new Set(), + "cast to set of one item"); + + assert.deepEqual(new Map(kvs), new Map([["a", 1], ["b", 2]]), + "cast to map"); + assert.deepEqual(new Map(kv), new Map([["a", 1]]), + "cast to single mapping"); + assert.deepEqual(new Map(zs), new Map(), + "cast to empty map"); + + assert.deepEqual(object(...kvs), {a: 1, b: 2}, + "cast to object"); + assert.deepEqual(object(...kv), {a: 1}, + "cast to single pair"); + assert.deepEqual(object(...zs), {}, + "cast to empty object"); +}; + +exports["test pairs"] = assert => { + assert.deepEqual([...pairs(null)], [], "pairs on null is empty"); + assert.deepEqual([...pairs(void(0))], [], "pairs on void is empty"); + assert.deepEqual([...pairs({})], [], "empty sequence"); + assert.deepEqual([...pairs({a: 1})], [["a", 1]], "single pair"); + assert.deepEqual([...pairs({a: 1, b: 2, c: 3})].sort(), + [["a", 1], ["b", 2], ["c", 3]], + "creates pairs"); + let items = []; + for (let [key, value] of pairs({a: 1, b: 2, c: 3})) + items.push([key, value]); + + assert.deepEqual(items.sort(), + [["a", 1], ["b", 2], ["c", 3]], + "for of works on pairs"); + + + assert.deepEqual([...pairs([])], [], "pairs on empty array is empty"); + assert.deepEqual([...pairs([1])], [[0, 1]], "pairs on array is [index, element]"); + assert.deepEqual([...pairs([1, 2, 3])], + [[0, 1], [1, 2], [2, 3]], + "for arrays it pair of [index, element]"); + + assert.deepEqual([...pairs("")], [], "pairs on empty string is empty"); + assert.deepEqual([...pairs("a")], [[0, "a"]], "pairs on char is [0, char]"); + assert.deepEqual([...pairs("hello")], + [[0, "h"], [1, "e"], [2, "l"], [3, "l"], [4, "o"]], + "for strings it's pair of [index, char]"); + + assert.deepEqual([...pairs(new Map())], + [], + "pairs on empty map is empty"); + assert.deepEqual([...pairs(new Map([[1, 3]]))], + [[1, 3]], + "pairs on single mapping single mapping"); + assert.deepEqual([...pairs(new Map([[1, 2], [3, 4]]))], + [[1, 2], [3, 4]], + "pairs on map returs key vaule pairs"); + + assert.throws(() => pairs(new Set()), + "can't pair set"); + + assert.throws(() => pairs(4), + "can't pair number"); + + assert.throws(() => pairs(true), + "can't pair boolean"); +}; + +exports["test keys"] = assert => { + assert.deepEqual([...keys(null)], [], "keys on null is empty"); + assert.deepEqual([...keys(void(0))], [], "keys on void is empty"); + assert.deepEqual([...keys({})], [], "empty sequence"); + assert.deepEqual([...keys({a: 1})], ["a"], "single key"); + assert.deepEqual([...keys({a: 1, b: 2, c: 3})].sort(), + ["a", "b", "c"], + "all keys"); + + let items = []; + for (let key of keys({a: 1, b: 2, c: 3})) + items.push(key); + + assert.deepEqual(items.sort(), + ["a", "b", "c"], + "for of works on keys"); + + + assert.deepEqual([...keys([])], [], "keys on empty array is empty"); + assert.deepEqual([...keys([1])], [0], "keys on array is indexes"); + assert.deepEqual([...keys([1, 2, 3])], + [0, 1, 2], + "keys on arrays returns indexes"); + + assert.deepEqual([...keys("")], [], "keys on empty string is empty"); + assert.deepEqual([...keys("a")], [0], "keys on char is 0"); + assert.deepEqual([...keys("hello")], + [0, 1, 2, 3, 4], + "keys on strings is char indexes"); + + assert.deepEqual([...keys(new Map())], + [], + "keys on empty map is empty"); + assert.deepEqual([...keys(new Map([[1, 3]]))], + [1], + "keys on single mapping single mapping is single key"); + assert.deepEqual([...keys(new Map([[1, 2], [3, 4]]))], + [1, 3], + "keys on map is keys from map"); + + assert.throws(() => keys(new Set()), + "can't keys set"); + + assert.throws(() => keys(4), + "can't keys number"); + + assert.throws(() => keys(true), + "can't keys boolean"); +}; + +exports["test values"] = assert => { + assert.deepEqual([...values({})], [], "empty sequence"); + assert.deepEqual([...values({a: 1})], [1], "single value"); + assert.deepEqual([...values({a: 1, b: 2, c: 3})].sort(), + [1, 2, 3], + "all values"); + + let items = []; + for (let value of values({a: 1, b: 2, c: 3})) + items.push(value); + + assert.deepEqual(items.sort(), + [1, 2, 3], + "for of works on values"); + + assert.deepEqual([...values([])], [], "values on empty array is empty"); + assert.deepEqual([...values([1])], [1], "values on array elements"); + assert.deepEqual([...values([1, 2, 3])], + [1, 2, 3], + "values on arrays returns elements"); + + assert.deepEqual([...values("")], [], "values on empty string is empty"); + assert.deepEqual([...values("a")], ["a"], "values on char is char"); + assert.deepEqual([...values("hello")], + ["h", "e", "l", "l", "o"], + "values on strings is chars"); + + assert.deepEqual([...values(new Map())], + [], + "values on empty map is empty"); + assert.deepEqual([...values(new Map([[1, 3]]))], + [3], + "keys on single mapping single mapping is single key"); + assert.deepEqual([...values(new Map([[1, 2], [3, 4]]))], + [2, 4], + "values on map is values from map"); + + assert.deepEqual([...values(new Set())], [], "values on empty set is empty"); + assert.deepEqual([...values(new Set([1]))], [1], "values on set is it's items"); + assert.deepEqual([...values(new Set([1, 2, 3]))], + [1, 2, 3], + "values on set is it's items"); + + + assert.throws(() => values(4), + "can't values number"); + + assert.throws(() => values(true), + "can't values boolean"); +}; + +exports["test fromEnumerator"] = assert => { + const { Cc, Ci } = require("chrome"); + const { enumerateObservers, + addObserver, + removeObserver } = Cc["@mozilla.org/observer-service;1"]. + getService(Ci.nsIObserverService); + + + const topic = "sec:" + Math.random().toString(32).substr(2); + const [a, b, c] = [{wrappedJSObject: {}}, + {wrappedJSObject: {}}, + {wrappedJSObject: {}}]; + const unwrap = x => x.wrappedJSObject; + + [a, b, c].forEach(x => addObserver(x, topic, false)); + + const xs = fromEnumerator(() => enumerateObservers(topic)); + const ys = map(unwrap, xs); + + assert.deepEqual([...ys], [a, b, c].map(unwrap), + "all observers are there"); + + removeObserver(b, topic); + + assert.deepEqual([...ys], [a, c].map(unwrap), + "b was removed"); + + removeObserver(a, topic); + + assert.deepEqual([...ys], [c].map(unwrap), + "a was removed"); + + removeObserver(c, topic); + + assert.deepEqual([...ys], [], + "c was removed, now empty"); + + addObserver(a, topic, false); + + assert.deepEqual([...ys], [a].map(unwrap), + "a was added"); + + removeObserver(a, topic); + + assert.deepEqual([...ys], [].map(unwrap), + "a was removed, now empty"); + +}; + +exports["test filter"] = assert => { + const isOdd = x => x % 2; + const odds = seq(function*() { yield 1; yield 3; yield 5; }); + const evens = seq(function*() { yield 2; yield 4; yield 6; }); + const mixed = seq(function*() { + yield 1; + yield 2; + yield 3; + yield 4; + }); + + assert.deepEqual([...filter(isOdd, mixed)], [1, 3], + "filtered odds"); + assert.deepEqual([...filter(isOdd, odds)], [1, 3, 5], + "kept all"); + assert.deepEqual([...filter(isOdd, evens)], [], + "kept none"); + + + let xs = filter(boom, mixed); + assert.throws(() => [...xs], /Boom/, "errors propagate"); + + assert.throws(() => [...filter(isOdd, broken)], /Boom/, + "sequence errors propagate"); +}; + +exports["test filter array"] = assert => { + let isOdd = x => x % 2; + let xs = filter(isOdd, [1, 2, 3, 4]); + let ys = filter(isOdd, [1, 3, 5]); + let zs = filter(isOdd, [2, 4, 6]); + + assert.deepEqual([...xs], [1, 3], "filteres odds"); + assert.deepEqual([...ys], [1, 3, 5], "kept all"); + assert.deepEqual([...zs], [], "kept none"); + assert.ok(!Array.isArray(xs)); +}; + +exports["test filter set"] = assert => { + let isOdd = x => x % 2; + let xs = filter(isOdd, new Set([1, 2, 3, 4])); + let ys = filter(isOdd, new Set([1, 3, 5])); + let zs = filter(isOdd, new Set([2, 4, 6])); + + assert.deepEqual([...xs], [1, 3], "filteres odds"); + assert.deepEqual([...ys], [1, 3, 5], "kept all"); + assert.deepEqual([...zs], [], "kept none"); +}; + +exports["test filter string"] = assert => { + let isUpperCase = x => x.toUpperCase() === x; + let xs = filter(isUpperCase, "aBcDe"); + let ys = filter(isUpperCase, "ABC"); + let zs = filter(isUpperCase, "abcd"); + + assert.deepEqual([...xs], ["B", "D"], "filteres odds"); + assert.deepEqual([...ys], ["A", "B", "C"], "kept all"); + assert.deepEqual([...zs], [], "kept none"); +}; + +exports["test filter lazy"] = assert => { + const x = 1; + let y = 2; + + const xy = seq(function*() { yield x; yield y; }); + const isOdd = x => x % 2; + const actual = filter(isOdd, xy); + + assert.deepEqual([...actual], [1], "only one odd number"); + y = 3; + assert.deepEqual([...actual], [1, 3], "filter is lazy"); +}; + +exports["test filter non sequences"] = assert => { + const False = _ => false; + assert.throws(() => [...filter(False, 1)], + "can't iterate number"); + assert.throws(() => [...filter(False, {a: 1, b:2})], + "can't iterate object"); +}; + +exports["test map"] = assert => { + let inc = x => x + 1; + let xs = seq(function*() { yield 1; yield 2; yield 3; }); + let ys = map(inc, xs); + + assert.deepEqual([...ys], [2, 3, 4], "incremented each item"); + + assert.deepEqual([...map(inc, null)], [], "mapping null is empty"); + assert.deepEqual([...map(inc, void(0))], [], "mapping void is empty"); + assert.deepEqual([...map(inc, new Set([1, 2, 3]))], [2, 3, 4], "maps set items"); +}; + +exports["test map two inputs"] = assert => { + let sum = (x, y) => x + y; + let xs = seq(function*() { yield 1; yield 2; yield 3; }); + let ys = seq(function*() { yield 4; yield 5; yield 6; }); + + let zs = map(sum, xs, ys); + + assert.deepEqual([...zs], [5, 7, 9], "summed numbers"); +}; + +exports["test map diff sized inputs"] = assert => { + let sum = (x, y) => x + y; + let xs = seq(function*() { yield 1; yield 2; yield 3; }); + let ys = seq(function*() { yield 4; yield 5; yield 6; yield 7; yield 8; }); + + let zs = map(sum, xs, ys); + + assert.deepEqual([...zs], [5, 7, 9], "summed numbers"); + assert.deepEqual([...map(sum, ys, xs)], [5, 7, 9], + "index of exhasting input is irrelevant"); +}; + +exports["test map multi"] = assert => { + let sum = (x, y, z, w) => x + y + z + w; + let xs = seq(function*() { yield 1; yield 2; yield 3; yield 4; }); + let ys = seq(function*() { yield 4; yield 5; yield 6; yield 7; yield 8; }); + let zs = seq(function*() { yield 10; yield 11; yield 12; }); + let ws = seq(function*() { yield 0; yield 20; yield 40; yield 60; }); + + let actual = map(sum, xs, ys, zs, ws); + + assert.deepEqual([...actual], [15, 38, 61], "summed numbers"); +}; + +exports["test map errors"] = assert => { + assert.deepEqual([...map(boom, [])], [], + "won't throw if empty"); + + const xs = map(boom, [1, 2, 4]); + + assert.throws(() => [...xs], /Boom/, "propagates errors"); + + assert.throws(() => [...map(x => x, broken)], /Boom/, + "sequence errors propagate"); +}; + +exports["test reductions"] = assert => { + let sum = (...xs) => xs.reduce((x, y) => x + y, 0); + + assert.deepEqual([...reductions(sum, [1, 1, 1, 1])], + [1, 2, 3, 4], + "works with arrays"); + assert.deepEqual([...reductions(sum, 5, [1, 1, 1, 1])], + [5, 6, 7, 8, 9], + "array with initial"); + + assert.deepEqual([...reductions(sum, seq(function*() { + yield 1; + yield 2; + yield 3; + }))], + [1, 3, 6], + "works with sequences"); + + assert.deepEqual([...reductions(sum, 10, seq(function*() { + yield 1; + yield 2; + yield 3; + }))], + [10, 11, 13, 16], + "works with sequences"); + + assert.deepEqual([...reductions(sum, [])], [0], + "invokes accumulator with no args"); + + assert.throws(() => [...reductions(boom, 1, [1])], + /Boom/, + "arg errors errors propagate"); + assert.throws(() => [...reductions(sum, 1, broken)], + /Boom/, + "sequence errors propagate"); +}; + +exports["test reduce"] = assert => { + let sum = (...xs) => xs.reduce((x, y) => x + y, 0); + + assert.deepEqual(reduce(sum, [1, 2, 3, 4, 5]), + 15, + "works with arrays"); + + assert.deepEqual(reduce(sum, seq(function*() { + yield 1; + yield 2; + yield 3; + })), + 6, + "works with sequences"); + + assert.deepEqual(reduce(sum, 10, [1, 2, 3, 4, 5]), + 25, + "works with array & initial"); + + assert.deepEqual(reduce(sum, 5, seq(function*() { + yield 1; + yield 2; + yield 3; + })), + 11, + "works with sequences & initial"); + + assert.deepEqual(reduce(sum, []), 0, "reduce with no args"); + assert.deepEqual(reduce(sum, "a", []), "a", "reduce with initial"); + assert.deepEqual(reduce(sum, 1, [1]), 2, "reduce with single & initial"); + + assert.throws(() => [...reduce(boom, 1, [1])], + /Boom/, + "arg errors errors propagate"); + assert.throws(() => [...reduce(sum, 1, broken)], + /Boom/, + "sequence errors propagate"); +}; + +exports["test each"] = assert => { + const collect = xs => { + let result = []; + each((...etc) => result.push(...etc), xs); + return result; + }; + + assert.deepEqual(collect(null), [], "each ignores null"); + assert.deepEqual(collect(void(0)), [], "each ignores void"); + + assert.deepEqual(collect([]), [], "each ignores empty"); + assert.deepEqual(collect([1]), [1], "each works on single item arrays"); + assert.deepEqual(collect([1, 2, 3, 4, 5]), + [1, 2, 3, 4, 5], + "works with arrays"); + + assert.deepEqual(collect(seq(function*() { + yield 1; + yield 2; + yield 3; + })), + [1, 2, 3], + "works with sequences"); + + assert.deepEqual(collect(""), [], "ignores empty strings"); + assert.deepEqual(collect("a"), ["a"], "works on chars"); + assert.deepEqual(collect("hello"), ["h", "e", "l", "l", "o"], + "works on strings"); + + assert.deepEqual(collect(new Set()), [], "ignores empty sets"); + assert.deepEqual(collect(new Set(["a"])), ["a"], + "works on single item sets"); + assert.deepEqual(collect(new Set([1, 2, 3])), [1, 2, 3], + "works on muti item tests"); + + assert.deepEqual(collect(new Map()), [], "ignores empty maps"); + assert.deepEqual(collect(new Map([["a", 1]])), [["a", 1]], + "works on single mapping maps"); + assert.deepEqual(collect(new Map([[1, 2], [3, 4], [5, 6]])), + [[1, 2], [3, 4], [5, 6]], + "works on muti mapping maps"); + + assert.throws(() => collect({}), "objects arn't supported"); + assert.throws(() => collect(1), "numbers arn't supported"); + assert.throws(() => collect(true), "booleas arn't supported"); +}; + +exports["test count"] = assert => { + assert.equal(count(null), 0, "null counts to 0"); + assert.equal(count(), 0, "undefined counts to 0"); + assert.equal(count([]), 0, "empty array"); + assert.equal(count([1, 2, 3]), 3, "non-empty array"); + assert.equal(count(""), 0, "empty string"); + assert.equal(count("hello"), 5, "non-empty string"); + assert.equal(count(new Map()), 0, "empty map"); + assert.equal(count(new Map([[1, 2], [2, 3]])), 2, "non-empty map"); + assert.equal(count(new Set()), 0, "empty set"); + assert.equal(count(new Set([1, 2, 3, 4])), 4, "non-empty set"); + assert.equal(count(seq(function*() {})), 0, "empty sequence"); + assert.equal(count(seq(function*() { yield 1; yield 2; })), 2, + "non-empty sequence"); + + assert.throws(() => count(broken), + /Boom/, + "sequence errors propagate"); +}; + +exports["test isEmpty"] = assert => { + assert.equal(isEmpty(null), true, "null is empty"); + assert.equal(isEmpty(), true, "undefined is empty"); + assert.equal(isEmpty([]), true, "array is array"); + assert.equal(isEmpty([1, 2, 3]), false, "array isn't empty"); + assert.equal(isEmpty(""), true, "string is empty"); + assert.equal(isEmpty("hello"), false, "non-empty string"); + assert.equal(isEmpty(new Map()), true, "empty map"); + assert.equal(isEmpty(new Map([[1, 2], [2, 3]])), false, "non-empty map"); + assert.equal(isEmpty(new Set()), true, "empty set"); + assert.equal(isEmpty(new Set([1, 2, 3, 4])), false , "non-empty set"); + assert.equal(isEmpty(seq(function*() {})), true, "empty sequence"); + assert.equal(isEmpty(seq(function*() { yield 1; yield 2; })), false, + "non-empty sequence"); + + assert.equal(isEmpty(broken), false, "hasn't reached error"); +}; + +exports["test isEvery"] = assert => { + let isOdd = x => x % 2; + let isTrue = x => x === true; + let isFalse = x => x === false; + + assert.equal(isEvery(isOdd, seq(function*() { + yield 1; + yield 3; + yield 5; + })), true, "all are odds"); + + assert.equal(isEvery(isOdd, seq(function*() { + yield 1; + yield 2; + yield 3; + })), false, "contains even"); + + assert.equal(isEvery(isTrue, seq(function*() {})), true, "true if empty"); + assert.equal(isEvery(isFalse, seq(function*() {})), true, "true if empty"); + + assert.equal(isEvery(isTrue, null), true, "true for null"); + assert.equal(isEvery(isTrue, undefined), true, "true for undefined"); + + assert.throws(() => isEvery(boom, [1, 2]), + /Boom/, + "arg errors errors propagate"); + assert.throws(() => isEvery(x => true, broken), + /Boom/, + "sequence errors propagate"); + + assert.equal(isEvery(x => false, broken), false, + "hasn't reached error"); +}; + +exports["test some"] = assert => { + let isOdd = x => x % 2; + let isTrue = x => x === true; + let isFalse = x => x === false; + + assert.equal(some(isOdd, seq(function*() { + yield 2; + yield 4; + yield 6; + })), null, "all are even"); + + assert.equal(some(isOdd, seq(function*() { + yield 2; + yield 3; + yield 4; + })), true, "contains odd"); + + assert.equal(some(isTrue, seq(function*() {})), null, + "null if empty") + assert.equal(some(isFalse, seq(function*() {})), null, + "null if empty") + + assert.equal(some(isTrue, null), null, "null for null"); + assert.equal(some(isTrue, undefined), null, "null for undefined"); + + assert.throws(() => some(boom, [1, 2]), + /Boom/, + "arg errors errors propagate"); + assert.throws(() => some(x => false, broken), + /Boom/, + "sequence errors propagate"); + + assert.equal(some(x => true, broken), true, + "hasn't reached error"); +}; + +exports["test take"] = assert => { + let xs = seq(function*() { + yield 1; + yield 2; + yield 3; + yield 4; + yield 5; + yield 6; + }); + + assert.deepEqual([...take(3, xs)], [1, 2, 3], "took 3 items"); + assert.deepEqual([...take(3, [1, 2, 3, 4, 5])], [1, 2, 3], + "took 3 from array"); + + let ys = seq(function*() { yield 1; yield 2; }); + assert.deepEqual([...take(3, ys)], [1, 2], "takes at max n"); + assert.deepEqual([...take(3, [1, 2])], [1, 2], + "takes at max n from arary"); + + let empty = seq(function*() {}); + assert.deepEqual([...take(5, empty)], [], "nothing to take"); + + assert.throws(() => [...take(3, broken)], + /Boom/, + "sequence errors propagate"); + + assert.deepEqual([...take(1, broken)], [1], + "hasn't reached error"); +}; + +exports["test iterate"] = assert => { + let inc = x => x + 1; + let nums = iterate(inc, 0); + + assert.deepEqual([...take(5, nums)], [0, 1, 2, 3, 4], "took 5"); + assert.deepEqual([...take(10, nums)], [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], "took 10"); + + let xs = iterate(x => x * 3, 2); + assert.deepEqual([...take(4, xs)], [2, 6, 18, 54], "took 4"); + + assert.throws(() => [...iterate(boom, 0)], + /Boom/, + "function exceptions propagate"); +}; + +exports["test takeWhile"] = assert => { + let isNegative = x => x < 0; + let xs = seq(function*() { + yield -2; + yield -1; + yield 0; + yield 1; + yield 2; + yield 3; + }); + + assert.deepEqual([...takeWhile(isNegative, xs)], [-2, -1], + "took until 0"); + + let ys = seq(function*() {}); + assert.deepEqual([...takeWhile(isNegative, ys)], [], + "took none"); + + let zs = seq(function*() { + yield 0; + yield 1; + yield 2; + yield 3; + }); + + assert.deepEqual([...takeWhile(isNegative, zs)], [], + "took none"); + + assert.throws(() => [...takeWhile(boom, zs)], + /Boom/, + "function errors errors propagate"); + assert.throws(() => [...takeWhile(x => true, broken)], + /Boom/, + "sequence errors propagate"); + + assert.deepEqual([...takeWhile(x => false, broken)], + [], + "hasn't reached error"); +}; + +exports["test drop"] = assert => { + let testDrop = xs => { + assert.deepEqual([...drop(2, xs)], + [3, 4], + "dropped two elements"); + + assert.deepEqual([...drop(1, xs)], + [2, 3, 4], + "dropped one"); + + assert.deepEqual([...drop(0, xs)], + [1, 2, 3, 4], + "dropped 0"); + + assert.deepEqual([...drop(-2, xs)], + [1, 2, 3, 4], + "dropped 0 on negative `n`"); + + assert.deepEqual([...drop(5, xs)], + [], + "dropped all items"); + }; + + testDrop([1, 2, 3, 4]); + testDrop(seq(function*() { + yield 1; + yield 2; + yield 3; + yield 4; + })); + + assert.throws(() => [...drop(1, broken)], + /Boom/, + "sequence errors propagate"); +}; + + +exports["test dropWhile"] = assert => { + let isNegative = x => x < 0; + let True = _ => true; + let False = _ => false; + + let test = xs => { + assert.deepEqual([...dropWhile(isNegative, xs)], + [0, 1, 2], + "dropped negative"); + + assert.deepEqual([...dropWhile(True, xs)], + [], + "drop all"); + + assert.deepEqual([...dropWhile(False, xs)], + [-2, -1, 0, 1, 2], + "keep all"); + }; + + test([-2, -1, 0, 1, 2]); + test(seq(function*() { + yield -2; + yield -1; + yield 0; + yield 1; + yield 2; + })); + + assert.throws(() => [...dropWhile(boom, [1, 2, 3])], + /Boom/, + "function errors errors propagate"); + assert.throws(() => [...dropWhile(x => true, broken)], + /Boom/, + "sequence errors propagate"); +}; + + +exports["test concat"] = assert => { + let test = (a, b, c, d) => { + assert.deepEqual([...concat()], + [], + "nothing to concat"); + assert.deepEqual([...concat(a)], + [1, 2, 3], + "concat with nothing returns same as first"); + assert.deepEqual([...concat(a, b)], + [1, 2, 3, 4, 5], + "concat items from both"); + assert.deepEqual([...concat(a, b, a)], + [1, 2, 3, 4, 5, 1, 2, 3], + "concat itself"); + assert.deepEqual([...concat(c)], + [], + "concat of empty is empty"); + assert.deepEqual([...concat(a, c)], + [1, 2, 3], + "concat with empty"); + assert.deepEqual([...concat(c, c, c)], + [], + "concat of empties is empty"); + assert.deepEqual([...concat(c, b)], + [4, 5], + "empty can be in front"); + assert.deepEqual([...concat(d)], + [7], + "concat singular"); + assert.deepEqual([...concat(d, d)], + [7, 7], + "concat singulars"); + + assert.deepEqual([...concat(a, a, b, c, d, c, d, d)], + [1, 2, 3, 1, 2, 3, 4, 5, 7, 7, 7], + "many concats"); + + let ab = concat(a, b); + let abcd = concat(ab, concat(c, d)); + let cdabcd = concat(c, d, abcd); + + assert.deepEqual([...cdabcd], + [7, 1, 2, 3, 4, 5, 7], + "nested concats"); + }; + + test([1, 2, 3], + [4, 5], + [], + [7]); + + test(seq(function*() { yield 1; yield 2; yield 3; }), + seq(function*() { yield 4; yield 5; }), + seq(function*() { }), + seq(function*() { yield 7; })); + + assert.throws(() => [...concat(broken, [1, 2, 3])], + /Boom/, + "function errors errors propagate"); +}; + + +exports["test first"] = assert => { + let test = (xs, empty) => { + assert.equal(first(xs), 1, "returns first"); + assert.equal(first(empty), null, "returns null empty"); + }; + + test("1234", ""); + test([1, 2, 3], []); + test([1, 2, 3], null); + test([1, 2, 3], undefined); + test(seq(function*() { yield 1; yield 2; yield 3; }), + seq(function*() { })); + assert.equal(first(broken), 1, "did not reached error"); +}; + +exports["test rest"] = assert => { + let test = (xs, x, nil) => { + assert.deepEqual([...rest(xs)], ["b", "c"], + "rest items"); + assert.deepEqual([...rest(x)], [], + "empty when singular"); + assert.deepEqual([...rest(nil)], [], + "empty when empty"); + }; + + test("abc", "a", ""); + test(["a", "b", "c"], ["d"], []); + test(seq(function*() { yield "a"; yield "b"; yield "c"; }), + seq(function*() { yield "d"; }), + seq(function*() {})); + test(["a", "b", "c"], ["d"], null); + test(["a", "b", "c"], ["d"], undefined); + + assert.throws(() => [...rest(broken)], + /Boom/, + "sequence errors propagate"); +}; + + +exports["test nth"] = assert => { + let notFound = {}; + let test = xs => { + assert.equal(nth(xs, 0), "h", "first"); + assert.equal(nth(xs, 1), "e", "second"); + assert.equal(nth(xs, 5), void(0), "out of bound"); + assert.equal(nth(xs, 5, notFound), notFound, "out of bound"); + assert.equal(nth(xs, -1), void(0), "out of bound"); + assert.equal(nth(xs, -1, notFound), notFound, "out of bound"); + assert.equal(nth(xs, 4), "o", "5th"); + }; + + let testEmpty = xs => { + assert.equal(nth(xs, 0), void(0), "no first in empty"); + assert.equal(nth(xs, 5), void(0), "no 5th in empty"); + assert.equal(nth(xs, 0, notFound), notFound, "notFound on out of bound"); + }; + + test("hello"); + test(["h", "e", "l", "l", "o"]); + test(seq(function*() { + yield "h"; + yield "e"; + yield "l"; + yield "l"; + yield "o"; + })); + testEmpty(null); + testEmpty(undefined); + testEmpty([]); + testEmpty(""); + testEmpty(seq(function*() {})); + + + assert.throws(() => nth(broken, 1), + /Boom/, + "sequence errors propagate"); + assert.equal(nth(broken, 0), 1, "have not reached error"); +}; + + +exports["test last"] = assert => { + assert.equal(last(null), null, "no last in null"); + assert.equal(last(void(0)), null, "no last in undefined"); + assert.equal(last([]), null, "no last in []"); + assert.equal(last(""), null, "no last in ''"); + assert.equal(last(seq(function*() { })), null, "no last in empty"); + + assert.equal(last("hello"), "o", "last from string"); + assert.equal(last([1, 2, 3]), 3, "last from array"); + assert.equal(last([1]), 1, "last from singular"); + assert.equal(last(seq(function*() { + yield 1; + yield 2; + yield 3; + })), 3, "last from sequence"); + + assert.throws(() => last(broken), + /Boom/, + "sequence errors propagate"); +}; + + +exports["test dropLast"] = assert => { + let test = xs => { + assert.deepEqual([...dropLast(xs)], + [1, 2, 3, 4], + "dropped last"); + assert.deepEqual([...dropLast(0, xs)], + [1, 2, 3, 4, 5], + "dropped none on 0"); + assert.deepEqual([...dropLast(-3, xs)], + [1, 2, 3, 4, 5], + "drop none on negative"); + assert.deepEqual([...dropLast(3, xs)], + [1, 2], + "dropped given number"); + assert.deepEqual([...dropLast(5, xs)], + [], + "dropped all"); + }; + + let testEmpty = xs => { + assert.deepEqual([...dropLast(xs)], + [], + "nothing to drop"); + assert.deepEqual([...dropLast(0, xs)], + [], + "dropped none on 0"); + assert.deepEqual([...dropLast(-3, xs)], + [], + "drop none on negative"); + assert.deepEqual([...dropLast(3, xs)], + [], + "nothing to drop"); + }; + + test([1, 2, 3, 4, 5]); + test(seq(function*() { + yield 1; + yield 2; + yield 3; + yield 4; + yield 5; + })); + testEmpty([]); + testEmpty(""); + testEmpty(seq(function*() {})); + + assert.throws(() => [...dropLast(broken)], + /Boom/, + "sequence errors propagate"); +}; + + +exports["test distinct"] = assert => { + let test = (xs, message) => { + assert.deepEqual([...distinct(xs)], + [1, 2, 3, 4, 5], + message); + }; + + test([1, 2, 1, 3, 1, 4, 1, 5], "works with arrays"); + test(seq(function*() { + yield 1; + yield 2; + yield 1; + yield 3; + yield 1; + yield 4; + yield 1; + yield 5; + }), "works with sequences"); + test(new Set([1, 2, 1, 3, 1, 4, 1, 5]), + "works with sets"); + test(seq(function*() { + yield 1; + yield 2; + yield 2; + yield 2; + yield 1; + yield 3; + yield 1; + yield 4; + yield 4; + yield 4; + yield 1; + yield 5; + }), "works with multiple repeatitions"); + test([1, 2, 3, 4, 5], "work with distinct arrays"); + test(seq(function*() { + yield 1; + yield 2; + yield 3; + yield 4; + yield 5; + }), "works with distinct seqs"); +}; + + +exports["test remove"] = assert => { + let isPositive = x => x > 0; + let test = xs => { + assert.deepEqual([...remove(isPositive, xs)], + [-2, -1, 0], + "removed positives"); + }; + + test([1, -2, 2, -1, 3, 7, 0]); + test(seq(function*() { + yield 1; + yield -2; + yield 2; + yield -1; + yield 3; + yield 7; + yield 0; + })); + + assert.throws(() => [...distinct(broken)], + /Boom/, + "sequence errors propagate"); +}; + + +exports["test mapcat"] = assert => { + let upto = n => seq(function* () { + let index = 0; + while (index < n) { + yield index; + index = index + 1; + } + }); + + assert.deepEqual([...mapcat(upto, [1, 2, 3, 4])], + [0, 0, 1, 0, 1, 2, 0, 1, 2, 3], + "expands given sequence"); + + assert.deepEqual([...mapcat(upto, [0, 1, 2, 0])], + [0, 0, 1], + "expands given sequence"); + + assert.deepEqual([...mapcat(upto, [0, 0, 0])], + [], + "expands given sequence"); + + assert.deepEqual([...mapcat(upto, [])], + [], + "nothing to expand"); + + assert.deepEqual([...mapcat(upto, null)], + [], + "nothing to expand"); + + assert.deepEqual([...mapcat(upto, void(0))], + [], + "nothing to expand"); + + let xs = seq(function*() { + yield 0; + yield 1; + yield 0; + yield 2; + yield 0; + }); + + assert.deepEqual([...mapcat(upto, xs)], + [0, 0, 1], + "expands given sequence"); + + assert.throws(() => [...mapcat(boom, xs)], + /Boom/, + "function errors errors propagate"); + assert.throws(() => [...mapcat(upto, broken)], + /Boom/, + "sequence errors propagate"); +}; + +require("sdk/test").run(exports); diff --git a/addon-sdk/source/test/test-tab.js b/addon-sdk/source/test/test-tab.js index 0632c270793a..f20b09c70ab9 100644 --- a/addon-sdk/source/test/test-tab.js +++ b/addon-sdk/source/test/test-tab.js @@ -9,6 +9,7 @@ const { getTabForWindow } = require('sdk/tabs/helpers'); const app = require("sdk/system/xul-app"); const { viewFor } = require("sdk/view/core"); const { getTabId } = require("sdk/tabs/utils"); +const { defer } = require("sdk/lang/functional"); // The primary test tab var primaryTab; @@ -139,14 +140,16 @@ exports["test behavior on close"] = function(assert, done) { }; exports["test viewFor(tab)"] = (assert, done) => { - tabs.once("open", tab => { + // Note we defer handlers as length collection is updated after + // handler is invoked, so if test is finished before counnts are + // updated wrong length will show up in followup tests. + tabs.once("open", defer(tab => { const view = viewFor(tab); assert.ok(view, "view is returned"); assert.equal(getTabId(view), tab.id, "tab has a same id"); - tab.close(); - done(); - }); + tab.close(defer(done)); + })); tabs.open({ url: "about:mozilla" }); } diff --git a/addon-sdk/source/test/test-windows-common.js b/addon-sdk/source/test/test-windows-common.js index 0cc1f7d66c36..a461b903192f 100644 --- a/addon-sdk/source/test/test-windows-common.js +++ b/addon-sdk/source/test/test-windows-common.js @@ -8,6 +8,8 @@ const { browserWindows } = require('sdk/windows'); const { viewFor } = require('sdk/view/core'); const { Ci } = require("chrome"); const { isBrowser, getWindowTitle } = require("sdk/window/utils"); +const { defer } = require("sdk/lang/functional"); + // TEST: browserWindows Iterator exports.testBrowserWindowsIterator = function(assert) { @@ -30,6 +32,7 @@ exports.testBrowserWindowsIterator = function(assert) { } }; + exports.testWindowTabsObject_alt = function(assert, done) { let window = browserWindows.activeWindow; window.tabs.open({ @@ -58,6 +61,7 @@ exports.testWindowActivateMethod_simple = function(assert) { 'Active tab is active after window.activate() call'); }; + exports["test getView(window)"] = function(assert, done) { browserWindows.once("open", window => { const view = viewFor(window); @@ -68,9 +72,11 @@ exports["test getView(window)"] = function(assert, done) { "window has a right title"); window.close(); - window.destroy(); - assert.equal(viewFor(window), null, "window view is gone"); - done(); + // Defer handler cause window is destroyed after event is dispatched. + browserWindows.once("close", defer(_ => { + assert.equal(viewFor(window), null, "window view is gone"); + done(); + })); }); From bdc10edc8a2d48bba8a2ff011974309a4158bba4 Mon Sep 17 00:00:00 2001 From: Nicholas Nethercote Date: Thu, 19 Dec 2013 17:33:34 -0800 Subject: [PATCH 78/78] Bug 631842 (part 2b) - Fix a mis-handling of platform-specific suppression files in |mach valgrind-test|. r=me. --HG-- extra : rebase_source : 7a8a33d9db10ed47cf273358d99b6da874b48137 --- build/valgrind/mach_commands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/valgrind/mach_commands.py b/build/valgrind/mach_commands.py index 1f0cd026729a..75bfef4f6da1 100644 --- a/build/valgrind/mach_commands.py +++ b/build/valgrind/mach_commands.py @@ -56,7 +56,7 @@ class MachCommands(MachCommandBase): machtype = subprocess.check_output(['bash', '-c', 'echo $MACHTYPE']).rstrip() arch_specific_supps_file = os.path.join(supps_dir, machtype + '.sup') if os.path.isfile(arch_specific_supps_file): - debugger_args += ' --suppressions=' + os.path.join(supps_dir, arch_specific_supps_file) + debugger_args.append('--suppressions=' + os.path.join(supps_dir, arch_specific_supps_file)) print('Using platform-specific suppression file: ', arch_specific_supps_file + '\n') else: