diff options
Diffstat (limited to 'testing/marionette/dom.js')
-rw-r--r-- | testing/marionette/dom.js | 215 |
1 files changed, 215 insertions, 0 deletions
diff --git a/testing/marionette/dom.js b/testing/marionette/dom.js new file mode 100644 index 0000000000..ff90e35a1d --- /dev/null +++ b/testing/marionette/dom.js @@ -0,0 +1,215 @@ +/* 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"; + +const EXPORTED_SYMBOLS = [ + "ContentEventObserverService", + "WebElementEventTarget", +]; + +const { XPCOMUtils } = ChromeUtils.import( + "resource://gre/modules/XPCOMUtils.jsm" +); + +XPCOMUtils.defineLazyModuleGetters(this, { + Log: "chrome://marionette/content/log.js", +}); + +XPCOMUtils.defineLazyGetter(this, "logger", () => Log.get()); + +/** + * The ``EventTarget`` for web elements can be used to observe DOM + * events in the content document. + * + * A caveat of the current implementation is that it is only possible + * to listen for top-level ``window`` global events. + * + * It needs to be backed by a :js:class:`ContentEventObserverService` + * in a content frame script. + * + * Usage:: + * + * let observer = new WebElementEventTarget(messageManager); + * await new Promise(resolve => { + * observer.addEventListener("visibilitychange", resolve, {once: true}); + * chromeWindow.minimize(); + * }); + */ +class WebElementEventTarget { + /** + * @param {function(): nsIMessageListenerManager} messageManagerFn + * Message manager to the current browser. + */ + constructor(messageManager) { + this.mm = messageManager; + this.listeners = {}; + this.mm.addMessageListener("Marionette:DOM:OnEvent", this); + } + + /** + * Register an event handler of a specific event type from the content + * frame. + * + * @param {string} type + * Event type to listen for. + * @param {EventListener} listener + * Object which receives a notification (a ``BareEvent``) + * when an event of the specified type occurs. This must be + * an object implementing the ``EventListener`` interface, + * or a JavaScript function. + * @param {boolean=} once + * Indicates that the ``listener`` should be invoked at + * most once after being added. If true, the ``listener`` + * would automatically be removed when invoked. + */ + addEventListener(type, listener, { once = false } = {}) { + if (!(type in this.listeners)) { + this.listeners[type] = []; + } + + if (!this.listeners[type].includes(listener)) { + listener.once = once; + this.listeners[type].push(listener); + } + + this.mm.sendAsyncMessage("Marionette:DOM:AddEventListener", { type }); + } + + /** + * Removes an event listener. + * + * @param {string} type + * Type of event to cease listening for. + * @param {EventListener} listener + * Event handler to remove from the event target. + */ + removeEventListener(type, listener) { + if (!(type in this.listeners)) { + return; + } + + let stack = this.listeners[type]; + for (let i = stack.length - 1; i >= 0; --i) { + if (stack[i] === listener) { + stack.splice(i, 1); + if (stack.length == 0) { + this.mm.sendAsyncMessage("Marionette:DOM:RemoveEventListener", { + type, + }); + } + return; + } + } + } + + dispatchEvent(event) { + if (!(event.type in this.listeners)) { + return; + } + + event.target = this; + + let stack = this.listeners[event.type].slice(0); + stack.forEach(listener => { + if (typeof listener.handleEvent == "function") { + listener.handleEvent(event); + } else { + listener(event); + } + + if (listener.once) { + this.removeEventListener(event.type, listener); + } + }); + } + + receiveMessage({ name, data }) { + if (name != "Marionette:DOM:OnEvent") { + return; + } + + let ev = { + type: data.type, + }; + this.dispatchEvent(ev); + } +} +this.WebElementEventTarget = WebElementEventTarget; + +/** + * Provides the frame script backend for the + * :js:class:`WebElementEventTarget`. + * + * This service receives requests for new DOM events to listen for and + * to cease listening for, and despatches IPC messages to the browser + * when they fire. + */ +class ContentEventObserverService { + /** + * @param {WindowProxy} windowGlobal + * Window. + * @param {nsIMessageSender.sendAsyncMessage} sendAsyncMessage + * Function for sending an async message to the parent browser. + */ + constructor(windowGlobal, sendAsyncMessage) { + this.window = windowGlobal; + this.sendAsyncMessage = sendAsyncMessage; + this.events = new Set(); + } + + /** + * Observe a new DOM event. + * + * When the DOM event of ``type`` fires, a message is passed to + * the parent browser's event observer. + * + * If event type is already being observed, only a single message + * is sent. E.g. multiple registration for events will only ever emit + * a maximum of one message. + * + * @param {string} type + * DOM event to listen for. + */ + add(type) { + if (this.events.has(type)) { + return; + } + this.window.addEventListener(type, this); + this.events.add(type); + } + + /** + * Ceases observing a DOM event. + * + * @param {string} type + * DOM event to stop listening for. + */ + remove(type) { + if (!this.events.has(type)) { + return; + } + this.window.removeEventListener(type, this); + this.events.delete(type); + } + + /** Ceases observing all previously registered DOM events. */ + clear() { + for (let ev of this) { + this.remove(ev); + } + } + + *[Symbol.iterator]() { + for (let ev of this.events) { + yield ev; + } + } + + handleEvent({ type, target }) { + logger.trace(`Received DOM event ${type}`); + this.sendAsyncMessage("Marionette:DOM:OnEvent", { type }); + } +} +this.ContentEventObserverService = ContentEventObserverService; |