Source code

Revision control

Copy as Markdown

Other Tools

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
/**
*
* @param {String} groupID
* @param {String} eventType
* @param {Function} condition: Optional function that takes a Window as parameter. When
* passed, the event will only be included if the result of the function
* call is `true` (See `getAvailableEventBreakpoints`).
* @returns {Object}
*/
function generalEvent(groupID, eventType, condition) {
return {
id: `event.${groupID}.${eventType}`,
type: "event",
name: eventType,
message: `DOM '${eventType}' event`,
eventType,
filter: "general",
condition,
};
}
function nodeEvent(groupID, eventType) {
return {
...generalEvent(groupID, eventType),
filter: "node",
};
}
function mediaNodeEvent(groupID, eventType) {
return {
...generalEvent(groupID, eventType),
filter: "media",
};
}
function globalEvent(groupID, eventType) {
return {
...generalEvent(groupID, eventType),
message: `Global '${eventType}' event`,
filter: "global",
};
}
function xhrEvent(groupID, eventType) {
return {
...generalEvent(groupID, eventType),
message: `XHR '${eventType}' event`,
filter: "xhr",
};
}
function webSocketEvent(groupID, eventType) {
return {
...generalEvent(groupID, eventType),
message: `WebSocket '${eventType}' event`,
filter: "websocket",
};
}
function workerEvent(eventType) {
return {
...generalEvent("worker", eventType),
message: `Worker '${eventType}' event`,
filter: "worker",
};
}
function timerEvent(type, operation, name, notificationType) {
return {
id: `timer.${type}.${operation}`,
type: "simple",
name,
message: name,
notificationType,
};
}
function animationEvent(operation, name, notificationType) {
return {
id: `animationframe.${operation}`,
type: "simple",
name,
message: name,
notificationType,
};
}
const SCRIPT_FIRST_STATEMENT_BREAKPOINT = {
id: "script.source.firstStatement",
type: "script",
name: "Script First Statement",
message: "Script First Statement",
};
const AVAILABLE_BREAKPOINTS = [
{
name: "Animation",
items: [
animationEvent(
"request",
"Request Animation Frame",
"requestAnimationFrame"
),
animationEvent(
"cancel",
"Cancel Animation Frame",
"cancelAnimationFrame"
),
animationEvent(
"fire",
"Animation Frame fired",
"requestAnimationFrameCallback"
),
],
},
{
name: "Clipboard",
items: [
generalEvent("clipboard", "copy"),
generalEvent("clipboard", "cut"),
generalEvent("clipboard", "paste"),
generalEvent("clipboard", "beforecopy"),
generalEvent("clipboard", "beforecut"),
generalEvent("clipboard", "beforepaste"),
],
},
{
name: "Control",
items: [
// The condition should be removed when "dom.element.popover.enabled" is removed
generalEvent("control", "beforetoggle", () =>
// Services.prefs isn't available on worker targets
Services.prefs?.getBoolPref("dom.element.popover.enabled")
),
generalEvent("control", "blur"),
generalEvent("control", "change"),
generalEvent("control", "focus"),
generalEvent("control", "focusin"),
generalEvent("control", "focusout"),
// The condition should be removed when "dom.element.invokers.enabled" is removed
generalEvent(
"control",
"invoke",
global => global && "InvokeEvent" in global
),
generalEvent("control", "reset"),
generalEvent("control", "resize"),
generalEvent("control", "scroll"),
generalEvent("control", "scrollend"),
generalEvent("control", "select"),
generalEvent("control", "toggle"),
generalEvent("control", "submit"),
generalEvent("control", "zoom"),
],
},
{
name: "DOM Mutation",
items: [
// Deprecated DOM events.
nodeEvent("dom-mutation", "DOMActivate"),
nodeEvent("dom-mutation", "DOMFocusIn"),
nodeEvent("dom-mutation", "DOMFocusOut"),
// Standard DOM mutation events.
nodeEvent("dom-mutation", "DOMAttrModified"),
nodeEvent("dom-mutation", "DOMCharacterDataModified"),
nodeEvent("dom-mutation", "DOMNodeInserted"),
nodeEvent("dom-mutation", "DOMNodeInsertedIntoDocument"),
nodeEvent("dom-mutation", "DOMNodeRemoved"),
nodeEvent("dom-mutation", "DOMNodeRemovedIntoDocument"),
nodeEvent("dom-mutation", "DOMSubtreeModified"),
// DOM load events.
nodeEvent("dom-mutation", "DOMContentLoaded"),
],
},
{
name: "Device",
items: [
globalEvent("device", "deviceorientation"),
globalEvent("device", "devicemotion"),
],
},
{
name: "Drag and Drop",
items: [
generalEvent("drag-and-drop", "drag"),
generalEvent("drag-and-drop", "dragstart"),
generalEvent("drag-and-drop", "dragend"),
generalEvent("drag-and-drop", "dragenter"),
generalEvent("drag-and-drop", "dragover"),
generalEvent("drag-and-drop", "dragleave"),
generalEvent("drag-and-drop", "drop"),
],
},
{
name: "Keyboard",
items: [
generalEvent("keyboard", "beforeinput"),
generalEvent("keyboard", "input"),
generalEvent("keyboard", "keydown"),
generalEvent("keyboard", "keyup"),
generalEvent("keyboard", "keypress"),
generalEvent("keyboard", "compositionstart"),
generalEvent("keyboard", "compositionupdate"),
generalEvent("keyboard", "compositionend"),
].filter(Boolean),
},
{
name: "Load",
items: [
globalEvent("load", "load"),
globalEvent("load", "beforeunload"),
globalEvent("load", "unload"),
globalEvent("load", "abort"),
globalEvent("load", "error"),
globalEvent("load", "hashchange"),
globalEvent("load", "popstate"),
],
},
{
name: "Media",
items: [
mediaNodeEvent("media", "play"),
mediaNodeEvent("media", "pause"),
mediaNodeEvent("media", "playing"),
mediaNodeEvent("media", "canplay"),
mediaNodeEvent("media", "canplaythrough"),
mediaNodeEvent("media", "seeking"),
mediaNodeEvent("media", "seeked"),
mediaNodeEvent("media", "timeupdate"),
mediaNodeEvent("media", "ended"),
mediaNodeEvent("media", "ratechange"),
mediaNodeEvent("media", "durationchange"),
mediaNodeEvent("media", "volumechange"),
mediaNodeEvent("media", "loadstart"),
mediaNodeEvent("media", "progress"),
mediaNodeEvent("media", "suspend"),
mediaNodeEvent("media", "abort"),
mediaNodeEvent("media", "error"),
mediaNodeEvent("media", "emptied"),
mediaNodeEvent("media", "stalled"),
mediaNodeEvent("media", "loadedmetadata"),
mediaNodeEvent("media", "loadeddata"),
mediaNodeEvent("media", "waiting"),
],
},
{
name: "Mouse",
items: [
generalEvent("mouse", "auxclick"),
generalEvent("mouse", "click"),
generalEvent("mouse", "dblclick"),
generalEvent("mouse", "mousedown"),
generalEvent("mouse", "mouseup"),
generalEvent("mouse", "mouseover"),
generalEvent("mouse", "mousemove"),
generalEvent("mouse", "mouseout"),
generalEvent("mouse", "mouseenter"),
generalEvent("mouse", "mouseleave"),
generalEvent("mouse", "mousewheel"),
generalEvent("mouse", "wheel"),
generalEvent("mouse", "contextmenu"),
],
},
{
name: "Pointer",
items: [
generalEvent("pointer", "pointerover"),
generalEvent("pointer", "pointerout"),
generalEvent("pointer", "pointerenter"),
generalEvent("pointer", "pointerleave"),
generalEvent("pointer", "pointerdown"),
generalEvent("pointer", "pointerup"),
generalEvent("pointer", "pointermove"),
generalEvent("pointer", "pointercancel"),
generalEvent("pointer", "gotpointercapture"),
generalEvent("pointer", "lostpointercapture"),
],
},
{
name: "Script",
items: [SCRIPT_FIRST_STATEMENT_BREAKPOINT],
},
{
name: "Timer",
items: [
timerEvent("timeout", "set", "setTimeout", "setTimeout"),
timerEvent("timeout", "clear", "clearTimeout", "clearTimeout"),
timerEvent("timeout", "fire", "setTimeout fired", "setTimeoutCallback"),
timerEvent("interval", "set", "setInterval", "setInterval"),
timerEvent("interval", "clear", "clearInterval", "clearInterval"),
timerEvent(
"interval",
"fire",
"setInterval fired",
"setIntervalCallback"
),
],
},
{
name: "Touch",
items: [
generalEvent("touch", "touchstart"),
generalEvent("touch", "touchmove"),
generalEvent("touch", "touchend"),
generalEvent("touch", "touchcancel"),
],
},
{
name: "WebSocket",
items: [
webSocketEvent("websocket", "open"),
webSocketEvent("websocket", "message"),
webSocketEvent("websocket", "error"),
webSocketEvent("websocket", "close"),
],
},
{
name: "Worker",
items: [
workerEvent("message"),
workerEvent("messageerror"),
// Service Worker events.
globalEvent("serviceworker", "fetch"),
],
},
{
name: "XHR",
items: [
xhrEvent("xhr", "readystatechange"),
xhrEvent("xhr", "load"),
xhrEvent("xhr", "loadstart"),
xhrEvent("xhr", "loadend"),
xhrEvent("xhr", "abort"),
xhrEvent("xhr", "error"),
xhrEvent("xhr", "progress"),
xhrEvent("xhr", "timeout"),
],
},
];
const FLAT_EVENTS = [];
for (const category of AVAILABLE_BREAKPOINTS) {
for (const event of category.items) {
FLAT_EVENTS.push(event);
}
}
const EVENTS_BY_ID = {};
for (const event of FLAT_EVENTS) {
if (EVENTS_BY_ID[event.id]) {
throw new Error("Duplicate event ID detected: " + event.id);
}
EVENTS_BY_ID[event.id] = event;
}
const SIMPLE_EVENTS = {};
const DOM_EVENTS = {};
for (const eventBP of FLAT_EVENTS) {
if (eventBP.type === "simple") {
const { notificationType } = eventBP;
if (SIMPLE_EVENTS[notificationType]) {
throw new Error("Duplicate simple event");
}
SIMPLE_EVENTS[notificationType] = eventBP.id;
} else if (eventBP.type === "event") {
const { eventType, filter } = eventBP;
let targetTypes;
if (filter === "global") {
targetTypes = ["global"];
} else if (filter === "xhr") {
targetTypes = ["xhr"];
} else if (filter === "websocket") {
targetTypes = ["websocket"];
} else if (filter === "worker") {
targetTypes = ["worker"];
} else if (filter === "general") {
targetTypes = ["global", "node"];
} else if (filter === "node" || filter === "media") {
targetTypes = ["node"];
} else {
throw new Error("Unexpected filter type");
}
for (const targetType of targetTypes) {
let byEventType = DOM_EVENTS[targetType];
if (!byEventType) {
byEventType = {};
DOM_EVENTS[targetType] = byEventType;
}
if (byEventType[eventType]) {
throw new Error("Duplicate dom event: " + eventType);
}
byEventType[eventType] = eventBP.id;
}
} else if (eventBP.type === "script") {
// Nothing to do.
} else {
throw new Error("Unknown type: " + eventBP.type);
}
}
exports.eventBreakpointForNotification = eventBreakpointForNotification;
function eventBreakpointForNotification(dbg, notification) {
const notificationType = notification.type;
if (notification.type === "domEvent") {
const domEventNotification = DOM_EVENTS[notification.targetType];
if (!domEventNotification) {
return null;
}
// The 'event' value is a cross-compartment wrapper for the DOM Event object.
// While we could use that directly in the main thread as an Xray wrapper,
// when debugging workers we can't, because it is an opaque wrapper.
// To make things work, we have to always interact with the Event object via
// the Debugger.Object interface.
const evt = dbg
.makeGlobalObjectReference(notification.global)
.makeDebuggeeValue(notification.event);
const eventType = evt.getProperty("type").return;
const id = domEventNotification[eventType];
if (!id) {
return null;
}
const eventBreakpoint = EVENTS_BY_ID[id];
if (eventBreakpoint.filter === "media") {
const currentTarget = evt.getProperty("currentTarget").return;
if (!currentTarget) {
return null;
}
const nodeType = currentTarget.getProperty("nodeType").return;
const namespaceURI = currentTarget.getProperty("namespaceURI").return;
if (
nodeType !== 1 /* ELEMENT_NODE */ ||
namespaceURI !== "http://www.w3.org/1999/xhtml"
) {
return null;
}
const nodeName = currentTarget
.getProperty("nodeName")
.return.toLowerCase();
if (nodeName !== "audio" && nodeName !== "video") {
return null;
}
}
return id;
}
return SIMPLE_EVENTS[notificationType] || null;
}
exports.makeEventBreakpointMessage = makeEventBreakpointMessage;
function makeEventBreakpointMessage(id) {
return EVENTS_BY_ID[id].message;
}
exports.firstStatementBreakpointId = firstStatementBreakpointId;
function firstStatementBreakpointId() {
return SCRIPT_FIRST_STATEMENT_BREAKPOINT.id;
}
exports.eventsRequireNotifications = eventsRequireNotifications;
function eventsRequireNotifications(ids) {
for (const id of ids) {
const eventBreakpoint = EVENTS_BY_ID[id];
// Script events are implemented directly in the server and do not require
// notifications from Gecko, so there is no need to watch for them.
if (eventBreakpoint && eventBreakpoint.type !== "script") {
return true;
}
}
return false;
}
exports.getAvailableEventBreakpoints = getAvailableEventBreakpoints;
/**
* Get all available event breakpoints
*
* @param {Window|WorkerGlobalScope} global
* @returns {Array<Object>} An array containing object with 2 properties, an id and a name,
* representing the event.
*/
function getAvailableEventBreakpoints(global) {
const available = [];
for (const { name, items } of AVAILABLE_BREAKPOINTS) {
available.push({
name,
events: items
.filter(item => !item.condition || item.condition(global))
.map(item => ({
id: item.id,
name: item.name,
})),
});
}
return available;
}
exports.validateEventBreakpoint = validateEventBreakpoint;
function validateEventBreakpoint(id) {
return !!EVENTS_BY_ID[id];
}