Commit aed2bec5 authored by Julian Descottes's avatar Julian Descottes
Browse files

Bug 1713442 - [remote] Support events in windowglobal MessageHandlers r=webdriver-reviewers,whimboo

parent 1e11a69a
Loading
Loading
Loading
Loading
+22 −0
Original line number Diff line number Diff line
@@ -88,6 +88,28 @@ class MessageHandler extends EventEmitter {
    this.emit("message-handler-destroyed", this);
  }

  /**
   * Emit a message-handler-event. Such events should bubble up to the root of
   * a MessageHandler network.
   *
   * @param {String} method
   *     A string literal of the form [module name].[event name]. This is the
   *     event name.
   * @param {Object} params
   *     The event parameters.
   */
  emitMessageHandlerEvent(method, params) {
    this.emit("message-handler-event", {
      // TODO: The messageHandlerInfo needs to be wrapped in the event so
      // that consumers can check the type/context. Once MessageHandlerRegistry
      // becomes context-specific (Bug 1722659), only the sessionId will be
      // required.
      messageHandlerInfo: this._messageHandlerInfo,
      method,
      params,
    });
  }

  /**
   * @typedef {Object} CommandDestination
   * @property {String} type - One of MessageHandler.type.
+39 −2
Original line number Diff line number Diff line
@@ -11,6 +11,8 @@ const { XPCOMUtils } = ChromeUtils.import(
);

XPCOMUtils.defineLazyModuleGetters(this, {
  EventEmitter: "resource://gre/modules/EventEmitter.jsm",

  Log: "chrome://remote/content/shared/Log.jsm",
  MessageHandlerInfo:
    "chrome://remote/content/shared/messagehandler/MessageHandlerInfo.jsm",
@@ -61,8 +63,10 @@ function getMessageHandlerClass(type) {
 *
 * Note: this is still created as a class, but exposed as a singleton.
 */
