event-breakpoints.js (15848B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 "use strict"; 6 7 /** 8 * 9 * @param {string} groupID 10 * @param {string} eventType 11 * @param {Function} condition: Optional function that takes a Window as parameter. When 12 * passed, the event will only be included if the result of the function 13 * call is `true` (See `getAvailableEventBreakpoints`). 14 * @returns {object} 15 */ 16 function generalEvent(groupID, eventType, condition) { 17 return { 18 id: `event.${groupID}.${eventType}`, 19 type: "event", 20 name: eventType, 21 message: `DOM '${eventType}' event`, 22 eventType, 23 // DOM Events which may fire on the global object, or on DOM Elements 24 targetTypes: ["global", "node"], 25 condition, 26 }; 27 } 28 function nodeEvent(groupID, eventType) { 29 return { 30 ...generalEvent(groupID, eventType), 31 targetTypes: ["node"], 32 }; 33 } 34 function mediaNodeEvent(groupID, eventType) { 35 return { 36 ...generalEvent(groupID, eventType), 37 targetTypes: ["node"], 38 39 // Media events need some specific handling in `eventBreakpointForNotification()` 40 // to ensure that the event is fired on either <video> or <audio> tags. 41 isMediaEvent: true, 42 }; 43 } 44 function globalEvent(groupID, eventType) { 45 return { 46 ...generalEvent(groupID, eventType), 47 message: `Global '${eventType}' event`, 48 // DOM Events which are only fired on the global object 49 targetTypes: ["global"], 50 }; 51 } 52 function xhrEvent(groupID, eventType) { 53 return { 54 ...generalEvent(groupID, eventType), 55 message: `XHR '${eventType}' event`, 56 targetTypes: ["xhr"], 57 }; 58 } 59 60 function closeWatcherEvent(groupID, eventType) { 61 return { 62 ...generalEvent(groupID, eventType), 63 message: `CloseWatcher '${eventType}' event`, 64 targetTypes: ["closewatcher"], 65 }; 66 } 67 68 function webSocketEvent(groupID, eventType) { 69 return { 70 ...generalEvent(groupID, eventType), 71 message: `WebSocket '${eventType}' event`, 72 targetTypes: ["websocket"], 73 }; 74 } 75 76 function workerEvent(eventType) { 77 return { 78 ...generalEvent("worker", eventType), 79 message: `Worker '${eventType}' event`, 80 targetTypes: ["worker"], 81 }; 82 } 83 84 function timerEvent(type, operation, name, notificationType) { 85 return { 86 id: `timer.${type}.${operation}`, 87 type: "simple", 88 name, 89 message: name, 90 notificationType, 91 }; 92 } 93 94 function animationEvent(operation, name, notificationType) { 95 return { 96 id: `animationframe.${operation}`, 97 type: "simple", 98 name, 99 message: name, 100 notificationType, 101 }; 102 } 103 104 const SCRIPT_FIRST_STATEMENT_BREAKPOINT = { 105 id: "script.source.firstStatement", 106 type: "script", 107 name: "Script First Statement", 108 message: "Script First Statement", 109 }; 110 111 const AVAILABLE_BREAKPOINTS = [ 112 { 113 name: "Animation", 114 items: [ 115 animationEvent( 116 "request", 117 "Request Animation Frame", 118 "requestAnimationFrame" 119 ), 120 animationEvent( 121 "cancel", 122 "Cancel Animation Frame", 123 "cancelAnimationFrame" 124 ), 125 animationEvent( 126 "fire", 127 "Animation Frame fired", 128 "requestAnimationFrameCallback" 129 ), 130 ], 131 }, 132 { 133 name: "Clipboard", 134 items: [ 135 generalEvent("clipboard", "copy"), 136 generalEvent("clipboard", "cut"), 137 generalEvent("clipboard", "paste"), 138 generalEvent("clipboard", "beforecopy"), 139 generalEvent("clipboard", "beforecut"), 140 generalEvent("clipboard", "beforepaste"), 141 ], 142 }, 143 { 144 name: "CloseWatcher", 145 items: [ 146 closeWatcherEvent("closewatcher", "cancel", () => 147 Services.prefs.getBoolPref("dom.closewatcher.enabled") 148 ), 149 closeWatcherEvent("closewatcher", "close", () => 150 Services.prefs.getBoolPref("dom.closewatcher.enabled") 151 ), 152 ], 153 }, 154 { 155 name: "Control", 156 items: [ 157 generalEvent("control", "beforetoggle"), 158 generalEvent("control", "blur"), 159 generalEvent("control", "change"), 160 generalEvent("control", "focus"), 161 generalEvent("control", "focusin"), 162 generalEvent("control", "focusout"), 163 // The condition should be removed when "dom.element.commandfor.enabled" is removed 164 generalEvent( 165 "control", 166 "command", 167 global => global && "CommandEvent" in global 168 ), 169 generalEvent("control", "reset"), 170 generalEvent("control", "resize"), 171 generalEvent("control", "scroll"), 172 generalEvent("control", "scrollend"), 173 generalEvent("control", "select"), 174 generalEvent("control", "toggle"), 175 generalEvent("control", "submit"), 176 generalEvent("control", "zoom"), 177 ], 178 }, 179 { 180 name: "DOM Mutation", 181 items: [ 182 // Deprecated DOM events. 183 nodeEvent("dom-mutation", "DOMActivate"), 184 nodeEvent("dom-mutation", "DOMFocusIn"), 185 nodeEvent("dom-mutation", "DOMFocusOut"), 186 187 // DOM load events. 188 nodeEvent("dom-mutation", "DOMContentLoaded"), 189 ], 190 }, 191 { 192 name: "Device", 193 items: [ 194 globalEvent("device", "deviceorientation"), 195 globalEvent("device", "devicemotion"), 196 ], 197 }, 198 { 199 name: "Drag and Drop", 200 items: [ 201 generalEvent("drag-and-drop", "drag"), 202 generalEvent("drag-and-drop", "dragstart"), 203 generalEvent("drag-and-drop", "dragend"), 204 generalEvent("drag-and-drop", "dragenter"), 205 generalEvent("drag-and-drop", "dragover"), 206 generalEvent("drag-and-drop", "dragleave"), 207 generalEvent("drag-and-drop", "drop"), 208 ], 209 }, 210 { 211 name: "Keyboard", 212 items: [ 213 generalEvent("keyboard", "beforeinput"), 214 generalEvent("keyboard", "input"), 215 generalEvent("keyboard", "textInput", () => 216 // Services.prefs isn't available on worker targets 217 Services.prefs?.getBoolPref("dom.events.textevent.enabled") 218 ), 219 generalEvent("keyboard", "keydown"), 220 generalEvent("keyboard", "keyup"), 221 generalEvent("keyboard", "keypress"), 222 generalEvent("keyboard", "compositionstart"), 223 generalEvent("keyboard", "compositionupdate"), 224 generalEvent("keyboard", "compositionend"), 225 ].filter(Boolean), 226 }, 227 { 228 name: "Load", 229 items: [ 230 globalEvent("load", "load"), 231 globalEvent("load", "beforeunload"), 232 globalEvent("load", "unload"), 233 globalEvent("load", "abort"), 234 globalEvent("load", "error"), 235 globalEvent("load", "hashchange"), 236 globalEvent("load", "popstate"), 237 ], 238 }, 239 { 240 name: "Media", 241 items: [ 242 mediaNodeEvent("media", "play"), 243 mediaNodeEvent("media", "pause"), 244 mediaNodeEvent("media", "playing"), 245 mediaNodeEvent("media", "canplay"), 246 mediaNodeEvent("media", "canplaythrough"), 247 mediaNodeEvent("media", "seeking"), 248 mediaNodeEvent("media", "seeked"), 249 mediaNodeEvent("media", "timeupdate"), 250 mediaNodeEvent("media", "ended"), 251 mediaNodeEvent("media", "ratechange"), 252 mediaNodeEvent("media", "durationchange"), 253 mediaNodeEvent("media", "volumechange"), 254 mediaNodeEvent("media", "loadstart"), 255 mediaNodeEvent("media", "progress"), 256 mediaNodeEvent("media", "suspend"), 257 mediaNodeEvent("media", "abort"), 258 mediaNodeEvent("media", "error"), 259 mediaNodeEvent("media", "emptied"), 260 mediaNodeEvent("media", "stalled"), 261 mediaNodeEvent("media", "loadedmetadata"), 262 mediaNodeEvent("media", "loadeddata"), 263 mediaNodeEvent("media", "waiting"), 264 ], 265 }, 266 { 267 name: "Mouse", 268 items: [ 269 generalEvent("mouse", "auxclick"), 270 generalEvent("mouse", "click"), 271 generalEvent("mouse", "dblclick"), 272 generalEvent("mouse", "mousedown"), 273 generalEvent("mouse", "mouseup"), 274 generalEvent("mouse", "mouseover"), 275 generalEvent("mouse", "mousemove"), 276 generalEvent("mouse", "mouseout"), 277 generalEvent("mouse", "mouseenter"), 278 generalEvent("mouse", "mouseleave"), 279 generalEvent("mouse", "mousewheel"), 280 generalEvent("mouse", "wheel"), 281 generalEvent("mouse", "contextmenu"), 282 ], 283 }, 284 { 285 name: "Pointer", 286 items: [ 287 generalEvent("pointer", "pointerover"), 288 generalEvent("pointer", "pointerout"), 289 generalEvent("pointer", "pointerenter"), 290 generalEvent("pointer", "pointerleave"), 291 generalEvent("pointer", "pointerdown"), 292 generalEvent("pointer", "pointerup"), 293 generalEvent("pointer", "pointermove"), 294 generalEvent("pointer", "pointercancel"), 295 generalEvent("pointer", "pointerrawupdate"), 296 generalEvent("pointer", "gotpointercapture"), 297 generalEvent("pointer", "lostpointercapture"), 298 ], 299 }, 300 { 301 name: "Script", 302 items: [SCRIPT_FIRST_STATEMENT_BREAKPOINT], 303 }, 304 { 305 name: "Timer", 306 items: [ 307 timerEvent("timeout", "set", "setTimeout", "setTimeout"), 308 timerEvent("timeout", "clear", "clearTimeout", "clearTimeout"), 309 timerEvent("timeout", "fire", "setTimeout fired", "setTimeoutCallback"), 310 timerEvent("interval", "set", "setInterval", "setInterval"), 311 timerEvent("interval", "clear", "clearInterval", "clearInterval"), 312 timerEvent( 313 "interval", 314 "fire", 315 "setInterval fired", 316 "setIntervalCallback" 317 ), 318 ], 319 }, 320 { 321 name: "Touch", 322 items: [ 323 generalEvent("touch", "touchstart"), 324 generalEvent("touch", "touchmove"), 325 generalEvent("touch", "touchend"), 326 generalEvent("touch", "touchcancel"), 327 ], 328 }, 329 { 330 name: "WebSocket", 331 items: [ 332 webSocketEvent("websocket", "open"), 333 webSocketEvent("websocket", "message"), 334 webSocketEvent("websocket", "error"), 335 webSocketEvent("websocket", "close"), 336 ], 337 }, 338 { 339 name: "Worker", 340 items: [ 341 workerEvent("message"), 342 workerEvent("messageerror"), 343 344 // Service Worker events. 345 globalEvent("serviceworker", "fetch"), 346 ], 347 }, 348 { 349 name: "XHR", 350 items: [ 351 xhrEvent("xhr", "readystatechange"), 352 xhrEvent("xhr", "load"), 353 xhrEvent("xhr", "loadstart"), 354 xhrEvent("xhr", "loadend"), 355 xhrEvent("xhr", "abort"), 356 xhrEvent("xhr", "error"), 357 xhrEvent("xhr", "progress"), 358 xhrEvent("xhr", "timeout"), 359 ], 360 }, 361 ]; 362 363 const FLAT_EVENTS = []; 364 for (const category of AVAILABLE_BREAKPOINTS) { 365 for (const event of category.items) { 366 FLAT_EVENTS.push(event); 367 } 368 } 369 const EVENTS_BY_ID = {}; 370 for (const event of FLAT_EVENTS) { 371 if (EVENTS_BY_ID[event.id]) { 372 throw new Error("Duplicate event ID detected: " + event.id); 373 } 374 EVENTS_BY_ID[event.id] = event; 375 } 376 377 const SIMPLE_EVENTS = {}; 378 const DOM_EVENTS = {}; 379 for (const eventBP of FLAT_EVENTS) { 380 if (eventBP.type === "simple") { 381 const { notificationType } = eventBP; 382 if (SIMPLE_EVENTS[notificationType]) { 383 throw new Error("Duplicate simple event"); 384 } 385 SIMPLE_EVENTS[notificationType] = eventBP.id; 386 } else if (eventBP.type === "event") { 387 const { eventType, targetTypes } = eventBP; 388 389 if (!Array.isArray(targetTypes) || !targetTypes.length) { 390 throw new Error("Expect a targetTypes array for each event definition"); 391 } 392 393 for (const targetType of targetTypes) { 394 let byEventType = DOM_EVENTS[targetType]; 395 if (!byEventType) { 396 byEventType = {}; 397 DOM_EVENTS[targetType] = byEventType; 398 } 399 400 if (byEventType[eventType]) { 401 throw new Error("Duplicate dom event: " + eventType); 402 } 403 byEventType[eventType] = eventBP.id; 404 } 405 } else if (eventBP.type === "script") { 406 // Nothing to do. 407 } else { 408 throw new Error("Unknown type: " + eventBP.type); 409 } 410 } 411 412 exports.eventBreakpointForNotification = eventBreakpointForNotification; 413 function eventBreakpointForNotification(dbg, notification) { 414 const notificationType = notification.type; 415 416 if (notification.type === "domEvent") { 417 const domEventNotification = DOM_EVENTS[notification.targetType]; 418 if (!domEventNotification) { 419 return null; 420 } 421 422 // The 'event' value is a cross-compartment wrapper for the DOM Event object. 423 // While we could use that directly in the main thread as an Xray wrapper, 424 // when debugging workers we can't, because it is an opaque wrapper. 425 // To make things work, we have to always interact with the Event object via 426 // the Debugger.Object interface. 427 const evt = dbg 428 .makeGlobalObjectReference(notification.global) 429 .makeDebuggeeValue(notification.event); 430 431 const eventType = evt.getProperty("type").return; 432 const id = domEventNotification[eventType]; 433 if (!id) { 434 return null; 435 } 436 const eventBreakpoint = EVENTS_BY_ID[id]; 437 438 // Does some additional checks for media events to ensure the DOM Event 439 // was fired on either <audio> or <video> tags. 440 if (eventBreakpoint.isMediaEvent) { 441 const currentTarget = evt.getProperty("currentTarget").return; 442 if (!currentTarget) { 443 return null; 444 } 445 446 const nodeType = currentTarget.getProperty("nodeType").return; 447 const namespaceURI = currentTarget.getProperty("namespaceURI").return; 448 if ( 449 nodeType !== 1 /* ELEMENT_NODE */ || 450 namespaceURI !== "http://www.w3.org/1999/xhtml" 451 ) { 452 return null; 453 } 454 455 const nodeName = currentTarget 456 .getProperty("nodeName") 457 .return.toLowerCase(); 458 if (nodeName !== "audio" && nodeName !== "video") { 459 return null; 460 } 461 } 462 463 return id; 464 } 465 466 return SIMPLE_EVENTS[notificationType] || null; 467 } 468 469 exports.makeEventBreakpointMessage = makeEventBreakpointMessage; 470 function makeEventBreakpointMessage(id) { 471 return EVENTS_BY_ID[id].message; 472 } 473 474 exports.firstStatementBreakpointId = firstStatementBreakpointId; 475 function firstStatementBreakpointId() { 476 return SCRIPT_FIRST_STATEMENT_BREAKPOINT.id; 477 } 478 479 exports.eventsRequireNotifications = eventsRequireNotifications; 480 function eventsRequireNotifications(ids) { 481 for (const id of ids) { 482 const eventBreakpoint = EVENTS_BY_ID[id]; 483 484 // Script events are implemented directly in the server and do not require 485 // notifications from Gecko, so there is no need to watch for them. 486 if (eventBreakpoint && eventBreakpoint.type !== "script") { 487 return true; 488 } 489 } 490 return false; 491 } 492 493 exports.getAvailableEventBreakpoints = getAvailableEventBreakpoints; 494 /** 495 * Get all available event breakpoints 496 * 497 * @param {Window|WorkerGlobalScope} global 498 * @returns {Array<object>} An array containing object with a few properties : 499 * - {String} id: unique identifier 500 * - {String} name: Description for the event to be displayed in UI (no translated) 501 * - {String} type: Either "simple" or "event" 502 * Only for type="simple": 503 * - {String} notificationType: platform name of the event 504 * Only for type="event": 505 * - {String} eventType: platform name of the event 506 * - {Array<String>} targetTypes: List of potential target on which the event is fired. 507 * Can be "global", "node", "xhr", "worker",... 508 */ 509 function getAvailableEventBreakpoints(global) { 510 const available = []; 511 for (const { name, items } of AVAILABLE_BREAKPOINTS) { 512 available.push({ 513 name, 514 events: items 515 .filter(item => !item.condition || item.condition(global)) 516 .map(item => ({ 517 id: item.id, 518 519 // The name to be displayed in UI 520 name: item.name, 521 522 // The type of event: either simple or event 523 type: item.type, 524 525 // For type=simple 526 notificationType: item.notificationType, 527 528 // For type=event 529 eventType: item.eventType, 530 targetTypes: item.targetTypes, 531 })), 532 }); 533 } 534 return available; 535 } 536 exports.validateEventBreakpoint = validateEventBreakpoint; 537 function validateEventBreakpoint(id) { 538 return !!EVENTS_BY_ID[id]; 539 }