diff --git a/browser/devtools/debugger/debugger-controller.js b/browser/devtools/debugger/debugger-controller.js index 99131a4d223..231ae1ac079 100644 --- a/browser/devtools/debugger/debugger-controller.js +++ b/browser/devtools/debugger/debugger-controller.js @@ -1719,7 +1719,10 @@ EventListeners.prototype = { // Add all the listeners in the debugger view event linsteners container. for (let listener of aResponse.listeners) { - let definitionSite = yield this._getDefinitionSite(listener.function); + let definitionSite; + if (listener.function.class == "Function") { + definitionSite = yield this._getDefinitionSite(listener.function); + } listener.function.url = definitionSite; DebuggerView.EventListeners.addListener(listener, { staged: true }); } @@ -1740,18 +1743,18 @@ EventListeners.prototype = { * @param object aFunction * The grip of the function to get the definition site for. * @return object - * A promise that is resolved with the function's owner source url, - * or rejected if an error occured. + * A promise that is resolved with the function's owner source url. */ _getDefinitionSite: function(aFunction) { let deferred = promise.defer(); gThreadClient.pauseGrip(aFunction).getDefinitionSite(aResponse => { if (aResponse.error) { - deferred.reject("Error getting function definition site: " + aResponse.message); - } else { - deferred.resolve(aResponse.url); + // Don't make this error fatal, because it would break the entire events pane. + const msg = "Error getting function definition site: " + aResponse.message; + DevToolsUtils.reportException("_getDefinitionSite", msg); } + deferred.resolve(aResponse.url); }); return deferred.promise; diff --git a/browser/devtools/debugger/test/browser.ini b/browser/devtools/debugger/test/browser.ini index efe6a841216..831b35b12a4 100644 --- a/browser/devtools/debugger/test/browser.ini +++ b/browser/devtools/debugger/test/browser.ini @@ -51,8 +51,9 @@ support-files = doc_editor-mode.html doc_empty-tab-01.html doc_empty-tab-02.html - doc_event-listeners.html + doc_event-listeners-01.html doc_event-listeners-02.html + doc_event-listeners-03.html doc_frame-parameters.html doc_function-display-name.html doc_function-search.html @@ -115,8 +116,9 @@ skip-if = os == 'win' # bug 1005274 [browser_dbg_break-on-dom-06.js] [browser_dbg_break-on-dom-07.js] [browser_dbg_break-on-dom-08.js] -[browser_dbg_break-on-dom-event.js] +[browser_dbg_break-on-dom-event-01.js] skip-if = os == "mac" || e10s # Bug 895426 +[browser_dbg_break-on-dom-event-02.js] [browser_dbg_breakpoints-actual-location.js] [browser_dbg_breakpoints-actual-location2.js] [browser_dbg_breakpoints-break-on-last-line-of-script-on-reload.js] @@ -151,7 +153,8 @@ skip-if = true # Bug 933950 (leaky test) [browser_dbg_debugger-statement.js] [browser_dbg_editor-contextmenu.js] [browser_dbg_editor-mode.js] -[browser_dbg_event-listeners.js] +[browser_dbg_event-listeners-01.js] +[browser_dbg_event-listeners-02.js] [browser_dbg_file-reload.js] [browser_dbg_function-display-name.js] [browser_dbg_global-method-override.js] diff --git a/browser/devtools/debugger/test/browser_dbg_break-on-dom-event.js b/browser/devtools/debugger/test/browser_dbg_break-on-dom-event-01.js similarity index 99% rename from browser/devtools/debugger/test/browser_dbg_break-on-dom-event.js rename to browser/devtools/debugger/test/browser_dbg_break-on-dom-event-01.js index f1dd15d8e71..4ed6b99886f 100644 --- a/browser/devtools/debugger/test/browser_dbg_break-on-dom-event.js +++ b/browser/devtools/debugger/test/browser_dbg_break-on-dom-event-01.js @@ -5,7 +5,7 @@ * Tests that the break-on-dom-events request works. */ -const TAB_URL = EXAMPLE_URL + "doc_event-listeners.html"; +const TAB_URL = EXAMPLE_URL + "doc_event-listeners-01.html"; let gClient, gThreadClient, gInput, gButton; diff --git a/browser/devtools/debugger/test/browser_dbg_break-on-dom-event-02.js b/browser/devtools/debugger/test/browser_dbg_break-on-dom-event-02.js new file mode 100644 index 00000000000..d8aca79d277 --- /dev/null +++ b/browser/devtools/debugger/test/browser_dbg_break-on-dom-event-02.js @@ -0,0 +1,110 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that the break-on-dom-events request works even for bound event + * listeners and handler objects with 'handleEvent' methods. + */ + +const TAB_URL = EXAMPLE_URL + "doc_event-listeners-03.html"; + +let gClient, gThreadClient; + +function test() { + if (!DebuggerServer.initialized) { + DebuggerServer.init(() => true); + DebuggerServer.addBrowserActors(); + } + + let transport = DebuggerServer.connectPipe(); + gClient = new DebuggerClient(transport); + gClient.connect((aType, aTraits) => { + is(aType, "browser", + "Root actor should identify itself as a browser."); + + addTab(TAB_URL) + .then(() => attachThreadActorForUrl(gClient, TAB_URL)) + .then(aThreadClient => gThreadClient = aThreadClient) + .then(pauseDebuggee) + .then(testBreakOnClick) + .then(closeConnection) + .then(finish) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + }); +} + +function pauseDebuggee() { + let deferred = promise.defer(); + + gClient.addOneTimeListener("paused", (aEvent, aPacket) => { + is(aPacket.type, "paused", + "We should now be paused."); + is(aPacket.why.type, "debuggerStatement", + "The debugger statement was hit."); + + gThreadClient.resume(deferred.resolve); + }); + + // Spin the event loop before causing the debuggee to pause, to allow + // this function to return first. + executeSoon(() => triggerButtonClick("initialSetup")); + + return deferred.promise; +} + +// Test pause on a single event. +function testBreakOnClick() { + let deferred = promise.defer(); + + // Test calling pauseOnDOMEvents from a running state. + gThreadClient.pauseOnDOMEvents(["click"], (aPacket) => { + is(aPacket.error, undefined, + "The pause-on-click request completed successfully."); + let handlers = ["clicker"]; + + gClient.addListener("paused", function tester(aEvent, aPacket) { + is(aPacket.why.type, "pauseOnDOMEvents", + "A hidden breakpoint was hit."); + + switch(handlers.length) { + case 1: + is(aPacket.frame.where.line, 26, "Found the clicker handler."); + handlers.push("handleEventClick"); + break; + case 2: + is(aPacket.frame.where.line, 36, "Found the handleEventClick handler."); + handlers.push("boundHandleEventClick"); + break; + case 3: + is(aPacket.frame.where.line, 46, "Found the boundHandleEventClick handler."); + gClient.removeListener("paused", tester); + deferred.resolve(); + } + + gThreadClient.resume(() => triggerButtonClick(handlers.slice(-1))); + }); + + triggerButtonClick(handlers.slice(-1)); + }); + + return deferred.promise; +} + +function triggerButtonClick(aNodeId) { + let button = content.document.getElementById(aNodeId); + EventUtils.sendMouseEvent({ type: "click" }, button); +} + +function closeConnection() { + let deferred = promise.defer(); + gClient.close(deferred.resolve); + return deferred.promise; +} + +registerCleanupFunction(function() { + removeTab(gBrowser.selectedTab); + gClient = null; + gThreadClient = null; +}); diff --git a/browser/devtools/debugger/test/browser_dbg_event-listeners.js b/browser/devtools/debugger/test/browser_dbg_event-listeners-01.js similarity index 97% rename from browser/devtools/debugger/test/browser_dbg_event-listeners.js rename to browser/devtools/debugger/test/browser_dbg_event-listeners-01.js index 83c2f2fd317..b07aec4619c 100644 --- a/browser/devtools/debugger/test/browser_dbg_event-listeners.js +++ b/browser/devtools/debugger/test/browser_dbg_event-listeners-01.js @@ -5,7 +5,7 @@ * Tests that the eventListeners request works. */ -const TAB_URL = EXAMPLE_URL + "doc_event-listeners.html"; +const TAB_URL = EXAMPLE_URL + "doc_event-listeners-01.html"; let gClient; @@ -87,6 +87,7 @@ function testEventListeners(aThreadClient) { let types = []; for (let l of listeners) { + info("Listener for the "+l.type+" event."); let node = l.node; ok(node, "There is a node property."); ok(node.object, "There is a node object property."); diff --git a/browser/devtools/debugger/test/browser_dbg_event-listeners-02.js b/browser/devtools/debugger/test/browser_dbg_event-listeners-02.js new file mode 100644 index 00000000000..ca0c5efa824 --- /dev/null +++ b/browser/devtools/debugger/test/browser_dbg_event-listeners-02.js @@ -0,0 +1,130 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that the eventListeners request works when bound functions are used as + * event listeners. + */ + +const TAB_URL = EXAMPLE_URL + "doc_event-listeners-03.html"; + +let gClient; + +function test() { + if (!DebuggerServer.initialized) { + DebuggerServer.init(() => true); + DebuggerServer.addBrowserActors(); + } + + let transport = DebuggerServer.connectPipe(); + gClient = new DebuggerClient(transport); + gClient.connect((aType, aTraits) => { + is(aType, "browser", + "Root actor should identify itself as a browser."); + + addTab(TAB_URL) + .then(() => attachThreadActorForUrl(gClient, TAB_URL)) + .then(pauseDebuggee) + .then(testEventListeners) + .then(closeConnection) + .then(finish) + .then(null, aError => { + ok(false, "Got an error: " + aError.message + "\n" + aError.stack); + }); + }); +} + +function pauseDebuggee(aThreadClient) { + let deferred = promise.defer(); + + gClient.addOneTimeListener("paused", (aEvent, aPacket) => { + is(aPacket.type, "paused", + "We should now be paused."); + is(aPacket.why.type, "debuggerStatement", + "The debugger statement was hit."); + + deferred.resolve(aThreadClient); + }); + + // Spin the event loop before causing the debuggee to pause, to allow + // this function to return first. + executeSoon(() => { + EventUtils.sendMouseEvent({ type: "click" }, + content.document.querySelector("button"), + content); + }); + + return deferred.promise; +} + +function testEventListeners(aThreadClient) { + let deferred = promise.defer(); + + aThreadClient.eventListeners(aPacket => { + if (aPacket.error) { + let msg = "Error getting event listeners: " + aPacket.message; + ok(false, msg); + deferred.reject(msg); + return; + } + + is(aPacket.listeners.length, 3, + "Found all event listeners."); + + promise.all(aPacket.listeners.map(listener => { + const lDeferred = promise.defer(); + aThreadClient.pauseGrip(listener.function).getDefinitionSite(aResponse => { + if (aResponse.error) { + const msg = "Error getting function definition site: " + aResponse.message; + ok(false, msg); + lDeferred.reject(msg); + return; + } + listener.function.url = aResponse.url; + lDeferred.resolve(listener); + }); + return lDeferred.promise; + })).then(listeners => { + is (listeners.length, 3, "Found three event listeners."); + for (let l of listeners) { + let node = l.node; + ok(node, "There is a node property."); + ok(node.object, "There is a node object property."); + ok(node.selector == "window" || + content.document.querySelectorAll(node.selector).length == 1, + "The node property is a unique CSS selector."); + + let func = l.function; + ok(func, "There is a function property."); + is(func.type, "object", "The function form is of type 'object'."); + is(func.class, "Function", "The function form is of class 'Function'."); + is(func.url, TAB_URL, "The function url is correct."); + + is(l.type, "click", "This is a click event listener."); + is(l.allowsUntrusted, true, + "'allowsUntrusted' property has the right value."); + is(l.inSystemEventGroup, false, + "'inSystemEventGroup' property has the right value."); + is(l.isEventHandler, false, + "'isEventHandler' property has the right value."); + is(l.capturing, false, + "Capturing property has the right value."); + } + + aThreadClient.resume(deferred.resolve); + }); + }); + + return deferred.promise; +} + +function closeConnection() { + let deferred = promise.defer(); + gClient.close(deferred.resolve); + return deferred.promise; +} + +registerCleanupFunction(function() { + removeTab(gBrowser.selectedTab); + gClient = null; +}); diff --git a/browser/devtools/debugger/test/doc_event-listeners.html b/browser/devtools/debugger/test/doc_event-listeners-01.html similarity index 100% rename from browser/devtools/debugger/test/doc_event-listeners.html rename to browser/devtools/debugger/test/doc_event-listeners-01.html diff --git a/browser/devtools/debugger/test/doc_event-listeners-03.html b/browser/devtools/debugger/test/doc_event-listeners-03.html new file mode 100644 index 00000000000..b672a4360f8 --- /dev/null +++ b/browser/devtools/debugger/test/doc_event-listeners-03.html @@ -0,0 +1,63 @@ + + + + + + Bound event listeners test page + + + + + + + + + + + + diff --git a/toolkit/devtools/server/actors/script.js b/toolkit/devtools/server/actors/script.js index c416fbad5b6..cf34672a5ea 100644 --- a/toolkit/devtools/server/actors/script.js +++ b/toolkit/devtools/server/actors/script.js @@ -1257,7 +1257,29 @@ ThreadActor.prototype = { let l = Object.create(null); l.type = handler.type; let listener = handler.listenerObject; - l.script = this.globalDebugObject.makeDebuggeeValue(listener).script; + let listenerDO = this.globalDebugObject.makeDebuggeeValue(listener); + // If the listener is an object with a 'handleEvent' method, use that. + if (listenerDO.class == "Object" || listenerDO.class == "XULElement") { + // For some events we don't have permission to access the + // 'handleEvent' property when running in content scope. + if (!listenerDO.unwrap()) { + continue; + } + let heDesc; + while (!heDesc && listenerDO) { + heDesc = listenerDO.getOwnPropertyDescriptor("handleEvent"); + listenerDO = listenerDO.proto; + } + if (heDesc && heDesc.value) { + listenerDO = heDesc.value; + } + } + // When the listener is a bound function, we are actually interested in + // the target function. + while (listenerDO.isBoundFunction) { + listenerDO = listenerDO.boundTargetFunction; + } + l.script = listenerDO.script; // Chrome listeners won't be converted to debuggee values, since their // compartment is not added as a debuggee. if (!l.script) @@ -1827,9 +1849,32 @@ ThreadActor.prototype = { listenerForm.capturing = handler.capturing; listenerForm.allowsUntrusted = handler.allowsUntrusted; listenerForm.inSystemEventGroup = handler.inSystemEventGroup; - listenerForm.isEventHandler = !!node["on" + listenerForm.type]; + let handlerName = "on" + listenerForm.type; + listenerForm.isEventHandler = false; + if (typeof node.hasAttribute !== "undefined") { + listenerForm.isEventHandler = !!node.hasAttribute(handlerName); + } + if (!!node[handlerName]) { + listenerForm.isEventHandler = !!node[handlerName]; + } // Get the Debugger.Object for the listener object. let listenerDO = this.globalDebugObject.makeDebuggeeValue(listener); + // If the listener is an object with a 'handleEvent' method, use that. + if (listenerDO.class == "Object" || listenerDO.class == "XULElement") { + let heDesc; + while (!heDesc && listenerDO) { + heDesc = listenerDO.getOwnPropertyDescriptor("handleEvent"); + listenerDO = listenerDO.proto; + } + if (heDesc && heDesc.value) { + listenerDO = heDesc.value; + } + } + // When the listener is a bound function, we are actually interested in + // the target function. + while (listenerDO.isBoundFunction) { + listenerDO = listenerDO.boundTargetFunction; + } listenerForm.function = this.createValueGrip(listenerDO); listeners.push(listenerForm); }