diff --git a/browser/devtools/markupview/markup-view.js b/browser/devtools/markupview/markup-view.js index 29b47fb15b6a..adaaacb9847a 100644 --- a/browser/devtools/markupview/markup-view.js +++ b/browser/devtools/markupview/markup-view.js @@ -2146,7 +2146,7 @@ MarkupElementContainer.prototype = Heritage.extend(MarkupContainer.prototype, { this.tooltipData.data = promise.resolve(res); }); }, () => { - this.tooltipData.data = promise.reject(); + this.tooltipData.data = promise.resolve({}); }); } }, @@ -2166,9 +2166,11 @@ MarkupElementContainer.prototype = Heritage.extend(MarkupContainer.prototype, { } return this.tooltipData.data.then(({data, size}) => { - tooltip.setImageContent(data, size); - }, () => { - tooltip.setBrokenImageContent(); + if (data && size) { + tooltip.setImageContent(data, size); + } else { + tooltip.setBrokenImageContent(); + } }); }, diff --git a/browser/devtools/markupview/test/browser.ini b/browser/devtools/markupview/test/browser.ini index 6c1ff3346dd2..e13446f7f45a 100644 --- a/browser/devtools/markupview/test/browser.ini +++ b/browser/devtools/markupview/test/browser.ini @@ -68,6 +68,7 @@ skip-if = e10s # Bug 1040751 - CodeMirror editor.destroy() isn't e10s compatible skip-if = e10s # Bug 1040751 - CodeMirror editor.destroy() isn't e10s compatible [browser_markupview_events_jquery_2.1.1.js] skip-if = e10s # Bug 1040751 - CodeMirror editor.destroy() isn't e10s compatible +[browser_markupview_load_01.js] [browser_markupview_html_edit_01.js] [browser_markupview_html_edit_02.js] [browser_markupview_html_edit_03.js] diff --git a/browser/devtools/markupview/test/browser_markupview_load_01.js b/browser/devtools/markupview/test/browser_markupview_load_01.js new file mode 100644 index 000000000000..f40ced9f587e --- /dev/null +++ b/browser/devtools/markupview/test/browser_markupview_load_01.js @@ -0,0 +1,70 @@ +/* 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"; + +// Tests that selecting an element with the 'Inspect Element' context +// menu during a page reload doesn't cause the markup view to become empty. +// See https://bugzilla.mozilla.org/show_bug.cgi?id=1036324 + +const server = createTestHTTPServer(); + +// Register a slow image handler so we can simulate a long time between +// a reload and the load event firing. +server.registerContentType("gif", "image/gif"); +server.registerPathHandler("/slow.gif", function (metadata, response) { + info ("Image has been requested"); + response.processAsync(); + setTimeout(() => { + info ("Image is responding"); + response.finish(); + }, 500); +}); + +// Test page load events. +const TEST_URL = "data:text/html," + + "" + + "" + + "" + + "

Slow script

