Properly handle bound functions in event listeners and objects with 'handleEvent' methods (bug 746622). r=vporof,mratcliffe

--HG--
rename : browser/devtools/debugger/test/browser_dbg_break-on-dom-event.js => browser/devtools/debugger/test/browser_dbg_break-on-dom-event-01.js
rename : browser/devtools/debugger/test/browser_dbg_break-on-dom-event.js => browser/devtools/debugger/test/browser_dbg_break-on-dom-event-02.js
rename : browser/devtools/debugger/test/browser_dbg_event-listeners.js => browser/devtools/debugger/test/browser_dbg_event-listeners-01.js
rename : browser/devtools/debugger/test/doc_event-listeners.html => browser/devtools/debugger/test/doc_event-listeners-01.html
rename : browser/devtools/debugger/test/doc_event-listeners.html => browser/devtools/debugger/test/doc_event-listeners-03.html
extra : rebase_source : 5ff8e76fd2a8518ea007c7594dcc544cb0cc0808
This commit is contained in:
Panos Astithas 2014-06-12 17:18:09 +03:00
parent 30679ed261
commit 5bd95bcead
9 changed files with 368 additions and 13 deletions

View File

@ -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;

View File

@ -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]

View File

@ -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;

View File

@ -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;
});

View File

@ -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.");

View File

@ -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;
});

View File

@ -0,0 +1,63 @@
<!-- Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ -->
<!doctype html>
<html>
<head>
<meta charset="utf-8"/>
<title>Bound event listeners test page</title>
</head>
<body>
<button id="initialSetup">initialSetup</button>
<button id="clicker">clicker</button>
<button id="handleEventClick">handleEventClick</button>
<button id="boundHandleEventClick">boundHandleEventClick</button>
<script type="text/javascript">
window.addEventListener("load", function onload() {
window.removeEventListener("load", onload);
function initialSetup(event) {
var button = document.getElementById("initialSetup");
button.removeEventListener("click", initialSetup);
debugger;
}
function clicker(event) {
window.foobar = "clicker";
}
function handleEventClick() {
var button = document.getElementById("handleEventClick");
// Create a long prototype chain to test for weird edge cases.
button.addEventListener("click", Object.create(Object.create(this)));
}
handleEventClick.prototype.handleEvent = function() {
window.foobar = "handleEventClick";
};
function boundHandleEventClick() {
var button = document.getElementById("boundHandleEventClick");
this.handleEvent = this.handleEvent.bind(this);
button.addEventListener("click", this);
}
boundHandleEventClick.prototype.handleEvent = function() {
window.foobar = "boundHandleEventClick";
};
var button = document.getElementById("clicker");
// Bind more than once to test for weird edge cases.
var boundClicker = clicker.bind(this).bind(this).bind(this);
button.addEventListener("click", boundClicker);
new handleEventClick();
new boundHandleEventClick();
var initButton = document.getElementById("initialSetup");
initButton.addEventListener("click", initialSetup);
});
</script>
</body>
</html>

View File

@ -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);
}