From edc963261f4282f87e47f5e23da742b24291146b Mon Sep 17 00:00:00 2001 From: Patrick Brosset Date: Tue, 3 Jun 2014 09:51:55 +0200 Subject: [PATCH] Bug 1007021 - The ReflowActor observes iframes and works after page navigations; r=bgrins r=ochameau --- browser/devtools/layoutview/test/browser.ini | 5 + ...wser_layoutview_update-after-navigation.js | 99 +++++++++++ .../browser_layoutview_update-after-reload.js | 42 +++++ .../browser_layoutview_update-in-iframes.js | 61 +++++++ .../test/doc_layoutview_iframe1.html | 3 + .../test/doc_layoutview_iframe2.html | 3 + ...browser_styleeditor_private_perwindowpb.js | 54 +++--- toolkit/devtools/server/actors/layout.js | 74 ++++++-- toolkit/devtools/server/actors/root.js | 40 ++++- toolkit/devtools/server/actors/webbrowser.js | 168 +++++++++++++++--- .../unit/test_layout-reflows-observer.js | 16 +- 11 files changed, 492 insertions(+), 73 deletions(-) create mode 100644 browser/devtools/layoutview/test/browser_layoutview_update-after-navigation.js create mode 100644 browser/devtools/layoutview/test/browser_layoutview_update-after-reload.js create mode 100644 browser/devtools/layoutview/test/browser_layoutview_update-in-iframes.js create mode 100644 browser/devtools/layoutview/test/doc_layoutview_iframe1.html create mode 100644 browser/devtools/layoutview/test/doc_layoutview_iframe2.html diff --git a/browser/devtools/layoutview/test/browser.ini b/browser/devtools/layoutview/test/browser.ini index 954c4db5cf9f..d4857e9cfc81 100644 --- a/browser/devtools/layoutview/test/browser.ini +++ b/browser/devtools/layoutview/test/browser.ini @@ -2,10 +2,15 @@ skip-if = e10s # Bug ?????? - devtools tests disabled with e10s subsuite = devtools support-files = + doc_layoutview_iframe1.html + doc_layoutview_iframe2.html head.js [browser_layoutview.js] [browser_layoutview_rotate-labels-on-sides.js] +[browser_layoutview_update-after-navigation.js] +[browser_layoutview_update-after-reload.js] +[browser_layoutview_update-in-iframes.js] [browser_editablemodel.js] [browser_editablemodel_allproperties.js] [browser_editablemodel_border.js] diff --git a/browser/devtools/layoutview/test/browser_layoutview_update-after-navigation.js b/browser/devtools/layoutview/test/browser_layoutview_update-after-navigation.js new file mode 100644 index 000000000000..d6d7c0459ef7 --- /dev/null +++ b/browser/devtools/layoutview/test/browser_layoutview_update-after-navigation.js @@ -0,0 +1,99 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the layout-view continues to work after a page navigation and that +// it also works after going back + +let test = asyncTest(function*() { + yield addTab(TEST_URL_ROOT + "doc_layoutview_iframe1.html"); + let {toolbox, inspector, view} = yield openLayoutView(); + yield runTests(inspector, view); + yield destroyToolbox(inspector); +}); + +addTest("Test that the layout-view works on the first page", +function*(inspector, view) { + info("Selecting the test node"); + yield selectNode("p", inspector); + + info("Checking that the layout-view shows the right value"); + let paddingElt = view.doc.querySelector(".padding.top > span"); + is(paddingElt.textContent, "50"); + + info("Listening for layout-view changes and modifying the padding"); + let onUpdated = waitForUpdate(inspector); + getNode("p").style.padding = "20px"; + yield onUpdated; + ok(true, "Layout-view got updated"); + + info("Checking that the layout-view shows the right value after update"); + is(paddingElt.textContent, "20"); +}); + +addTest("Navigate to the second page", +function*(inspector, view) { + yield navigateTo(TEST_URL_ROOT + "doc_layoutview_iframe2.html"); + yield inspector.once("markuploaded"); +}); + +addTest("Test that the layout-view works on the second page", +function*(inspector, view) { + info("Selecting the test node"); + yield selectNode("p", inspector); + + info("Checking that the layout-view shows the right value"); + let sizeElt = view.doc.querySelector(".size > span"); + is(sizeElt.textContent, "100x100"); + + info("Listening for layout-view changes and modifying the size"); + let onUpdated = waitForUpdate(inspector); + getNode("p").style.width = "200px"; + yield onUpdated; + ok(true, "Layout-view got updated"); + + info("Checking that the layout-view shows the right value after update"); + is(sizeElt.textContent, "200x100"); +}); + +addTest("Go back to the first page", +function*(inspector, view) { + content.history.back(); + yield inspector.once("markuploaded"); +}); + +addTest("Test that the layout-view works on the first page after going back", +function*(inspector, view) { + info("Selecting the test node"); + yield selectNode("p", inspector); + + info("Checking that the layout-view shows the right value, which is the" + + "modified value from step one because of the bfcache"); + let paddingElt = view.doc.querySelector(".padding.top > span"); + is(paddingElt.textContent, "20"); + + info("Listening for layout-view changes and modifying the padding"); + let onUpdated = waitForUpdate(inspector); + getNode("p").style.padding = "100px"; + yield onUpdated; + ok(true, "Layout-view got updated"); + + info("Checking that the layout-view shows the right value after update"); + is(paddingElt.textContent, "100"); +}); + +function navigateTo(url) { + info("Navigating to " + url); + + let def = promise.defer(); + gBrowser.selectedBrowser.addEventListener("load", function onload() { + gBrowser.selectedBrowser.removeEventListener("load", onload, true); + info("URL " + url + " loading complete"); + waitForFocus(def.resolve, content); + }, true); + content.location = url; + + return def.promise; +} diff --git a/browser/devtools/layoutview/test/browser_layoutview_update-after-reload.js b/browser/devtools/layoutview/test/browser_layoutview_update-after-reload.js new file mode 100644 index 000000000000..22c7c5821b8c --- /dev/null +++ b/browser/devtools/layoutview/test/browser_layoutview_update-after-reload.js @@ -0,0 +1,42 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the layout-view continues to work after the page is reloaded + +let test = asyncTest(function*() { + yield addTab(TEST_URL_ROOT + "doc_layoutview_iframe1.html"); + let {toolbox, inspector, view} = yield openLayoutView(); + + info("Test that the layout-view works on the first page"); + yield assertLayoutView(inspector, view); + + info("Reload the page"); + content.location.reload(); + yield inspector.once("markuploaded"); + + info("Test that the layout-view works on the reloaded page"); + yield assertLayoutView(inspector, view); + + yield destroyToolbox(inspector); +}); + +function* assertLayoutView(inspector, view) { + info("Selecting the test node"); + yield selectNode("p", inspector); + + info("Checking that the layout-view shows the right value"); + let paddingElt = view.doc.querySelector(".padding.top > span"); + is(paddingElt.textContent, "50"); + + info("Listening for layout-view changes and modifying the padding"); + let onUpdated = waitForUpdate(inspector); + getNode("p").style.padding = "20px"; + yield onUpdated; + ok(true, "Layout-view got updated"); + + info("Checking that the layout-view shows the right value after update"); + is(paddingElt.textContent, "20"); +} diff --git a/browser/devtools/layoutview/test/browser_layoutview_update-in-iframes.js b/browser/devtools/layoutview/test/browser_layoutview_update-in-iframes.js new file mode 100644 index 000000000000..b90275375126 --- /dev/null +++ b/browser/devtools/layoutview/test/browser_layoutview_update-in-iframes.js @@ -0,0 +1,61 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that the layout-view for elements within iframes also updates when they +// change + +let test = asyncTest(function*() { + yield addTab(TEST_URL_ROOT + "doc_layoutview_iframe1.html"); + let iframe2 = getNode("iframe").contentDocument.querySelector("iframe"); + + let {toolbox, inspector, view} = yield openLayoutView(); + yield runTests(inspector, view, iframe2); + yield destroyToolbox(inspector); +}); + +addTest("Test that resizing an element in an iframe updates its box model", +function*(inspector, view, iframe2) { + info("Selecting the nested test node"); + let node = iframe2.contentDocument.querySelector("div"); + yield selectNode(node, inspector); + + info("Checking that the layout-view shows the right value"); + let sizeElt = view.doc.querySelector(".size > span"); + is(sizeElt.textContent, "400x200"); + + info("Listening for layout-view changes and modifying its size"); + let onUpdated = waitForUpdate(inspector); + node.style.width = "200px"; + yield onUpdated; + ok(true, "Layout-view got updated"); + + info("Checking that the layout-view shows the right value after update"); + is(sizeElt.textContent, "200x200"); +}); + +addTest("Test reflows are still sent to the layout-view after deleting an iframe", +function*(inspector, view, iframe2) { + info("Deleting the iframe2"); + iframe2.remove(); + yield inspector.once("inspector-updated"); + + info("Selecting the test node in iframe1"); + let node = getNode("iframe").contentDocument.querySelector("p"); + yield selectNode(node, inspector); + + info("Checking that the layout-view shows the right value"); + let sizeElt = view.doc.querySelector(".size > span"); + is(sizeElt.textContent, "100x100"); + + info("Listening for layout-view changes and modifying its size"); + let onUpdated = waitForUpdate(inspector); + node.style.width = "200px"; + yield onUpdated; + ok(true, "Layout-view got updated"); + + info("Checking that the layout-view shows the right value after update"); + is(sizeElt.textContent, "200x100"); +}); diff --git a/browser/devtools/layoutview/test/doc_layoutview_iframe1.html b/browser/devtools/layoutview/test/doc_layoutview_iframe1.html new file mode 100644 index 000000000000..5d1bbc3df873 --- /dev/null +++ b/browser/devtools/layoutview/test/doc_layoutview_iframe1.html @@ -0,0 +1,3 @@ + +

