Commit 5404dba9 authored by Olivia Hall's avatar Olivia Hall
Browse files

Bug 1712414 - GeckoView Prompts Adjustment for Marionette r=agi

Added prompts to geckoview.js window to collect active prompts using the added
GeckoViewPrompterChild and GeckoViewPrompterParent.
Added a way to get and set the prompt message text, callback, and
accept prompt(), confirm(), and alert().

Changed confirm-different-origin-frame.sub.html.ini and
prompt-different-origin-frame.sub.html.ini to expect ERROR on
Android because the prior GeckoView test runner was dismissing or
canceling prompts automatically due to not having a prompt delegate.

Differential Revision: https://phabricator.services.mozilla.com/D133927
parent 24f56775
Loading
Loading
Loading
Loading
+78 −0
Original line number Diff line number Diff line
/* 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";

var EXPORTED_SYMBOLS = ["GeckoViewPrompterChild"];

const { GeckoViewActorChild } = ChromeUtils.import(
  "resource://gre/modules/GeckoViewActorChild.jsm"
);

class GeckoViewPrompterChild extends GeckoViewActorChild {
  constructor() {
    super();
    this._prompts = new Map();
  }

  registerPrompt(prompt) {
    this._prompts.set(prompt.id, prompt);
    this.sendAsyncMessage("RegisterPrompt", {
      id: prompt.id,
      promptType: prompt.getPromptType(),
    });
  }

  unregisterPrompt(prompt) {
    this._prompts.delete(prompt.id);
    this.sendAsyncMessage("UnregisterPrompt", {
      id: prompt.id,
    });
  }

  notifyPromptShow(prompt) {
    this.sendAsyncMessage("NotifyPromptShow", {
      id: prompt.id,
    });
  }

  /**
   * Handles the message coming from GeckoViewPrompterParent.
   *
   * @param   {string} message.name The subject of the message.
   * @param   {object} message.data The data of the message.
   */
  async receiveMessage({ name, data }) {
    const prompt = this._prompts.get(data.id);
    if (!prompt) {
      // Unknown prompt, probably for a different child actor.
      return;
    }
    switch (name) {
      case "GetPromptText": {
        // eslint-disable-next-line consistent-return
        return prompt.getPromptText();
      }
      case "GetInputText": {
        // eslint-disable-next-line consistent-return
        return prompt.getInputText();
      }
      case "SetInputText": {
        prompt.setInputText(data.text);
        break;
      }
      case "AcceptPrompt": {
        prompt.accept();
        break;
      }
      case "DismissPrompt": {
        prompt.dismiss();
        break;
      }
      default: {
        break;
      }
    }
  }
}
+164 −0
Original line number Diff line number Diff line
/* 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";

var EXPORTED_SYMBOLS = ["GeckoViewPrompterParent"];

const { GeckoViewActorParent } = ChromeUtils.import(
  "resource://gre/modules/GeckoViewActorParent.jsm"
);

const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");

const DIALOGS = [
  "alert",
  "alertCheck",
  "confirm",
  "confirmCheck",
  "prompt",
  "promptCheck",
];

class GeckoViewPrompterParent extends GeckoViewActorParent {
  constructor() {
    super();
    this._prompts = new Map();
  }

  get rootActor() {
    return this.browsingContext.top.currentWindowGlobal.getActor(
      "GeckoViewPrompter"
    );
  }

  registerPrompt(promptId, promptType, actor) {
    return this._prompts.set(
      promptId,
      new RemotePrompt(promptId, promptType, actor)
    );
  }

  unregisterPrompt(promptId) {
    this._prompts.delete(promptId);
  }

  notifyPromptShow(promptId) {
    // ToDo: Bug 1761480 - GeckoView can send additional prompts to Marionette
    if (this._prompts.get(promptId).isDialog) {
      Services.obs.notifyObservers({ id: promptId }, "geckoview-prompt-show");
    }
  }

  getPrompts() {
    const self = this;
    const prompts = [];
    // Marionette expects this event to be fired from the parent
    const dialogClosedEvent = new CustomEvent("DOMModalDialogClosed", {
      cancelable: true,
      bubbles: true,
    });
    for (const [, prompt] of this._prompts) {
      // Adding only WebDriver compliant dialogs to the window
      if (prompt.isDialog) {
        prompts.push({
          args: {
            modalType: "GeckoViewPrompter",
            promptType: prompt.type,
            isDialog: prompt.isDialog,
          },
          setInputText(text) {
            prompt.setInputText(text);
          },
          async getPromptText() {
            return prompt.getPromptText();
          },
          async getInputText() {
            return prompt.getInputText();
          },
          acceptPrompt() {
            prompt.acceptPrompt();
            self.window.dispatchEvent(dialogClosedEvent);
          },
          dismissPrompt() {
            prompt.dismissPrompt();
            self.window.dispatchEvent(dialogClosedEvent);
          },
        });
      }
    }
    return prompts;
  }

  /**
   * Handles the message coming from GeckoViewPrompterChild.
   *
   * @param   {string} message.name The subject of the message.
   * @param   {object} message.data The data of the message.
   */
  // eslint-disable-next-line consistent-return
  async receiveMessage({ name, data }) {
    switch (name) {
      case "RegisterPrompt": {
        this.rootActor.registerPrompt(data.id, data.promptType, this);
        break;
      }
      case "UnregisterPrompt": {
        this.rootActor.unregisterPrompt(data.id);
        break;
      }
      case "NotifyPromptShow": {
        this.rootActor.notifyPromptShow(data.id);
        break;
      }
      default: {
        return super.receiveMessage({ name, data });
      }
    }
  }
}

