зеркало из https://github.com/mozilla/gecko-dev.git
Implement backend support for breaking on DOM events and retrieving all the event listeners on a page (bug 832982); r=rcampbell,smaug
This commit is contained in:
Родитель
b157d819e2
Коммит
169d483962
|
@ -110,6 +110,8 @@ MOCHITEST_BROWSER_TESTS = \
|
|||
browser_dbg_source_maps-01.js \
|
||||
browser_dbg_source_maps-02.js \
|
||||
browser_dbg_step-out.js \
|
||||
browser_dbg_event-listeners.js \
|
||||
browser_dbg_break-on-dom-event.js \
|
||||
head.js \
|
||||
$(NULL)
|
||||
|
||||
|
@ -154,6 +156,7 @@ MOCHITEST_BROWSER_PAGES = \
|
|||
test-location-changes-bp.html \
|
||||
test-step-out.html \
|
||||
test-pause-exceptions-reload.html \
|
||||
test-event-listeners.html \
|
||||
$(NULL)
|
||||
|
||||
# Bug 888811 & bug 891176:
|
||||
|
|
|
@ -0,0 +1,158 @@
|
|||
/*
|
||||
* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/
|
||||
*/
|
||||
|
||||
// Tests that the break-on-dom-events request works.
|
||||
|
||||
var gClient = null;
|
||||
var gTab = null;
|
||||
var gThreadClient = null;
|
||||
var gInput = null;
|
||||
var gButton = null;
|
||||
const DEBUGGER_TAB_URL = EXAMPLE_URL + "test-event-listeners.html";
|
||||
|
||||
function test()
|
||||
{
|
||||
let transport = DebuggerServer.connectPipe();
|
||||
gClient = new DebuggerClient(transport);
|
||||
gClient.connect(function(type, traits) {
|
||||
gTab = addTab(DEBUGGER_TAB_URL, function() {
|
||||
attach_thread_actor_for_url(gClient,
|
||||
DEBUGGER_TAB_URL,
|
||||
function(threadClient) {
|
||||
gThreadClient = threadClient;
|
||||
gInput = content.document.querySelector("input");
|
||||
gButton = content.document.querySelector("button");
|
||||
testBreakOnAll();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Test pause on all events.
|
||||
function testBreakOnAll()
|
||||
{
|
||||
gClient.addOneTimeListener("paused", function(event, packet) {
|
||||
is(packet.why.type, "debuggerStatement", "debugger statement was hit.");
|
||||
// Test calling pauseOnDOMEvents from a paused state.
|
||||
gThreadClient.pauseOnDOMEvents("*", function(packet) {
|
||||
is(packet, undefined, "The pause-on-any-event request completed successfully.");
|
||||
|
||||
gThreadClient.resume(function() {
|
||||
gClient.addOneTimeListener("paused", function(event, packet) {
|
||||
is(packet.why.type, "pauseOnDOMEvents", "A hidden breakpoint was hit.");
|
||||
is(packet.frame.callee.name, "keyupHandler", "The keyupHandler is entered.");
|
||||
|
||||
gThreadClient.resume(function() {
|
||||
gClient.addOneTimeListener("paused", function(event, packet) {
|
||||
is(packet.why.type, "pauseOnDOMEvents", "A hidden breakpoint was hit.");
|
||||
is(packet.frame.callee.name, "clickHandler", "The clickHandler is entered.");
|
||||
|
||||
gThreadClient.resume(function() {
|
||||
gClient.addOneTimeListener("paused", function(event, packet) {
|
||||
is(packet.why.type, "pauseOnDOMEvents", "A hidden breakpoint was hit.");
|
||||
is(packet.frame.callee.name, "onchange", "The onchange handler is entered.");
|
||||
|
||||
gThreadClient.resume(testBreakOnDisabled);
|
||||
});
|
||||
|
||||
gInput.focus();
|
||||
gInput.value = "foo";
|
||||
gInput.blur();
|
||||
});
|
||||
});
|
||||
|
||||
EventUtils.sendMouseEvent({ type: "click" }, gButton);
|
||||
});
|
||||
});
|
||||
|
||||
gInput.focus();
|
||||
EventUtils.synthesizeKey("e", {}, content);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
EventUtils.sendMouseEvent({ type: "click" }, gButton);
|
||||
}
|
||||
|
||||
// Test that removing events from the array disables them.
|
||||
function testBreakOnDisabled()
|
||||
{
|
||||
// Test calling pauseOnDOMEvents from a running state.
|
||||
gThreadClient.pauseOnDOMEvents(["click"], function(packet) {
|
||||
is(packet.error, undefined, "The pause-on-click-only request completed successfully.");
|
||||
|
||||
gClient.addListener("paused", unexpectedListener);
|
||||
|
||||
// This non-capturing event listener is guaranteed to run after the page's
|
||||
// capturing one had a chance to execute and modify window.foobar.
|
||||
gInput.addEventListener("keyup", function tempHandler() {
|
||||
gInput.removeEventListener("keyup", tempHandler, false);
|
||||
is(content.wrappedJSObject.foobar, "keyupHandler", "No hidden breakpoint was hit.");
|
||||
gClient.removeListener("paused", unexpectedListener);
|
||||
testBreakOnNone();
|
||||
}, false);
|
||||
|
||||
gInput.focus();
|
||||
EventUtils.synthesizeKey("e", {}, content);
|
||||
});
|
||||
}
|
||||
|
||||
// Test that specifying an empty event array clears all hidden breakpoints.
|
||||
function testBreakOnNone()
|
||||
{
|
||||
// Test calling pauseOnDOMEvents from a running state.
|
||||
gThreadClient.pauseOnDOMEvents([], function(packet) {
|
||||
is(packet.error, undefined, "The pause-on-none request completed successfully.");
|
||||
|
||||
gClient.addListener("paused", unexpectedListener);
|
||||
|
||||
// This non-capturing event listener is guaranteed to run after the page's
|
||||
// capturing one had a chance to execute and modify window.foobar.
|
||||
gInput.addEventListener("keyup", function tempHandler() {
|
||||
gInput.removeEventListener("keyup", tempHandler, false);
|
||||
is(content.wrappedJSObject.foobar, "keyupHandler", "No hidden breakpoint was hit.");
|
||||
gClient.removeListener("paused", unexpectedListener);
|
||||
testBreakOnClick();
|
||||
}, false);
|
||||
|
||||
gInput.focus();
|
||||
EventUtils.synthesizeKey("g", {}, content);
|
||||
});
|
||||
}
|
||||
|
||||
function unexpectedListener(event, packet, callback) {
|
||||
gClient.removeListener("paused", unexpectedListener);
|
||||
ok(false, "An unexpected hidden breakpoint was hit.");
|
||||
gThreadClient.resume(testBreakOnClick);
|
||||
}
|
||||
|
||||
// Test pause on a single event.
|
||||
function testBreakOnClick()
|
||||
{
|
||||
// Test calling pauseOnDOMEvents from a running state.
|
||||
gThreadClient.pauseOnDOMEvents(["click"], function(packet) {
|
||||
is(packet.error, undefined, "The pause-on-click request completed successfully.");
|
||||
|
||||
gClient.addOneTimeListener("paused", function(event, packet) {
|
||||
is(packet.why.type, "pauseOnDOMEvents", "A hidden breakpoint was hit.");
|
||||
is(packet.frame.callee.name, "clickHandler", "The clickHandler is entered.");
|
||||
|
||||
gThreadClient.resume(function() {
|
||||
gClient.close(finish);
|
||||
});
|
||||
});
|
||||
|
||||
EventUtils.sendMouseEvent({ type: "click" }, gButton);
|
||||
});
|
||||
}
|
||||
|
||||
registerCleanupFunction(function() {
|
||||
removeTab(gTab);
|
||||
gTab = null;
|
||||
gClient = null;
|
||||
gThreadClient = null;
|
||||
gInput = null;
|
||||
gButton = null;
|
||||
});
|
|
@ -0,0 +1,91 @@
|
|||
/*
|
||||
* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/
|
||||
*/
|
||||
|
||||
// Tests that the eventListeners request works.
|
||||
|
||||
var gClient = null;
|
||||
var gTab = null;
|
||||
var gThreadClient = null;
|
||||
const DEBUGGER_TAB_URL = EXAMPLE_URL + "test-event-listeners.html";
|
||||
|
||||
function test()
|
||||
{
|
||||
let transport = DebuggerServer.connectPipe();
|
||||
gClient = new DebuggerClient(transport);
|
||||
gClient.connect(function(aType, aTraits) {
|
||||
gTab = addTab(DEBUGGER_TAB_URL, function() {
|
||||
attach_thread_actor_for_url(gClient,
|
||||
DEBUGGER_TAB_URL,
|
||||
function(threadClient) {
|
||||
gThreadClient = threadClient;
|
||||
testEventListeners();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function testEventListeners()
|
||||
{
|
||||
gClient.addOneTimeListener("paused", function(aEvent, aPacket) {
|
||||
is(aPacket.why.type, "debuggerStatement", "debugger statement was hit.");
|
||||
gThreadClient.eventListeners(function(aPacket) {
|
||||
is(aPacket.listeners.length, 4, "Found all event listeners.");
|
||||
let types = [];
|
||||
for (let l of aPacket.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");
|
||||
ok(l.function, "There is a function property.");
|
||||
is(l.function.type, "object", "The function form is of type 'object'.");
|
||||
is(l.function.class, "Function", "The function form is of class 'Function'.");
|
||||
is(l.function.url, DEBUGGER_TAB_URL, "The function url is correct.");
|
||||
is(l.allowsUntrusted, true,
|
||||
"allowsUntrusted property has the right value.");
|
||||
is(l.inSystemEventGroup, false,
|
||||
"inSystemEventGroup property has the right value.");
|
||||
|
||||
types.push(l.type);
|
||||
|
||||
if (l.type == "keyup") {
|
||||
is(l.capturing, true, "Capturing property has the right value.");
|
||||
is(l.isEventHandler, false,
|
||||
"isEventHandler property has the right value.");
|
||||
} else if (l.type == "load") {
|
||||
is(l.capturing, false, "Capturing property has the right value.");
|
||||
is(l.isEventHandler, false,
|
||||
"isEventHandler property has the right value.");
|
||||
} else {
|
||||
is(l.capturing, false, "Capturing property has the right value.");
|
||||
is(l.isEventHandler, true,
|
||||
"isEventHandler property has the right value.");
|
||||
}
|
||||
}
|
||||
ok(types.indexOf("click") != -1, "Found the click handler.");
|
||||
ok(types.indexOf("change") != -1, "Found the change handler.");
|
||||
ok(types.indexOf("keyup") != -1, "Found the keyup handler.");
|
||||
finish_test();
|
||||
});
|
||||
});
|
||||
|
||||
EventUtils.sendMouseEvent({ type: "click" },
|
||||
content.document.querySelector("button"));
|
||||
}
|
||||
|
||||
function finish_test()
|
||||
{
|
||||
gThreadClient.resume(function() {
|
||||
gClient.close(finish);
|
||||
});
|
||||
}
|
||||
|
||||
registerCleanupFunction(function() {
|
||||
removeTab(gTab);
|
||||
gTab = null;
|
||||
gClient = null;
|
||||
gThreadClient = null;
|
||||
});
|
|
@ -37,6 +37,13 @@ let gEnableLogging = Services.prefs.getBoolPref("devtools.debugger.log");
|
|||
Services.prefs.setBoolPref("devtools.debugger.remote-enabled", true);
|
||||
Services.prefs.setBoolPref("devtools.debugger.log", true);
|
||||
|
||||
// Redeclare dbg_assert with a fatal behavior.
|
||||
function dbg_assert(cond, e) {
|
||||
if (!cond) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
registerCleanupFunction(function() {
|
||||
Services.prefs.setBoolPref("devtools.debugger.remote-enabled", gEnableRemote);
|
||||
Services.prefs.setBoolPref("devtools.debugger.log", gEnableLogging);
|
||||
|
@ -143,11 +150,11 @@ function attach_tab_actor_for_url(aClient, aURL, aCallback) {
|
|||
|
||||
function attach_thread_actor_for_url(aClient, aURL, aCallback) {
|
||||
attach_tab_actor_for_url(aClient, aURL, function(aTabActor, aResponse) {
|
||||
aClient.attachThread(actor.threadActor, function(aResponse, aThreadClient) {
|
||||
aClient.attachThread(aResponse.threadActor, function(aResponse, aThreadClient) {
|
||||
// We don't care about the pause right now (use
|
||||
// get_actor_for_url() if you do), so resume it.
|
||||
aThreadClient.resume(function(aResponse) {
|
||||
aCallback(actor);
|
||||
aCallback(aThreadClient);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset='utf-8'/>
|
||||
<title>Debugger Test for Event Listeners</title>
|
||||
<!-- Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ -->
|
||||
<script type="text/javascript">
|
||||
window.addEventListener("load", function() {
|
||||
function initialSetup(event) {
|
||||
debugger;
|
||||
var button = document.querySelector("button");
|
||||
button.onclick = clickHandler;
|
||||
}
|
||||
function clickHandler(event) {
|
||||
window.foobar = "clickHandler";
|
||||
}
|
||||
function changeHandler(event) {
|
||||
window.foobar = "changeHandler";
|
||||
}
|
||||
function keyupHandler(event) {
|
||||
window.foobar = "keyupHandler";
|
||||
}
|
||||
var button = document.querySelector("button");
|
||||
button.onclick = initialSetup;
|
||||
var input = document.querySelector("input");
|
||||
input.addEventListener("keyup", keyupHandler, true);
|
||||
window.changeHandler = changeHandler;
|
||||
});
|
||||
</script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<button>Click me!</button>
|
||||
<input type="text" onchange="changeHandler()">
|
||||
</body>
|
||||
</html>
|
|
@ -3166,6 +3166,18 @@
|
|||
"n_buckets": "1000",
|
||||
"description": "The time (in milliseconds) that it took a 'navigateTo' request to go round trip."
|
||||
},
|
||||
"DEVTOOLS_DEBUGGER_RDP_LOCAL_EVENTLISTENERS_MS": {
|
||||
"kind": "exponential",
|
||||
"high": "10000",
|
||||
"n_buckets": "1000",
|
||||
"description": "The time (in milliseconds) that it took an 'eventListeners' request to go round trip."
|
||||
},
|
||||
"DEVTOOLS_DEBUGGER_RDP_REMOTE_EVENTLISTENERS_MS": {
|
||||
"kind": "exponential",
|
||||
"high": "10000",
|
||||
"n_buckets": "1000",
|
||||
"description": "The time (in milliseconds) that it took an 'eventListeners' request to go round trip."
|
||||
},
|
||||
"DEVTOOLS_DEBUGGER_RDP_LOCAL_DETACH_MS": {
|
||||
"kind": "exponential",
|
||||
"high": "10000",
|
||||
|
|
|
@ -20,6 +20,7 @@ this.EXPORTED_SYMBOLS = ["DebuggerTransport",
|
|||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/NetUtil.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/Timer.jsm");
|
||||
let promise = Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js").Promise;
|
||||
const { defer, resolve, reject } = promise;
|
||||
|
||||
|
@ -199,6 +200,7 @@ const UnsolicitedPauses = {
|
|||
"resumeLimit": "resumeLimit",
|
||||
"debuggerStatement": "debuggerStatement",
|
||||
"breakpoint": "breakpoint",
|
||||
"DOMEvent": "DOMEvent",
|
||||
"watchpoint": "watchpoint",
|
||||
"exception": "exception"
|
||||
};
|
||||
|
@ -1023,6 +1025,7 @@ ThreadClient.prototype = {
|
|||
get paused() { return this._state === "paused"; },
|
||||
|
||||
_pauseOnExceptions: false,
|
||||
_pauseOnDOMEvents: null,
|
||||
|
||||
_actor: null,
|
||||
get actor() { return this._actor; },
|
||||
|
@ -1058,7 +1061,15 @@ ThreadClient.prototype = {
|
|||
// further requests that should only be sent in the paused state.
|
||||
this._state = "resuming";
|
||||
|
||||
aPacket.pauseOnExceptions = this._pauseOnExceptions;
|
||||
if (!aPacket.resumeLimit) {
|
||||
delete aPacket.resumeLimit;
|
||||
}
|
||||
if (this._pauseOnExceptions) {
|
||||
aPacket.pauseOnExceptions = this._pauseOnExceptions;
|
||||
}
|
||||
if (this._pauseOnDOMEvents) {
|
||||
aPacket.pauseOnDOMEvents = this._pauseOnDOMEvents;
|
||||
}
|
||||
return aPacket;
|
||||
},
|
||||
after: function (aResponse) {
|
||||
|
@ -1147,6 +1158,33 @@ ThreadClient.prototype = {
|
|||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Enable pausing when the specified DOM events are triggered. Disabling
|
||||
* pausing on an event can be realized by calling this method with the updated
|
||||
* array of events that doesn't contain it.
|
||||
*
|
||||
* @param array|string events
|
||||
* An array of strings, representing the DOM event types to pause on,
|
||||
* or "*" to pause on all DOM events. Pass an empty array to
|
||||
* completely disable pausing on DOM events.
|
||||
* @param function onResponse
|
||||
* Called with the response packet in a future turn of the event loop.
|
||||
*/
|
||||
pauseOnDOMEvents: function (events, onResponse) {
|
||||
this._pauseOnDOMEvents = events;
|
||||
// If the debuggee is paused, the value of the array will be communicated in
|
||||
// the next resumption. Otherwise we have to force a pause in order to send
|
||||
// the array.
|
||||
if (this.paused)
|
||||
return void setTimeout(onResponse, 0);
|
||||
this.interrupt(response => {
|
||||
// Can't continue if pausing failed.
|
||||
if (response.error)
|
||||
return void onResponse(response);
|
||||
this.resume(onResponse);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Send a clientEvaluate packet to the debuggee. Response
|
||||
* will be a resume packet.
|
||||
|
@ -1272,6 +1310,18 @@ ThreadClient.prototype = {
|
|||
telemetry: "THREADGRIPS"
|
||||
}),
|
||||
|
||||
/**
|
||||
* Return the event listeners defined on the page.
|
||||
*
|
||||
* @param aOnResponse Function
|
||||
* Called with the thread's response.
|
||||
*/
|
||||
eventListeners: DebuggerClient.requester({
|
||||
type: "eventListeners"
|
||||
}, {
|
||||
telemetry: "EVENTLISTENERS"
|
||||
}),
|
||||
|
||||
/**
|
||||
* Request the loaded sources for the current thread.
|
||||
*
|
||||
|
|
|
@ -31,10 +31,13 @@ function ThreadActor(aHooks, aGlobal)
|
|||
this._environmentActors = [];
|
||||
this._hooks = aHooks;
|
||||
this.global = aGlobal;
|
||||
// A map of actorID -> actor for breakpoints created and managed by the server.
|
||||
this._hiddenBreakpoints = new Map();
|
||||
|
||||
this.findGlobals = this.globalManager.findGlobals.bind(this);
|
||||
this.onNewGlobal = this.globalManager.onNewGlobal.bind(this);
|
||||
this.onNewSource = this.onNewSource.bind(this);
|
||||
this._allEventsListener = this._allEventsListener.bind(this);
|
||||
|
||||
this._options = {
|
||||
useSourceMaps: false
|
||||
|
@ -85,14 +88,18 @@ ThreadActor.prototype = {
|
|||
|
||||
/**
|
||||
* Add a debuggee global to the Debugger object.
|
||||
*
|
||||
* @returns the Debugger.Object that corresponds to the global.
|
||||
*/
|
||||
addDebuggee: function TA_addDebuggee(aGlobal) {
|
||||
let globalDebugObject;
|
||||
try {
|
||||
this.dbg.addDebuggee(aGlobal);
|
||||
globalDebugObject = this.dbg.addDebuggee(aGlobal);
|
||||
} catch (e) {
|
||||
// Ignore attempts to add the debugger's compartment as a debuggee.
|
||||
dumpn("Ignoring request to add the debugger's compartment as a debuggee");
|
||||
}
|
||||
return globalDebugObject;
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -122,15 +129,18 @@ ThreadActor.prototype = {
|
|||
|
||||
/**
|
||||
* Add the provided window and all windows in its frame tree as debuggees.
|
||||
*
|
||||
* @returns the Debugger.Object that corresponds to the window.
|
||||
*/
|
||||
_addDebuggees: function TA__addDebuggees(aWindow) {
|
||||
this.addDebuggee(aWindow);
|
||||
let globalDebugObject = this.addDebuggee(aWindow);
|
||||
let frames = aWindow.frames;
|
||||
if (frames) {
|
||||
for (let i = 0; i < frames.length; i++) {
|
||||
this._addDebuggees(frames[i]);
|
||||
}
|
||||
}
|
||||
return globalDebugObject;
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -139,7 +149,7 @@ ThreadActor.prototype = {
|
|||
*/
|
||||
globalManager: {
|
||||
findGlobals: function TA_findGlobals() {
|
||||
this._addDebuggees(this.global);
|
||||
this.globalDebugObject = this._addDebuggees(this.global);
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -426,7 +436,18 @@ ThreadActor.prototype = {
|
|||
if (aRequest) {
|
||||
this._options.pauseOnExceptions = aRequest.pauseOnExceptions;
|
||||
this.maybePauseOnExceptions();
|
||||
// Break-on-DOMEvents is only supported in content debugging.
|
||||
let events = aRequest.pauseOnDOMEvents;
|
||||
if (this.global && events &&
|
||||
(events == "*" ||
|
||||
(Array.isArray(events) && events.length))) {
|
||||
this._pauseOnDOMEvents = events;
|
||||
let els = Cc["@mozilla.org/eventlistenerservice;1"]
|
||||
.getService(Ci.nsIEventListenerService);
|
||||
els.addListenerForAllEvents(this.global, this._allEventsListener, true);
|
||||
}
|
||||
}
|
||||
|
||||
let packet = this._resumed();
|
||||
DebuggerServer.xpcInspector.exitNestedEventLoop();
|
||||
return packet;
|
||||
|
@ -441,6 +462,86 @@ ThreadActor.prototype = {
|
|||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* A listener that gets called for every event fired on the page, when a list
|
||||
* of interesting events was provided with the pauseOnDOMEvents property. It
|
||||
* is used to set server-managed breakpoints on any existing event listeners
|
||||
* for those events.
|
||||
*
|
||||
* @param Event event
|
||||
* The event that was fired.
|
||||
*/
|
||||
_allEventsListener: function(event) {
|
||||
if (this._pauseOnDOMEvents == "*" ||
|
||||
this._pauseOnDOMEvents.indexOf(event.type) != -1) {
|
||||
for (let listener of this._getAllEventListeners(event.target)) {
|
||||
if (event.type == listener.type || this._pauseOnDOMEvents == "*") {
|
||||
this._breakOnEnter(listener.script);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Return an array containing all the event listeners attached to the
|
||||
* specified event target and its ancestors in the event target chain.
|
||||
*
|
||||
* @param EventTarget eventTarget
|
||||
* The target the event was dispatched on.
|
||||
* @returns Array
|
||||
*/
|
||||
_getAllEventListeners: function(eventTarget) {
|
||||
let els = Cc["@mozilla.org/eventlistenerservice;1"]
|
||||
.getService(Ci.nsIEventListenerService);
|
||||
|
||||
let targets = els.getEventTargetChainFor(eventTarget);
|
||||
let listeners = [];
|
||||
|
||||
for (let target of targets) {
|
||||
let handlers = els.getListenerInfoFor(target);
|
||||
for (let handler of handlers) {
|
||||
// Null is returned for all-events handlers, and native event listeners
|
||||
// don't provide any listenerObject, which makes them not that useful to
|
||||
// a JS debugger.
|
||||
if (!handler || !handler.listenerObject || !handler.type)
|
||||
continue;
|
||||
// Create a listener-like object suitable for our purposes.
|
||||
let l = Object.create(null);
|
||||
l.type = handler.type;
|
||||
let listener = handler.listenerObject;
|
||||
l.script = this.globalDebugObject.makeDebuggeeValue(listener).script;
|
||||
// Chrome listeners won't be converted to debuggee values, since their
|
||||
// compartment is not added as a debuggee.
|
||||
if (!l.script)
|
||||
continue;
|
||||
listeners.push(l);
|
||||
}
|
||||
}
|
||||
return listeners;
|
||||
},
|
||||
|
||||
/**
|
||||
* Set a breakpoint on the first bytecode offset in the provided script.
|
||||
*/
|
||||
_breakOnEnter: function(script) {
|
||||
let offsets = script.getAllOffsets();
|
||||
for (let line = 0, n = offsets.length; line < n; line++) {
|
||||
if (offsets[line]) {
|
||||
let location = { url: script.url, line: line };
|
||||
let resp = this._createAndStoreBreakpoint(location);
|
||||
dbg_assert(!resp.actualLocation, "No actualLocation should be returned");
|
||||
if (resp.error) {
|
||||
reportError(new Error("Unable to set breakpoint on event listener"));
|
||||
return;
|
||||
}
|
||||
let bpActor = this._breakpointStore[location.url][location.line].actor;
|
||||
dbg_assert(bpActor, "Breakpoint actor must be created");
|
||||
this._hiddenBreakpoints.set(bpActor.actorID, bpActor);
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Helper method that returns the next frame when stepping.
|
||||
*/
|
||||
|
@ -576,19 +677,7 @@ ThreadActor.prototype = {
|
|||
return { error: "noScript" };
|
||||
}
|
||||
|
||||
// Add the breakpoint to the store for later reuse, in case it belongs to a
|
||||
// script that hasn't appeared yet.
|
||||
if (!this._breakpointStore[aLocation.url]) {
|
||||
this._breakpointStore[aLocation.url] = [];
|
||||
}
|
||||
let scriptBreakpoints = this._breakpointStore[aLocation.url];
|
||||
scriptBreakpoints[line] = {
|
||||
url: aLocation.url,
|
||||
line: line,
|
||||
column: aLocation.column
|
||||
};
|
||||
|
||||
let response = this._setBreakpoint(aLocation);
|
||||
let response = this._createAndStoreBreakpoint(aLocation);
|
||||
// If the original location of our generated location is different from
|
||||
// the original location we attempted to set the breakpoint on, we will
|
||||
// need to know so that we can set actualLocation on the response.
|
||||
|
@ -617,6 +706,25 @@ ThreadActor.prototype = {
|
|||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a breakpoint at the specified location and store it in the cache.
|
||||
*/
|
||||
_createAndStoreBreakpoint: function (aLocation) {
|
||||
// Add the breakpoint to the store for later reuse, in case it belongs to
|
||||
// a script that hasn't appeared yet.
|
||||
if (!this._breakpointStore[aLocation.url]) {
|
||||
this._breakpointStore[aLocation.url] = [];
|
||||
}
|
||||
let scriptBreakpoints = this._breakpointStore[aLocation.url];
|
||||
scriptBreakpoints[aLocation.line] = {
|
||||
url: aLocation.url,
|
||||
line: aLocation.line,
|
||||
column: aLocation.column
|
||||
};
|
||||
|
||||
return this._setBreakpoint(aLocation);
|
||||
},
|
||||
|
||||
/**
|
||||
* Set a breakpoint using the jsdbg2 API. If the line on which the breakpoint
|
||||
* is being set contains no code, then the breakpoint will slide down to the
|
||||
|
@ -836,6 +944,59 @@ ThreadActor.prototype = {
|
|||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle a protocol request to retrieve all the event listeners on the page.
|
||||
*/
|
||||
onEventListeners: function TA_onEventListeners(aRequest) {
|
||||
// This request is only supported in content debugging.
|
||||
if (!this.global) {
|
||||
return {
|
||||
error: "notImplemented",
|
||||
message: "eventListeners request is only supported in content debugging"
|
||||
}
|
||||
}
|
||||
|
||||
let els = Cc["@mozilla.org/eventlistenerservice;1"]
|
||||
.getService(Ci.nsIEventListenerService);
|
||||
|
||||
let nodes = this.global.document.getElementsByTagName("*");
|
||||
nodes = [this.global].concat([].slice.call(nodes));
|
||||
let listeners = [];
|
||||
|
||||
for (let node of nodes) {
|
||||
let handlers = els.getListenerInfoFor(node);
|
||||
|
||||
for (let handler of handlers) {
|
||||
// Create a form object for serializing the listener via the protocol.
|
||||
let listenerForm = Object.create(null);
|
||||
let listener = handler.listenerObject;
|
||||
// Native event listeners don't provide any listenerObject and are not
|
||||
// that useful to a JS debugger.
|
||||
if (!listener) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// There will be no tagName if the event listener is set on the window.
|
||||
let selector = node.tagName ? findCssSelector(node) : "window";
|
||||
let nodeDO = this.globalDebugObject.makeDebuggeeValue(node);
|
||||
listenerForm.node = {
|
||||
selector: selector,
|
||||
object: this.createValueGrip(nodeDO)
|
||||
};
|
||||
listenerForm.type = handler.type;
|
||||
listenerForm.capturing = handler.capturing;
|
||||
listenerForm.allowsUntrusted = handler.allowsUntrusted;
|
||||
listenerForm.inSystemEventGroup = handler.inSystemEventGroup;
|
||||
listenerForm.isEventHandler = !!node["on" + listenerForm.type];
|
||||
// Get the Debugger.Object for the listener object.
|
||||
let listenerDO = this.globalDebugObject.makeDebuggeeValue(listener);
|
||||
listenerForm.function = this.createValueGrip(listenerDO);
|
||||
listeners.push(listenerForm);
|
||||
}
|
||||
}
|
||||
return { listeners: listeners };
|
||||
},
|
||||
|
||||
/**
|
||||
* Return the Debug.Frame for a frame mentioned by the protocol.
|
||||
*/
|
||||
|
@ -870,6 +1031,18 @@ ThreadActor.prototype = {
|
|||
aFrame.onStep = undefined;
|
||||
aFrame.onPop = undefined;
|
||||
}
|
||||
// Clear DOM event breakpoints.
|
||||
// XPCShell tests don't use actual DOM windows for globals and cause
|
||||
// removeListenerForAllEvents to throw.
|
||||
if (this.global && !this.global.toString().contains("Sandbox")) {
|
||||
let els = Cc["@mozilla.org/eventlistenerservice;1"]
|
||||
.getService(Ci.nsIEventListenerService);
|
||||
els.removeListenerForAllEvents(this.global, this._allEventsListener, true);
|
||||
for (let [,bp] of this._hiddenBreakpoints) {
|
||||
bp.onDelete();
|
||||
}
|
||||
this._hiddenBreakpoints.clear();
|
||||
}
|
||||
|
||||
this._state = "paused";
|
||||
|
||||
|
@ -879,7 +1052,7 @@ ThreadActor.prototype = {
|
|||
|
||||
// Create the actor pool that will hold the pause actor and its
|
||||
// children.
|
||||
dbg_assert(!this._pausePool);
|
||||
dbg_assert(!this._pausePool, "No pause pool should exist yet");
|
||||
this._pausePool = new ActorPool(this.conn);
|
||||
this.conn.addActorPool(this._pausePool);
|
||||
|
||||
|
@ -888,7 +1061,7 @@ ThreadActor.prototype = {
|
|||
this._pausePool.threadActor = this;
|
||||
|
||||
// Create the pause actor itself...
|
||||
dbg_assert(!this._pauseActor);
|
||||
dbg_assert(!this._pauseActor, "No pause actor should exist yet");
|
||||
this._pauseActor = new PauseActor(this._pausePool);
|
||||
this._pausePool.addActor(this._pauseActor);
|
||||
|
||||
|
@ -920,7 +1093,7 @@ ThreadActor.prototype = {
|
|||
requestor.connection = this.conn;
|
||||
DebuggerServer.xpcInspector.enterNestedEventLoop(requestor);
|
||||
|
||||
dbg_assert(this.state === "running");
|
||||
dbg_assert(this.state === "running", "Should be in the running state");
|
||||
|
||||
if (this._hooks.postNest) {
|
||||
this._hooks.postNest(nestData)
|
||||
|
@ -1358,6 +1531,7 @@ ThreadActor.prototype.requestTypes = {
|
|||
"clientEvaluate": ThreadActor.prototype.onClientEvaluate,
|
||||
"frames": ThreadActor.prototype.onFrames,
|
||||
"interrupt": ThreadActor.prototype.onInterrupt,
|
||||
"eventListeners": ThreadActor.prototype.onEventListeners,
|
||||
"releaseMany": ThreadActor.prototype.onReleaseMany,
|
||||
"setBreakpoint": ThreadActor.prototype.onSetBreakpoint,
|
||||
"sources": ThreadActor.prototype.onSources,
|
||||
|
@ -1593,6 +1767,12 @@ ObjectActor.prototype = {
|
|||
if (desc && desc.value && typeof desc.value == "string") {
|
||||
g.userDisplayName = this.threadActor.createValueGrip(desc.value);
|
||||
}
|
||||
|
||||
// Add source location information.
|
||||
if (this.obj.script) {
|
||||
g.url = this.obj.script.url;
|
||||
g.line = this.obj.script.startLine;
|
||||
}
|
||||
}
|
||||
|
||||
return g;
|
||||
|
@ -2209,8 +2389,14 @@ BreakpointActor.prototype = {
|
|||
return undefined;
|
||||
}
|
||||
|
||||
// TODO: add the rest of the breakpoints on that line (bug 676602).
|
||||
let reason = { type: "breakpoint", actors: [ this.actorID ] };
|
||||
let reason = {};
|
||||
if (this.threadActor._hiddenBreakpoints.has(this.actorID)) {
|
||||
reason.type = "pauseOnDOMEvents";
|
||||
} else {
|
||||
reason.type = "breakpoint";
|
||||
// TODO: add the rest of the breakpoints on that line (bug 676602).
|
||||
reason.actors = [ this.actorID ];
|
||||
}
|
||||
return this.threadActor._pauseAndRespond(aFrame, reason, (aPacket) => {
|
||||
let { url, line } = aPacket.frame.where;
|
||||
return this.threadActor.sources.getOriginalLocation(url, line)
|
||||
|
@ -2645,7 +2831,7 @@ ThreadSources.prototype = {
|
|||
if (aScript.url in this._sourceMapsByGeneratedSource) {
|
||||
return this._sourceMapsByGeneratedSource[aScript.url];
|
||||
}
|
||||
dbg_assert(aScript.sourceMapURL);
|
||||
dbg_assert(aScript.sourceMapURL, "Script should have a sourceMapURL");
|
||||
let sourceMapURL = this._normalize(aScript.sourceMapURL, aScript.url);
|
||||
let map = this._fetchSourceMap(sourceMapURL)
|
||||
.then((aSourceMap) => {
|
||||
|
@ -2783,7 +2969,7 @@ ThreadSources.prototype = {
|
|||
* Normalize multiple relative paths towards the base paths on the right.
|
||||
*/
|
||||
_normalize: function TS__normalize(...aURLs) {
|
||||
dbg_assert(aURLs.length > 1);
|
||||
dbg_assert(aURLs.length > 1, "Should have more than 1 URL");
|
||||
let base = Services.io.newURI(aURLs.pop(), null, null);
|
||||
let url;
|
||||
while ((url = aURLs.pop())) {
|
||||
|
@ -2952,3 +3138,80 @@ function reportError(aError, aPrefix="") {
|
|||
Cu.reportError(msg);
|
||||
dumpn(msg);
|
||||
}
|
||||
|
||||
// The following are copied here verbatim from css-logic.js, until we create a
|
||||
// server-friendly helper module.
|
||||
|
||||
/**
|
||||
* Find a unique CSS selector for a given element
|
||||
* @returns a string such that ele.ownerDocument.querySelector(reply) === ele
|
||||
* and ele.ownerDocument.querySelectorAll(reply).length === 1
|
||||
*/
|
||||
function findCssSelector(ele) {
|
||||
var document = ele.ownerDocument;
|
||||
if (ele.id && document.getElementById(ele.id) === ele) {
|
||||
return '#' + ele.id;
|
||||
}
|
||||
|
||||
// Inherently unique by tag name
|
||||
var tagName = ele.tagName.toLowerCase();
|
||||
if (tagName === 'html') {
|
||||
return 'html';
|
||||
}
|
||||
if (tagName === 'head') {
|
||||
return 'head';
|
||||
}
|
||||
if (tagName === 'body') {
|
||||
return 'body';
|
||||
}
|
||||
|
||||
if (ele.parentNode == null) {
|
||||
console.log('danger: ' + tagName);
|
||||
}
|
||||
|
||||
// We might be able to find a unique class name
|
||||
var selector, index, matches;
|
||||
if (ele.classList.length > 0) {
|
||||
for (var i = 0; i < ele.classList.length; i++) {
|
||||
// Is this className unique by itself?
|
||||
selector = '.' + ele.classList.item(i);
|
||||
matches = document.querySelectorAll(selector);
|
||||
if (matches.length === 1) {
|
||||
return selector;
|
||||
}
|
||||
// Maybe it's unique with a tag name?
|
||||
selector = tagName + selector;
|
||||
matches = document.querySelectorAll(selector);
|
||||
if (matches.length === 1) {
|
||||
return selector;
|
||||
}
|
||||
// Maybe it's unique using a tag name and nth-child
|
||||
index = positionInNodeList(ele, ele.parentNode.children) + 1;
|
||||
selector = selector + ':nth-child(' + index + ')';
|
||||
matches = document.querySelectorAll(selector);
|
||||
if (matches.length === 1) {
|
||||
return selector;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// So we can be unique w.r.t. our parent, and use recursion
|
||||
index = positionInNodeList(ele, ele.parentNode.children) + 1;
|
||||
selector = findCssSelector(ele.parentNode) + ' > ' +
|
||||
tagName + ':nth-child(' + index + ')';
|
||||
|
||||
return selector;
|
||||
};
|
||||
|
||||
/**
|
||||
* Find the position of [element] in [nodeList].
|
||||
* @returns an index of the match, or -1 if there is no match
|
||||
*/
|
||||
function positionInNodeList(element, nodeList) {
|
||||
for (var i = 0; i < nodeList.length; i++) {
|
||||
if (element === nodeList[i]) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
|
|
@ -43,6 +43,13 @@ function scriptErrorFlagsToKind(aFlags) {
|
|||
return kind;
|
||||
}
|
||||
|
||||
// Redeclare dbg_assert with a fatal behavior.
|
||||
function dbg_assert(cond, e) {
|
||||
if (!cond) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// Register a console listener, so console messages don't just disappear
|
||||
// into the ether.
|
||||
let errorCount = 0;
|
||||
|
|
Загрузка…
Ссылка в новой задаче