class MessageHandlerRegistryClass {
class MessageHandlerRegistryClass extends EventEmitter {
  constructor() {
    super();

    /**
     * Map of all message handlers registered in this process.
     * Keys are based on session id, message handler type and message handler
@@ -75,6 +79,7 @@ class MessageHandlerRegistryClass {
    this._onMessageHandlerDestroyed = this._onMessageHandlerDestroyed.bind(
      this
    );
    this._onMessageHandlerEvent = this._onMessageHandlerEvent.bind(this);
  }

  /**
@@ -154,6 +159,30 @@ class MessageHandlerRegistryClass {
    return messageHandler;
  }

  /**
   * Retrieve an already registered RootMessageHandler instance matching the
   * provided sessionId.
   *
   * @param {String} sessionId
   *     ID of the session the handler is used for.
   * @return {RootMessageHandler}
   *     A RootMessageHandler instance.
   * @throws {Error}
   *     If no root MessageHandler can be found for the provided session id.
   */
  getRootMessageHandler(sessionId) {
    const rootMessageHandler = this.getExistingMessageHandler(
      sessionId,
      RootMessageHandler.type
    );
    if (!rootMessageHandler) {
      throw new Error(
        `Unable to find a root MessageHandler for session id ${sessionId}`
      );
    }
    return rootMessageHandler;
  }

  toString() {
    return `[object ${this.constructor.name}]`;
  }
@@ -182,6 +211,7 @@ class MessageHandlerRegistryClass {
      "message-handler-destroyed",
      this._onMessageHandlerDestroyed
    );
    messageHandler.on("message-handler-event", this._onMessageHandlerEvent);
    return messageHandler;
  }

@@ -198,13 +228,20 @@ class MessageHandlerRegistryClass {

  // Event handlers

  _onMessageHandlerDestroyed(evt, messageHandler) {
  _onMessageHandlerDestroyed(eventName, messageHandler) {
    messageHandler.off(
      "message-handler-destroyed",
      this._onMessageHandlerDestroyed
    );
    messageHandler.off("message-handler-event", this._onMessageHandlerEvent);
    this._unregisterMessageHandler(messageHandler);
  }

  _onMessageHandlerEvent(eventName, messageHandlerEvent) {
    // The registry simply re-emits MessageHandler events so that consumers
    // don't have to attach listeners to individual MessageHandler instances.
    this.emit("message-handler-registry-event", messageHandlerEvent);
  }
}

const MessageHandlerRegistry = new MessageHandlerRegistryClass();
+2 −46
Original line number Diff line number Diff line
@@ -6,37 +6,10 @@
add_task(async function test_broadcasting_with_frames() {
  info("Navigate the initial tab to the test URL");
  const tab = gBrowser.selectedTab;

  // Create a test page with 2 iframes:
  // - one with a different eTLD+1 (example.com)
  // - one with a nested iframe on a different eTLD+1 (example.net)
  //
  // Overall the document structure should look like:
  //
  // html (example.org)
  //   iframe (example.org)
  //     iframe (example.net)
  //   iframe(example.com)
  //
  // Which means we should have 4 browsing contexts in total.

  // Create the markup for an example.net frame nested in an example.com frame.
  const NESTED_FRAME_MARKUP = createFrameForUri(
    `http://example.org/document-builder.sjs?html=${createFrame("example.net")}`
  );

  // Combine the nested frame markup created above with an example.com frame.
  const TEST_URI_MARKUP = `${NESTED_FRAME_MARKUP}${createFrame("example.com")}`;

  // Create the test page URI on example.org.
  const TEST_URI = `http://example.org/document-builder.sjs?html=${encodeURI(
    TEST_URI_MARKUP
  )}`;

  await loadURL(tab.linkedBrowser, TEST_URI);
  await loadURL(tab.linkedBrowser, createTestMarkupWithFrames());

  const contexts = tab.linkedBrowser.browsingContext.getAllBrowsingContextsInSubtree();
  is(contexts.length, 4, "Test tab has 3 children contexts");
  is(contexts.length, 4, "Test tab has 3 children contexts (4 in total)");

  const rootMessageHandler = createRootMessageHandler(
    "session-id-broadcasting_with_frames"
@@ -63,20 +36,3 @@ add_task(async function test_broadcasting_with_frames() {

  rootMessageHandler.destroy();
});

/**
 * Create inline markup for a simple iframe that can be used with
 * document-builder.sjs. The iframe will be served under the provided domain.
 *
 * @param {String} domain
 *     A domain (eg "example.com"), compatible with build/pgo/server-locations.txt
 */
function createFrame(domain) {
  return createFrameForUri(
    `http://${domain}/document-builder.sjs?html=frame-${domain}`
  );
}

function createFrameForUri(uri) {
  return `<iframe src="${encodeURI(uri)}"></iframe>`;
}
+1 −0
Original line number Diff line number Diff line
@@ -7,6 +7,7 @@ support-files =
prefs =
  remote.messagehandler.modulecache.useBrowserTestRoot=true

[browser_events.js]
[browser_handle_command_errors.js]
[browser_handle_simple_command.js]
[browser_registry.js]
+248 −0
Original line number Diff line number Diff line
/* Any copyright is dedicated to the Public Domain.
 * http://creativecommons.org/publicdomain/zero/1.0/ */

"use strict";

const { MessageHandlerRegistry } = ChromeUtils.import(
  "chrome://remote/content/shared/messagehandler/MessageHandlerRegistry.jsm"
);
const { RootMessageHandler } = ChromeUtils.import(
  "chrome://remote/content/shared/messagehandler/RootMessageHandler.jsm"
);
const { WindowGlobalMessageHandler } = ChromeUtils.import(
  "chrome://remote/content/shared/messagehandler/WindowGlobalMessageHandler.jsm"
);

/**
 * Emit an event from a WindowGlobal module triggered by a specific command.
 * Check that the event is emitted on the RootMessageHandler as well as on
 * the parent process MessageHandlerRegistry.
 */
add_task(async function test_event() {
  const tab = BrowserTestUtils.addTab(
    gBrowser,
    "http://example.com/document-builder.sjs?html=tab"
  );
  await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
  const browsingContext = tab.linkedBrowser.browsingContext;

  const rootMessageHandler = createRootMessageHandler("session-id-event");

  const onTestEvent = rootMessageHandler.once("message-handler-event");
  // MessageHandlerRegistry should forward all the message-handler-events.
  const onRegistryEvent = MessageHandlerRegistry.once(
    "message-handler-registry-event"
  );

  callTestEmitEvent(rootMessageHandler, browsingContext.id);

  const messageHandlerEvent = await onTestEvent;
  is(
    messageHandlerEvent.method,
    "event.testEvent",
    "Received event.testEvent on the ROOT MessageHandler"
  );
  is(
    messageHandlerEvent.params.text,
    `event from ${browsingContext.id}`,
    "Received the expected data in testEvent"
  );
  const registryEvent = await onRegistryEvent;
  is(
    registryEvent,
    messageHandlerEvent,
    "The event forwarded by the MessageHandlerRegistry is identical to the MessageHandler event"
  );

  rootMessageHandler.destroy();
  gBrowser.removeTab(tab);
});

/**
 * Emit an event from a Root module triggered by a specific command.
 * Check that the event is emitted on the RootMessageHandler.
 */
add_task(async function test_root_event() {
  const rootMessageHandler = createRootMessageHandler("session-id-root_event");

  const onTestEvent = rootMessageHandler.once("message-handler-event");
  rootMessageHandler.handleCommand({
    moduleName: "event",
    commandName: "testEmitRootEvent",
    destination: {
      type: RootMessageHandler.type,
    },
  });

  const { method, params } = await onTestEvent;
  is(
    method,
    "event.testRootEvent",
    "Received event.testRootEvent on the ROOT MessageHandler"
  );
  is(
    params.text,
    "event from root",
    "Received the expected payload in testRootEvent"
  );

  rootMessageHandler.destroy();
});

/**
 * Emit an event from a windowglobal-in-root module triggered by a specific command.
 * Check that the event is emitted on the RootMessageHandler.
 */
add_task(async function test_windowglobal_in_root_event() {
  const tab = BrowserTestUtils.addTab(
    gBrowser,
    "http://example.com/document-builder.sjs?html=tab"
  );
  await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
  const browsingContext = tab.linkedBrowser.browsingContext;

  const rootMessageHandler = createRootMessageHandler(
    "session-id-windowglobal_in_root_event"
  );

  const onTestEvent = rootMessageHandler.once("message-handler-event");
  rootMessageHandler.handleCommand({
    moduleName: "event",
    commandName: "testEmitWindowGlobalInRootEvent",
    destination: {
      type: WindowGlobalMessageHandler.type,
      id: browsingContext.id,
    },
  });

  const { method, params } = await onTestEvent;
  is(
    method,
    "event.testWindowGlobalInRootEvent",
    "Received event.testWindowGlobalInRoot on the ROOT MessageHandler"
  );
  is(
    params.text,
    `windowglobal-in-root event for ${browsingContext.id}`,
    "Received the expected payload in testWindowGlobalInRoot"
  );

  rootMessageHandler.destroy();
  gBrowser.removeTab(tab);
});

/**
 * Emit an event from a windowglobal module, but from 2 different sessions.
 * Check that the event is emitted by the corresponding RootMessageHandler as
 * well as by the parent process MessageHandlerRegistry.
 */
add_task(async function test_event_multisession() {
  const tab = BrowserTestUtils.addTab(
    gBrowser,
    "http://example.com/document-builder.sjs?html=tab"
  );
  await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
  const browsingContextId = tab.linkedBrowser.browsingContext.id;

  const root1 = createRootMessageHandler("session-id-event_multisession-1");
  let root1Events = 0;
  const onRoot1Event = function(evtName, wrappedEvt) {
    if (wrappedEvt.method === "event.testEvent") {
      root1Events++;
    }
  };
  root1.on("message-handler-event", onRoot1Event);

  const root2 = createRootMessageHandler("session-id-event_multisession-2");
  let root2Events = 0;
  const onRoot2Event = function(evtName, wrappedEvt) {
    if (wrappedEvt.method === "event.testEvent") {
      root2Events++;
    }
  };
  root2.on("message-handler-event", onRoot2Event);

  let registryEvents = 0;
  const onRegistryEvent = function(evtName, wrappedEvt) {
    if (wrappedEvt.method === "event.testEvent") {
      registryEvents++;
    }
  };
  MessageHandlerRegistry.on("message-handler-registry-event", onRegistryEvent);

  callTestEmitEvent(root1, browsingContextId);
  callTestEmitEvent(root2, browsingContextId);

  info("Wait for root1 event to be received");
  await TestUtils.waitForCondition(() => root1Events === 1);
  info("Wait for root2 event to be received");
  await TestUtils.waitForCondition(() => root2Events === 1);

  await TestUtils.waitForTick();
  is(root1Events, 1, "Session 1 only received 1 event");
  is(root2Events, 1, "Session 2 only received 1 event");
  is(
    registryEvents,
    2,
    "MessageHandlerRegistry forwarded events from both sessions"
  );

  root1.off("message-handler-event", onRoot1Event);
  root2.off("message-handler-event", onRoot2Event);
  MessageHandlerRegistry.off("message-handler-registry-event", onRegistryEvent);
  root1.destroy();
  root2.destroy();
  gBrowser.removeTab(tab);
});

/**
 * Test that events can be emitted from individual frame contexts and that
 * events going through a shared content process MessageHandlerRegistry are not
 * duplicated.
 */
add_task(async function test_event_with_frames() {
  info("Navigate the initial tab to the test URL");
  const tab = gBrowser.selectedTab;
  await loadURL(tab.linkedBrowser, createTestMarkupWithFrames());

  const contexts = tab.linkedBrowser.browsingContext.getAllBrowsingContextsInSubtree();
  is(contexts.length, 4, "Test tab has 3 children contexts (4 in total)");

  const rootMessageHandler = createRootMessageHandler(
    "session-id-event_with_frames"
  );

  let rootEvents = [];
  const onRootEvent = function(evtName, wrappedEvt) {
    if (wrappedEvt.method === "event.testEvent") {
      rootEvents.push(wrappedEvt.params.text);
    }
  };
  rootMessageHandler.on("message-handler-event", onRootEvent);

  for (const context of contexts) {
    callTestEmitEvent(rootMessageHandler, context.id);
    info("Wait for root event to be received");
    await TestUtils.waitForCondition(() =>
      rootEvents.includes(`event from ${context.id}`)
    );
  }

  info("Wait for a bit and check that we did not receive duplicated events");
  await TestUtils.waitForTick();
  is(rootEvents.length, 4, "Only received 4 events");

  rootMessageHandler.off("message-handler-event", onRootEvent);
  rootMessageHandler.destroy();
});

function callTestEmitEvent(rootMessageHandler, browsingContextId) {
  rootMessageHandler.handleCommand({
    moduleName: "event",
    commandName: "testEmitEvent",
    destination: {
      type: WindowGlobalMessageHandler.type,
      id: browsingContextId,
    },
  });
}
Loading