class RemotePrompt {
  constructor(id, type, actor) {
    this.id = id;
    this.type = type;
    this.actor = actor;
  }

  // Checks if the prompt conforms to a WebDriver simple dialog.
  get isDialog() {
    return DIALOGS.includes(this.type);
  }

  getPromptText() {
    return this.actor.sendQuery("GetPromptText", {
      id: this.id,
    });
  }

  getInputText() {
    return this.actor.sendQuery("GetInputText", {
      id: this.id,
    });
  }

  setInputText(inputText) {
    this.actor.sendAsyncMessage("SetInputText", {
      id: this.id,
      text: inputText,
    });
  }

  acceptPrompt() {
    this.actor.sendAsyncMessage("AcceptPrompt", {
      id: this.id,
    });
  }

  dismissPrompt() {
    this.actor.sendAsyncMessage("DismissPrompt", {
      id: this.id,
    });
  }
}
+2 −0
Original line number Diff line number Diff line
@@ -13,6 +13,8 @@ FINAL_TARGET_FILES.actors += [
    "GeckoViewContentParent.jsm",
    "GeckoViewFormValidationChild.jsm",
    "GeckoViewPromptChild.jsm",
    "GeckoViewPrompterChild.jsm",
    "GeckoViewPrompterParent.jsm",
    "GeckoViewSettingsChild.jsm",
    "LoadURIDelegateChild.jsm",
    "MediaControlDelegateChild.jsm",
+21 −0
Original line number Diff line number Diff line
@@ -747,6 +747,23 @@ function startup() {
        },
      },
    },
    {
      name: "GeckoViewPrompter",
      onInit: {
        actors: {
          GeckoViewPrompter: {
            parent: {
              moduleURI: "resource:///actors/GeckoViewPrompterParent.jsm",
            },
            child: {
              moduleURI: "resource:///actors/GeckoViewPrompterChild.jsm",
            },
            allFrames: true,
            includeChrome: true,
          },
        },
      },
    },
  ]);

  if (!Services.appinfo.sessionHistoryInParent) {
@@ -767,6 +784,10 @@ function startup() {
  // Allows actors to access ModuleManager.
  window.moduleManager = ModuleManager;

  window.prompts = () => {
    return window.ModuleManager.getActor("GeckoViewPrompter").getPrompts();
  };

  Services.tm.dispatchToMainThread(() => {
    // This should always be the first thing we do here - any additional delayed
    // initialisation tasks should be added between "browser-delayed-startup-finished"
+60 −0
Original line number Diff line number Diff line
@@ -52,6 +52,11 @@ class GeckoViewPrompter {
    return this._domWin;
  }

  get prompterActor() {
    const actor = this.domWin?.windowGlobalChild.getActor("GeckoViewPrompter");
    return actor;
  }

  _changeModalState(aEntering) {
    if (!this._domWin) {
      // Allow not having a DOM window.
@@ -82,6 +87,54 @@ class GeckoViewPrompter {
    return false;
  }

  accept(aInputText = this.inputText) {
    if (this.callback) {
      let acceptMsg = {};
      switch (this.message.type) {
        case "alert":
          acceptMsg = null;
          break;
        case "button":
          acceptMsg.button = 0;
          break;
        case "text":
          acceptMsg.text = aInputText;
          break;
        default:
          acceptMsg = null;
          break;
      }
      this.callback(acceptMsg);
      // Notify the UI that this prompt should be hidden.
      this.dismiss();
    }
  }

  getPromptType() {
    switch (this.message.type) {
      case "alert":
        return this.message.checkValue ? "alertCheck" : "alert";
      case "button":
        return this.message.checkValue ? "confirmCheck" : "confirm";
      case "text":
        return this.message.checkValue ? "promptCheck" : "prompt";
      default:
        return this.message.type;
    }
  }

  getPromptText() {
    return this.message.msg;
  }

  getInputText() {
    return this.inputText;
  }

  setInputText(aInput) {
    this.inputText = aInput;
  }

  /**
   * Shows a native prompt, and then spins the event loop for this thread while we wait
   * for a response
@@ -124,10 +177,16 @@ class GeckoViewPrompter {

  dismiss() {
    this._dispatcher.dispatch("GeckoView:Prompt:Dismiss", { id: this.id });
    this.prompterActor?.unregisterPrompt(this);
  }

  asyncShowPrompt(aMsg, aCallback) {
    let handled = false;
    this.message = aMsg;
    this.inputText = aMsg.value;
    this.callback = aCallback;
    this.prompterActor?.registerPrompt(this);

    const onResponse = response => {
      if (handled) {
        return;
@@ -161,5 +220,6 @@ class GeckoViewPrompter {
        onResponse(null);
      },
    });
    this.prompterActor?.notifyPromptShow(this);
  }
}
Loading