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:
Panos Astithas 2013-07-18 14:14:16 +03:00
Родитель b157d819e2
Коммит 169d483962
9 изменённых файлов: 654 добавлений и 26 удалений

Просмотреть файл

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