" + + "" + + "" + + ""; + +add_task(function*() { + let tab = yield addTab(TEST_URL); + let {inspector} = yield openInspector(); + let domContentLoaded = waitForLinkedBrowserEvent(tab, "DOMContentLoaded"); + let pageLoaded = waitForLinkedBrowserEvent(tab, "load"); + + ok (inspector.markup, "There is a markup view"); + + // Select an element while the tab is in the middle of a slow reload. + reloadTab(); + yield domContentLoaded; + yield chooseWithInspectElementContextMenu("img"); + yield pageLoaded; + + yield inspector.once("markuploaded"); + ok (inspector.markup, "There is a markup view"); + is (inspector.markup._elt.children.length, 1, "The markup view is rendering"); +}); + +function* chooseWithInspectElementContextMenu(selector) { + yield executeInContent("Test:SynthesizeMouse", { + center: true, + selector: selector, + options: {type: "contextmenu", button: 2} + }); + executeInContent("Test:SynthesizeKey", {key: "Q", options: {}}); +} + +function waitForLinkedBrowserEvent(tab, event) { + let def = promise.defer(); + tab.linkedBrowser.addEventListener(event, function cb() { + tab.linkedBrowser.removeEventListener(event, cb, true); + def.resolve(); + }, true); + return def.promise; +} diff --git a/browser/devtools/markupview/test/head.js b/browser/devtools/markupview/test/head.js index a79c1e165e88..2ee7e4976aaf 100644 --- a/browser/devtools/markupview/test/head.js +++ b/browser/devtools/markupview/test/head.js @@ -172,6 +172,13 @@ function executeInContent(name, data={}, objects={}, expectResponse=true) { } } +/** + * Reload the current tab location. + */ +function reloadTab() { + return executeInContent("devtools:test:reload", {}, {}, false); +} + /** * Simple DOM node accesor function that takes either a node or a string css * selector as argument and returns the corresponding node @@ -647,3 +654,34 @@ function* waitForMultipleChildrenUpdates(inspector) { return yield waitForMultipleChildrenUpdates(inspector); } } + +/** + * Create an HTTP server that can be used to simulate custom requests within + * a test. It is automatically cleaned up when the test ends, so no need to + * call `destroy`. + * + * See https://developer.mozilla.org/en-US/docs/Httpd.js/HTTP_server_for_unit_tests + * for more information about how to register handlers. + * + * The server can be accessed like: + * + * const server = createTestHTTPServer(); + * let url = "http://localhost: " + server.identity.primaryPort + "/path"; + * + * @returns {HttpServer} + */ +function createTestHTTPServer() { + const {HttpServer} = Cu.import("resource://testing-common/httpd.js", {}); + let server = new HttpServer(); + + registerCleanupFunction(function* cleanup() { + let destroyed = promise.defer(); + server.stop(() => { + destroyed.resolve(); + }); + yield destroyed.promise; + }); + + server.start(-1); + return server; +} diff --git a/browser/devtools/shared/frame-script-utils.js b/browser/devtools/shared/frame-script-utils.js index 4c0f1d4850d4..aeee2a63c8c3 100644 --- a/browser/devtools/shared/frame-script-utils.js +++ b/browser/devtools/shared/frame-script-utils.js @@ -3,11 +3,14 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; -const Cu = Components.utils; - +const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; const { devtools } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}); devtools.lazyImporter(this, "promise", "resource://gre/modules/Promise.jsm", "Promise"); devtools.lazyImporter(this, "Task", "resource://gre/modules/Task.jsm", "Task"); +const loader = Cc["@mozilla.org/moz/jssubscript-loader;1"] + .getService(Ci.mozIJSSubScriptLoader); +let EventUtils = {}; +loader.loadSubScript("chrome://marionette/content/EventUtils.js", EventUtils); addMessageListener("devtools:test:history", function ({ data }) { content.history[data.direction](); @@ -189,6 +192,55 @@ addMessageListener("devtools:test:setAttribute", function(msg) { sendAsyncMessage("devtools:test:setAttribute"); }); +/** + * Synthesize a mouse event on an element. This handler doesn't send a message + * back. Consumers should listen to specific events on the inspector/highlighter + * to know when the event got synthesized. + * @param {Object} msg The msg.data part expects the following properties: + * - {Number} x + * - {Number} y + * - {Boolean} center If set to true, x/y will be ignored and + * synthesizeMouseAtCenter will be used instead + * - {Object} options Other event options + * - {String} selector An optional selector that will be used to find the node to + * synthesize the event on, if msg.objects doesn't contain the CPOW. + * The msg.objects part should be the element. + * @param {Object} data Event detail properties: + */ +addMessageListener("Test:SynthesizeMouse", function(msg) { + let {x, y, center, options, selector} = msg.data; + let {node} = msg.objects; + + if (!node && selector) { + node = superQuerySelector(selector); + } + + if (center) { + EventUtils.synthesizeMouseAtCenter(node, options, node.ownerDocument.defaultView); + } else { + EventUtils.synthesizeMouse(node, x, y, options, node.ownerDocument.defaultView); + } + + // Most consumers won't need to listen to this message, unless they want to + // wait for the mouse event to be synthesized and don't have another event + // to listen to instead. + sendAsyncMessage("Test:SynthesizeMouse"); +}); + +/** + * Synthesize a key event for an element. This handler doesn't send a message + * back. Consumers should listen to specific events on the inspector/highlighter + * to know when the event got synthesized. + * @param {Object} msg The msg.data part expects the following properties: + * - {String} key + * - {Object} options + */ +addMessageListener("Test:SynthesizeKey", function(msg) { + let {key, options} = msg.data; + + EventUtils.synthesizeKey(key, options, content); +}); + /** * Like document.querySelector but can go into iframes too. * ".container iframe || .sub-container div" will first try to find the node diff --git a/toolkit/devtools/server/actors/inspector.js b/toolkit/devtools/server/actors/inspector.js index 01ff79207f8d..29dd47f5ca4c 100644 --- a/toolkit/devtools/server/actors/inspector.js +++ b/toolkit/devtools/server/actors/inspector.js @@ -1410,6 +1410,8 @@ var WalkerActor = protocol.ActorClass({ * Named options, including: * `sameDocument`: If true, parents will be restricted to the same * document as the node. + * `sameTypeRootTreeItem`: If true, this will not traverse across + * different types of docshells. */ parents: method(function(node, options={}) { if (isNodeDead(node)) { @@ -1420,16 +1422,23 @@ var WalkerActor = protocol.ActorClass({ let parents = []; let cur; while((cur = walker.parentNode())) { - if (options.sameDocument && cur.ownerDocument != node.rawNode.ownerDocument) { + if (options.sameDocument && nodeDocument(cur) != nodeDocument(node.rawNode)) { break; } + + if (options.sameTypeRootTreeItem && + nodeDocshell(cur).sameTypeRootTreeItem != nodeDocshell(node.rawNode).sameTypeRootTreeItem) { + break; + } + parents.push(this._ref(cur)); } return parents; }, { request: { node: Arg(0, "domnode"), - sameDocument: Option(1) + sameDocument: Option(1), + sameTypeRootTreeItem: Option(1) }, response: { nodes: RetVal("array:domnode") @@ -3226,7 +3235,7 @@ var WalkerFront = exports.WalkerFront = protocol.FrontClass(WalkerActor, { let nodeType = types.getType("domnode"); let returnNode = nodeType.read(nodeType.write(nodeActor, walkerActor), this); let top = returnNode; - let extras = walkerActor.parents(nodeActor); + let extras = walkerActor.parents(nodeActor, {sameTypeRootTreeItem: true}); for (let extraActor of extras) { top = nodeType.read(nodeType.write(extraActor, walkerActor), this); } @@ -3519,6 +3528,16 @@ function nodeDocument(node) { return node.ownerDocument || (node.nodeType == Ci.nsIDOMNode.DOCUMENT_NODE ? node : null); } +function nodeDocshell(node) { + let doc = node ? nodeDocument(node) : null; + let win = doc ? doc.defaultView : null; + if (win) { + return win. + QueryInterface(Ci.nsIInterfaceRequestor). + getInterface(Ci.nsIDocShell); + } +} + function isNodeDead(node) { return !node || !node.rawNode || Cu.isDeadWrapper(node.rawNode); } diff --git a/toolkit/devtools/server/actors/styles.js b/toolkit/devtools/server/actors/styles.js index 5e4b28cc3666..39a6da7aa2fa 100644 --- a/toolkit/devtools/server/actors/styles.js +++ b/toolkit/devtools/server/actors/styles.js @@ -1020,7 +1020,8 @@ var StyleRuleActor = protocol.ActorClass({ // Elements don't have a parent stylesheet, and therefore // don't have an associated URI. Provide a URI for // those. - form.href = this.rawNode.ownerDocument.location.href; + let doc = this.rawNode.ownerDocument; + form.href = doc.location ? doc.location.href : ""; form.cssText = this.rawStyle.cssText || ""; break; case Ci.nsIDOMCSSRule.CHARSET_RULE: @@ -1231,7 +1232,7 @@ var StyleRuleFront = protocol.FrontClass(StyleRuleActor, { return this._form.href; } let sheet = this.parentStyleSheet; - return sheet.href; + return sheet ? sheet.href : ""; }, get nodeHref() {