summaryrefslogtreecommitdiffstats
path: root/testing/marionette/dom.js
diff options
context:
space:
mode:
Diffstat (limited to 'testing/marionette/dom.js')
-rw-r--r--testing/marionette/dom.js215
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;