Root page

+ \ No newline at end of file diff --git a/browser/devtools/layoutview/test/doc_layoutview_iframe2.html b/browser/devtools/layoutview/test/doc_layoutview_iframe2.html new file mode 100644 index 000000000000..b651f6f1e231 --- /dev/null +++ b/browser/devtools/layoutview/test/doc_layoutview_iframe2.html @@ -0,0 +1,3 @@ + +

iframe 1

+ \ No newline at end of file diff --git a/browser/devtools/styleeditor/test/browser_styleeditor_private_perwindowpb.js b/browser/devtools/styleeditor/test/browser_styleeditor_private_perwindowpb.js index 3dbb4cd3c158..ce912e943b14 100644 --- a/browser/devtools/styleeditor/test/browser_styleeditor_private_perwindowpb.js +++ b/browser/devtools/styleeditor/test/browser_styleeditor_private_perwindowpb.js @@ -5,53 +5,47 @@ // This test makes sure that the style editor does not store any // content CSS files in the permanent cache when opened from PB mode. -let gUI; - function test() { waitForExplicitFinish(); - let windowsToClose = []; + let gUI; let testURI = 'http://' + TEST_HOST + '/browser/browser/devtools/styleeditor/test/test_private.html'; - function checkCache() { - checkDiskCacheFor(TEST_HOST, function() { - gUI = null; - finish(); - }); - } + info("Opening a new private window"); + let win = OpenBrowserWindow({private: true}); + win.addEventListener("load", function onLoad() { + win.removeEventListener("load", onLoad, false); + executeSoon(startTest); + }, false); - function doTest(aWindow) { - aWindow.gBrowser.selectedBrowser.addEventListener("load", function onLoad() { - aWindow.gBrowser.selectedBrowser.removeEventListener("load", onLoad, true); + function startTest() { + win.gBrowser.selectedBrowser.addEventListener("load", function onLoad() { + win.gBrowser.selectedBrowser.removeEventListener("load", onLoad, true); + + info("Clearing the browser cache"); cache.clear(); - openStyleEditorInWindow(aWindow, function(panel) { + + info("Opening the style editor in the private window"); + openStyleEditorInWindow(win, function(panel) { gUI = panel.UI; gUI.on("editor-added", onEditorAdded); }); }, true); - aWindow.gBrowser.selectedBrowser.loadURI(testURI); + info("Loading the test URL in the new private window"); + win.content.location = testURI; } function onEditorAdded(aEvent, aEditor) { + info("The style editor is ready") aEditor.getSourceEditor().then(checkCache); } - function testOnWindow(options, callback) { - let win = OpenBrowserWindow(options); - win.addEventListener("load", function onLoad() { - win.removeEventListener("load", onLoad, false); - windowsToClose.push(win); - executeSoon(function() callback(win)); - }, false); - }; - - registerCleanupFunction(function() { - windowsToClose.forEach(function(win) { + function checkCache() { + checkDiskCacheFor(TEST_HOST, function() { win.close(); + win = null; + gUI = null; + finish(); }); - }); - - testOnWindow({private: true}, function(win) { - doTest(win); - }); + } } diff --git a/toolkit/devtools/server/actors/layout.js b/toolkit/devtools/server/actors/layout.js index 29474f1508e1..ad03c8f91220 100644 --- a/toolkit/devtools/server/actors/layout.js +++ b/toolkit/devtools/server/actors/layout.js @@ -30,6 +30,7 @@ const protocol = require("devtools/server/protocol"); const {method, Arg, RetVal, types} = protocol; const events = require("sdk/event/core"); const Heritage = require("sdk/core/heritage"); +const {setTimeout, clearTimeout} = require("sdk/timers"); const EventEmitter = require("devtools/toolkit/event-emitter"); exports.register = function(handle) { @@ -148,7 +149,6 @@ exports.ReflowFront = protocol.FrontClass(ReflowActor, { */ function Observable(tabActor, callback) { this.tabActor = tabActor; - this.win = tabActor.window; this.callback = callback; } @@ -199,7 +199,6 @@ Observable.prototype = { destroy: function() { this.stop(); this.callback = null; - this.win = null; this.tabActor = null; } }; @@ -237,6 +236,8 @@ function LayoutChangesObserver(tabActor) { EventEmitter.decorate(this); } +exports.LayoutChangesObserver = LayoutChangesObserver; + LayoutChangesObserver.prototype = Heritage.extend(Observable.prototype, { /** * How long does this observer waits before emitting a batched reflows event. @@ -281,12 +282,20 @@ LayoutChangesObserver.prototype = Heritage.extend(Observable.prototype, { this.emit("reflows", this.reflows); this.reflows = []; } - this.eventLoopTimer = this.win.setTimeout(this._startEventLoop, + this.eventLoopTimer = this._setTimeout(this._startEventLoop, this.EVENT_BATCHING_DELAY); }, _stopEventLoop: function() { - this.win.clearTimeout(this.eventLoopTimer); + this._clearTimeout(this.eventLoopTimer); + }, + + // Exposing set/clearTimeout here to let tests override them if needed + _setTimeout: function(cb, ms) { + return setTimeout(cb, ms); + }, + _clearTimeout: function(t) { + return clearTimeout(t); }, /** @@ -362,21 +371,60 @@ exports.releaseLayoutChangesObserver = releaseLayoutChangesObserver; */ function ReflowObserver(tabActor, callback) { Observable.call(this, tabActor, callback); - this.docshell = this.win.QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIWebNavigation) - .QueryInterface(Ci.nsIDocShell); + + this._onWindowReady = this._onWindowReady.bind(this); + events.on(this.tabActor, "window-ready", this._onWindowReady); + this._onWindowDestroyed = this._onWindowDestroyed.bind(this); + events.on(this.tabActor, "window-destroyed", this._onWindowDestroyed); } ReflowObserver.prototype = Heritage.extend(Observable.prototype, { QueryInterface: XPCOMUtils.generateQI([Ci.nsIReflowObserver, Ci.nsISupportsWeakReference]), + _onWindowReady: function({window}) { + if (this.observing) { + this._startListeners([window]); + } + }, + + _onWindowDestroyed: function({window}) { + if (this.observing) { + this._stopListeners([window]); + } + }, + _start: function() { - this.docshell.addWeakReflowObserver(this); + this._startListeners(this.tabActor.windows); }, _stop: function() { - this.docshell.removeWeakReflowObserver(this); + if (this.tabActor.attached && this.tabActor.docShell) { + // It's only worth stopping if the tabActor is still attached + this._stopListeners(this.tabActor.windows); + } + }, + + _startListeners: function(windows) { + for (let window of windows) { + let docshell = window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShell); + docshell.addWeakReflowObserver(this); + } + }, + + _stopListeners: function(windows) { + for (let window of windows) { + // Corner cases where a global has already been freed may happen, in which + // case, no need to remove the observer + try { + let docshell = window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShell); + docshell.removeWeakReflowObserver(this); + } catch (e) {} + } }, reflow: function(start, end) { @@ -388,7 +436,13 @@ ReflowObserver.prototype = Heritage.extend(Observable.prototype, { }, destroy: function() { + if (this._isDestroyed) { + return; + } + this._isDestroyed = true; + + events.off(this.tabActor, "window-ready", this._onWindowReady); + events.off(this.tabActor, "window-destroyed", this._onWindowDestroyed); Observable.prototype.destroy.call(this); - this.docshell = null; } }); diff --git a/toolkit/devtools/server/actors/root.js b/toolkit/devtools/server/actors/root.js index 1b7a21908fbe..973b158769fc 100644 --- a/toolkit/devtools/server/actors/root.js +++ b/toolkit/devtools/server/actors/root.js @@ -141,18 +141,52 @@ RootActor.prototype = { */ get window() Services.wm.getMostRecentWindow(DebuggerServer.chromeWindowType), + /** + * The list of all windows + */ + get windows() { + return this.docShells.map(docShell => { + return docShell.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindow); + }); + }, + /** * URL of the chrome window. */ get url() { return this.window ? this.window.document.location.href : null; }, + /** + * The top level window's docshell + */ + get docShell() { + return this.window + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDocShell); + }, + + /** + * The list of all docshells + */ + get docShells() { + let docShellsEnum = this.docShell.getDocShellEnumerator( + Ci.nsIDocShellTreeItem.typeAll, + Ci.nsIDocShell.ENUMERATE_FORWARDS + ); + + let docShells = []; + while (docShellsEnum.hasMoreElements()) { + docShells.push(docShellsEnum.getNext()); + } + + return docShells; + }, + /** * Getter for the best nsIWebProgress for to watching this window. */ get webProgress() { - return this.window - .QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIDocShell) + return this.docShell .QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIWebProgress); }, diff --git a/toolkit/devtools/server/actors/webbrowser.js b/toolkit/devtools/server/actors/webbrowser.js index d6e556da7459..7d873854e3ae 100644 --- a/toolkit/devtools/server/actors/webbrowser.js +++ b/toolkit/devtools/server/actors/webbrowser.js @@ -13,7 +13,7 @@ let { RootActor } = require("devtools/server/actors/root"); let { AddonThreadActor, ThreadActor } = require("devtools/server/actors/script"); let { DebuggerServer } = require("devtools/server/main"); let DevToolsUtils = require("devtools/toolkit/DevToolsUtils"); -let { dbg_assert, dumpn } = DevToolsUtils; +let { dbg_assert } = DevToolsUtils; let {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {}); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); @@ -567,6 +567,24 @@ TabActor.prototype = { throw "The docShell getter should be implemented by a subclass of TabActor"; }, + /** + * Getter for the list of all docshell in this tabActor + * @return {Array} + */ + get docShells() { + let docShellsEnum = this.docShell.getDocShellEnumerator( + Ci.nsIDocShellTreeItem.typeAll, + Ci.nsIDocShell.ENUMERATE_FORWARDS + ); + + let docShells = []; + while (docShellsEnum.hasMoreElements()) { + docShells.push(docShellsEnum.getNext()); + } + + return docShells; + }, + /** * Getter for the tab content's DOM window. */ @@ -576,6 +594,17 @@ TabActor.prototype = { .getInterface(Ci.nsIDOMWindow); }, + /** + * Getter for the list of all content DOM windows in this tabActor + * @return {Array} + */ + get windows() { + return this.docShells.map(docShell => { + return docShell.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindow); + }); + }, + /** * Getter for the nsIWebProgress for watching this window. */ @@ -740,7 +769,7 @@ TabActor.prototype = { /** * Does the actual work of detaching from a tab. * - * @returns false if the tab wasn't attached or true of detahing succeeds. + * @returns false if the tab wasn't attached or true of detaching succeeds. */ _detach: function BTA_detach() { if (!this.attached) { @@ -752,6 +781,7 @@ TabActor.prototype = { if (this.docShell) { this._progressListener.unwatch(this.docShell); } + this._progressListener.destroy(); this._progressListener = null; this._popContext(); @@ -945,7 +975,6 @@ TabActor.prototype = { */ _windowReady: function (window) { let isTopLevel = window == this.window; - dumpn("window-ready: " + window.location + " isTopLevel:" + isTopLevel); events.emit(this, "window-ready", { window: window, @@ -970,6 +999,13 @@ TabActor.prototype = { } }, + _windowDestroyed: function (window) { + events.emit(this, "window-destroyed", { + window: window, + isTopLevel: window == this.window + }); + }, + /** * Start notifying server codebase and client about a new document * being loaded in the currently targeted context. @@ -989,7 +1025,6 @@ TabActor.prototype = { request: request }); - // We don't do anything for inner frames in TabActor. // (we will only update thread actor on window-ready) if (!isTopLevel) { @@ -1390,6 +1425,15 @@ BrowserAddonActor.prototype.requestTypes = { function DebuggerProgressListener(aTabActor) { this._tabActor = aTabActor; this._onWindowCreated = this.onWindowCreated.bind(this); + this._onWindowHidden = this.onWindowHidden.bind(this); + + // Watch for windows destroyed (global observer that will need filtering) + Services.obs.addObserver(this, "inner-window-destroyed", false); + + // XXX: for now we maintain the list of windows we know about in this instance + // so that we can discriminate windows we care about when observing + // inner-window-destroyed events. Bug 1016952 would remove the need for this. + this._knownWindowIDs = new Map(); } DebuggerProgressListener.prototype = { @@ -1399,7 +1443,13 @@ DebuggerProgressListener.prototype = { Ci.nsISupports, ]), - watch: function DPL_watch(docShell) { + destroy: function() { + Services.obs.removeObserver(this, "inner-window-destroyed", false); + this._knownWindowIDs.clear(); + this._knownWindowIDs = null; + }, + + watch: function(docShell) { let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIWebProgress); webProgress.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_STATUS | @@ -1407,52 +1457,116 @@ DebuggerProgressListener.prototype = { Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT); // TODO: fix docShell.chromeEventHandler in child processes! - let chromeEventHandler = docShell.chromeEventHandler || - docShell.QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIContentFrameMessageManager); + let handler = docShell.chromeEventHandler || + docShell.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIContentFrameMessageManager); - // Watch for globals being created in this docshell tree. - chromeEventHandler.addEventListener("DOMWindowCreated", - this._onWindowCreated, true); - chromeEventHandler.addEventListener("pageshow", - this._onWindowCreated, true); + handler.addEventListener("DOMWindowCreated", this._onWindowCreated, true); + handler.addEventListener("pageshow", this._onWindowCreated, true); + handler.addEventListener("pagehide", this._onWindowHidden, true); + + // Dispatch the _windowReady event on the tabActor for pre-existing windows + for (let win of this._getWindowsInDocShell(docShell)) { + this._tabActor._windowReady(win); + this._knownWindowIDs.set(this._getWindowID(win), win); + } }, - unwatch: function DPL_unwatch(docShell) { + unwatch: function(docShell) { let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIWebProgress); webProgress.removeProgressListener(this); // TODO: fix docShell.chromeEventHandler in child processes! - let chromeEventHandler = docShell.chromeEventHandler || - docShell.QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIContentFrameMessageManager); - chromeEventHandler.removeEventListener("DOMWindowCreated", - this._onWindowCreated, true); - chromeEventHandler.removeEventListener("pageshow", - this._onWindowCreated, true); + let handler = docShell.chromeEventHandler || + docShell.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIContentFrameMessageManager); + + handler.removeEventListener("DOMWindowCreated", this._onWindowCreated, true); + handler.removeEventListener("pageshow", this._onWindowCreated, true); + handler.removeEventListener("pagehide", this._onWindowHidden, true); + + for (let win of this._getWindowsInDocShell(docShell)) { + this._knownWindowIDs.delete(this._getWindowID(win)); + } }, - onWindowCreated: - DevToolsUtils.makeInfallible(function DPL_onWindowCreated(evt) { - // Ignore any event if the tab actor isn't attached. + _getWindowsInDocShell: function(docShell) { + let docShellsEnum = docShell.getDocShellEnumerator( + Ci.nsIDocShellTreeItem.typeAll, + Ci.nsIDocShell.ENUMERATE_FORWARDS + ); + + let windows = []; + while (docShellsEnum.hasMoreElements()) { + let w = docShellsEnum.getNext().QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindow); + windows.push(w); + } + return windows; + }, + + _getWindowID: function(window) { + return window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils) + .currentInnerWindowID; + }, + + onWindowCreated: DevToolsUtils.makeInfallible(function(evt) { if (!this._tabActor.attached) { return; } // pageshow events for non-persisted pages have already been handled by a - // prior DOMWindowCreated event. + // prior DOMWindowCreated event. For persisted pages, act as if the window + // had just been created since it's been unfrozen from bfcache. if (evt.type == "pageshow" && !evt.persisted) { return; } let window = evt.target.defaultView; this._tabActor._windowReady(window); + + if (evt.type !== "pageshow") { + this._knownWindowIDs.set(this._getWindowID(window), window); + } }, "DebuggerProgressListener.prototype.onWindowCreated"), + onWindowHidden: DevToolsUtils.makeInfallible(function(evt) { + if (!this._tabActor.attached) { + return; + } + + // Only act as if the window has been destroyed if the 'pagehide' event + // was sent for a persisted window (persisted is set when the page is put + // and frozen in the bfcache). If the page isn't persisted, the observer's + // inner-window-destroyed event will handle it. + if (!evt.persisted) { + return; + } + + let window = evt.target.defaultView; + this._tabActor._windowDestroyed(window); + }, "DebuggerProgressListener.prototype.onWindowHidden"), + + observe: DevToolsUtils.makeInfallible(function(subject, topic) { + if (!this._tabActor.attached) { + return; + } + + // Because this observer will be called for all inner-window-destroyed in + // the application, we need to filter out events for windows we are not + // watching + let innerID = subject.QueryInterface(Ci.nsISupportsPRUint64).data; + let window = this._knownWindowIDs.get(innerID); + if (window) { + this._knownWindowIDs.delete(innerID); + this._tabActor._windowDestroyed(window); + } + }, "DebuggerProgressListener.prototype.observe"), + onStateChange: - DevToolsUtils.makeInfallible(function DPL_onStateChange(aProgress, aRequest, aFlag, aStatus) { - // Ignore any event if the tab actor isn't attached. + DevToolsUtils.makeInfallible(function(aProgress, aRequest, aFlag, aStatus) { if (!this._tabActor.attached) { return; } diff --git a/toolkit/devtools/server/tests/unit/test_layout-reflows-observer.js b/toolkit/devtools/server/tests/unit/test_layout-reflows-observer.js index a8cd3f1e33aa..07d19646c6b6 100644 --- a/toolkit/devtools/server/tests/unit/test_layout-reflows-observer.js +++ b/toolkit/devtools/server/tests/unit/test_layout-reflows-observer.js @@ -5,13 +5,22 @@ let { getLayoutChangesObserver, - releaseLayoutChangesObserver + releaseLayoutChangesObserver, + LayoutChangesObserver } = devtools.require("devtools/server/actors/layout"); +// Override set/clearTimeout on LayoutChangesObserver to avoid depending on +// time in this unit test. This means that LayoutChangesObserver.eventLoopTimer +// will be the timeout callback instead of the timeout itself, so test cases +// will need to execute it to fake a timeout +LayoutChangesObserver.prototype._setTimeout = cb => cb; +LayoutChangesObserver.prototype._clearTimeout = function() {}; + // Mock the tabActor since we only really want to test the LayoutChangesObserver // and don't want to depend on a window object, nor want to test protocol.js function MockTabActor() { this.window = new MockWindow(); + this.windows = [this.window]; } function MockWindow() {} @@ -22,7 +31,9 @@ MockWindow.prototype = { getInterface: function() { return { QueryInterface: function() { - self.docShell = new MockDocShell(); + if (!self.docShell) { + self.docShell = new MockDocShell(); + } return self.docShell; } }; @@ -91,7 +102,6 @@ function eventsAreBatched() { // Note that in this test, we mock the TabActor and its window property, so we // also mock the setTimeout/clearTimeout mechanism and just call the callback // manually - let tabActor = new MockTabActor(); let observer = getLayoutChangesObserver(tabActor);