summaryrefslogtreecommitdiffstats
path: root/devtools/server/actors/utils/event-breakpoints.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/server/actors/utils/event-breakpoints.js')
-rw-r--r--devtools/server/actors/utils/event-breakpoints.js503
1 files changed, 503 insertions, 0 deletions
diff --git a/devtools/server/actors/utils/event-breakpoints.js b/devtools/server/actors/utils/event-breakpoints.js
new file mode 100644
index 0000000000..9de20dc143
--- /dev/null
+++ b/devtools/server/actors/utils/event-breakpoints.js
@@ -0,0 +1,503 @@
+/* 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: [
+ generalEvent("control", "resize"),
+ generalEvent("control", "scroll"),
+ // The condition should be removed when "apz.scrollend-event.content.enabled" is removed
+ generalEvent("control", "scrollend", win => "onscrollend" in win),
+ generalEvent("control", "zoom"),
+ generalEvent("control", "focus"),
+ generalEvent("control", "focusin"),
+ generalEvent("control", "focusout"),
+ generalEvent("control", "blur"),
+ generalEvent("control", "select"),
+ generalEvent("control", "change"),
+ generalEvent("control", "submit"),
+ generalEvent("control", "reset"),
+ ],
+ },
+ {
+ 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"),
+ // TODO: Disabled pending fixes for bug 1569775.
+ // 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} window
+ * @returns {Array<Object>} An array containing object with 2 properties, an id and a name,
+ * representing the event.
+ */
+function getAvailableEventBreakpoints(window) {
+ const available = [];
+ for (const { name, items } of AVAILABLE_BREAKPOINTS) {
+ available.push({
+ name,
+ events: items
+ .filter(item => !item.condition || item.condition(window))
+ .map(item => ({
+ id: item.id,
+ name: item.name,
+ })),
+ });
+ }
+ return available;
+}
+exports.validateEventBreakpoint = validateEventBreakpoint;
+function validateEventBreakpoint(id) {
+ return !!EVENTS_BY_ID[id];
+}