Bug 1545724 - Add support for javascriptDialog APIs in Page domain r=remote-protocol-reviewers,whimboo,ochameau

Depends on D37168

Differential Revision: https://phabricator.services.mozilla.com/D37368

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Julian Descottes 2019-08-01 12:25:57 +00:00
Родитель 2986dc047b
Коммит e64c2ab141
10 изменённых файлов: 518 добавлений и 0 удалений

Просмотреть файл

@ -6,13 +6,62 @@
var EXPORTED_SYMBOLS = ["Page"];
const { DialogHandler } = ChromeUtils.import(
"chrome://remote/content/domains/parent/page/DialogHandler.jsm"
);
const { Domain } = ChromeUtils.import(
"chrome://remote/content/domains/Domain.jsm"
);
class Page extends Domain {
constructor(session) {
super(session);
this._onDialogLoaded = this._onDialogLoaded.bind(this);
this.enabled = false;
}
destructor() {
// Flip a flag to avoid to disable the content domain from this.disable()
this._isDestroyed = false;
this.disable();
super.destructor();
}
// commands
async enable() {
if (this.enabled) {
return;
}
this.enabled = true;
const { browser } = this.session.target;
this._dialogHandler = new DialogHandler(browser);
this._dialogHandler.on("dialog-loaded", this._onDialogLoaded);
await this.executeInChild("enable");
}
async disable() {
if (!this.enabled) {
return;
}
this._dialogHandler.destructor();
this._dialogHandler = null;
this.enabled = false;
if (!this._isDestroyed) {
// Only call disable in the content domain if we are not destroying the domain.
// If we are destroying the domain, the content domains will be destroyed
// independently after firing the remote:destroy event.
await this.executeInChild("disable");
}
}
bringToFront() {
const { browser } = this.session.target;
const navigator = browser.ownerGlobal;
@ -24,4 +73,40 @@ class Page extends Domain {
// Select the corresponding tab
gBrowser.selectedTab = gBrowser.getTabForBrowser(browser);
}
/**
* Interact with the currently opened JavaScript dialog (alert, confirm,
* prompt) for this page. This will always close the dialog, either accepting
* or rejecting it, with the optional prompt filled.
*
* @param {Object}
* - {Boolean} accept: For "confirm", "prompt", "beforeunload" dialogs
* true will accept the dialog, false will cancel it. For "alert"
* dialogs, true or false closes the dialog in the same way.
* - {String} promptText: for "prompt" dialogs, used to fill the prompt
* input.
*/
async handleJavaScriptDialog({ accept, promptText }) {
if (!this.enabled) {
throw new Error("Page domain is not enabled");
}
await this._dialogHandler.handleJavaScriptDialog({ accept, promptText });
}
/**
* Emit the proper CDP event javascriptDialogOpening when a javascript dialog
* opens for the current target.
*/
_onDialogLoaded(e, data) {
const { message, type } = data;
// XXX: We rely on the tabmodal-dialog-loaded event (see DialogHandler.jsm)
// which is inconsistent with the name "javascriptDialogOpening".
// For correctness we should rely on an event fired _before_ the prompt is
// visible, such as DOMWillOpenModalDialog. However the payload of this
// event does not contain enough data to populate javascriptDialogOpening.
//
// Since the event is fired asynchronously, this should not have an impact
// on the actual tests relying on this API.
this.emit("Page.javascriptDialogOpening", { message, type });
}
}

Просмотреть файл

@ -0,0 +1,117 @@
/* 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 = ["DialogHandler"];
const { EventEmitter } = ChromeUtils.import(
"resource://gre/modules/EventEmitter.jsm"
);
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
const DIALOG_TYPES = {
ALERT: "alert",
BEFOREUNLOAD: "beforeunload",
CONFIRM: "confirm",
PROMPT: "prompt",
};
/**
* Helper dedicated to detect and interact with browser dialogs such as `alert`,
* `confirm` etc. The current implementation only supports tabmodal dialogs,
* not full window dialogs.
*
* Emits "dialog-loaded" when a javascript dialog is opened for the current
* browser.
*
* @param {BrowserElement} browser
*/
class DialogHandler {
constructor(browser) {
EventEmitter.decorate(this);
this._dialog = null;
this._browser = browser;
this._onTabDialogLoaded = this._onTabDialogLoaded.bind(this);
Services.obs.addObserver(this._onTabDialogLoaded, "tabmodal-dialog-loaded");
}
destructor() {
this._dialog = null;
this._pageTarget = null;
Services.obs.removeObserver(
this._onTabDialogLoaded,
"tabmodal-dialog-loaded"
);
}
async handleJavaScriptDialog({ accept, promptText }) {
if (!this._dialog) {
throw new Error("No dialog available for handleJavaScriptDialog");
}
const type = this._getDialogType();
if (promptText && type === "prompt") {
this._dialog.ui.loginTextbox.value = promptText;
}
const onDialogClosed = new Promise(r => {
this._browser.addEventListener("DOMModalDialogClosed", r, {
once: true,
});
});
// 0 corresponds to the OK callback, 1 to the CANCEL callback.
if (accept) {
this._dialog.onButtonClick(0);
} else {
this._dialog.onButtonClick(1);
}
await onDialogClosed;
// Resetting dialog to null here might be racy and lead to errors if the
// content page is triggering several prompts in a row.
// See Bug 1569578.
this._dialog = null;
}
_getDialogType() {
const { inPermitUnload, promptType } = this._dialog.args;
if (inPermitUnload) {
return DIALOG_TYPES.BEFOREUNLOAD;
}
switch (promptType) {
case "alert":
return DIALOG_TYPES.ALERT;
case "confirm":
return DIALOG_TYPES.CONFIRM;
case "prompt":
return DIALOG_TYPES.PROMPT;
default:
throw new Error("Unsupported dialog type: " + promptType);
}
}
_onTabDialogLoaded(promptContainer) {
const prompts = this._browser.tabModalPromptBox.listPrompts();
const prompt = prompts.find(p => p.ui.promptContainer === promptContainer);
if (!prompt) {
// The dialog is not for the current tab.
return;
}
this._dialog = prompt;
const message = this._dialog.args.text;
const type = this._getDialogType();
this.emit("dialog-loaded", { message, type });
}
}

Просмотреть файл

@ -50,6 +50,7 @@ remote.jar:
content/domains/parent/Network.jsm (domains/parent/Network.jsm)
content/domains/parent/network/NetworkObserver.jsm (domains/parent/network/NetworkObserver.jsm)
content/domains/parent/Page.jsm (domains/parent/Page.jsm)
content/domains/parent/page/DialogHandler.jsm (domains/parent/page/DialogHandler.jsm)
content/domains/parent/Target.jsm (domains/parent/Target.jsm)
content/domains/parent/target/TabManager.jsm (domains/parent/target/TabManager.jsm)

Просмотреть файл

@ -25,6 +25,11 @@ skip-if = fission
skip-if = fission
[browser_page_frameNavigated_iframe.js]
skip-if = fission
[browser_page_javascriptDialog_alert.js]
[browser_page_javascriptDialog_beforeunload.js]
[browser_page_javascriptDialog_confirm.js]
[browser_page_javascriptDialog_otherTarget.js]
[browser_page_javascriptDialog_prompt.js]
[browser_page_runtime_events.js]
skip-if = fission
[browser_runtime_callFunctionOn.js]

Просмотреть файл

@ -0,0 +1,69 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const TEST_URI = "data:text/html;charset=utf-8,default-test-page";
// Test a browser alert is detected via Page.javascriptDialogOpening and can be
// closed with Page.handleJavaScriptDialog
add_task(async function() {
const { client, tab } = await setupTestForUri(TEST_URI);
const { Page } = client;
info("Enable the page domain");
await Page.enable();
info("Set window.alertIsClosed to false in the content page");
await ContentTask.spawn(gBrowser.selectedBrowser, null, () => {
// This boolean will be flipped after closing the dialog
content.alertIsClosed = false;
});
info("Create an alert dialog again");
const { message, type } = await createAlertDialog(Page);
is(type, "alert", "dialog event contains the correct type");
is(message, "test-1234", "dialog event contains the correct text");
info("Close the dialog with accept:false");
await Page.handleJavaScriptDialog({ accept: false });
info("Retrieve the alertIsClosed boolean on the content window");
let alertIsClosed = await getContentProperty("alertIsClosed");
ok(alertIsClosed, "The content process is no longer blocked on the alert");
info("Reset window.alertIsClosed to false in the content page");
await ContentTask.spawn(gBrowser.selectedBrowser, null, () => {
content.alertIsClosed = false;
});
info("Create an alert dialog again");
await createAlertDialog(Page);
info("Close the dialog with accept:true");
await Page.handleJavaScriptDialog({ accept: true });
alertIsClosed = await getContentProperty("alertIsClosed");
ok(alertIsClosed, "The content process is no longer blocked on the alert");
await client.close();
ok(true, "The client is closed");
BrowserTestUtils.removeTab(tab);
await RemoteAgent.close();
});
function createAlertDialog(Page) {
const onDialogOpen = Page.javascriptDialogOpening();
info("Trigger an alert in the test page");
ContentTask.spawn(gBrowser.selectedBrowser, null, () => {
content.alert("test-1234");
// Flip a boolean in the content page to check if the content process resumed
// after the alert was opened.
content.alertIsClosed = true;
});
return onDialogOpen;
}

Просмотреть файл

@ -0,0 +1,63 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const TEST_URI = "data:text/html;charset=utf-8,default-test-page";
// Test beforeunload dialog events.
add_task(async function() {
info("Allow to trigger onbeforeunload without user interaction");
await new Promise(resolve => {
const options = {
set: [["dom.require_user_interaction_for_beforeunload", false]],
};
SpecialPowers.pushPrefEnv(options, resolve);
});
const { client, tab } = await setupTestForUri(TEST_URI);
const { Page } = client;
info("Enable the page domain");
await Page.enable();
info("Attach a valid onbeforeunload handler");
await ContentTask.spawn(gBrowser.selectedBrowser, null, () => {
content.onbeforeunload = () => true;
});
info("Trigger the beforeunload again but reject the prompt");
const { type } = await triggerBeforeUnload(Page, tab, false);
is(type, "beforeunload", "dialog event contains the correct type");
info("Trigger the beforeunload again and accept the prompt");
const onTabClose = BrowserTestUtils.waitForEvent(tab, "TabClose");
await triggerBeforeUnload(Page, tab, true);
info("Wait for the TabClose event");
await onTabClose;
await client.close();
ok(true, "The client is closed");
await RemoteAgent.close();
});
function triggerBeforeUnload(Page, tab, accept) {
// We use then here because after clicking on the close button, nothing
// in the main block of the function will be executed until the prompt
// is accepted or rejected. Attaching a then to this promise still works.
const onDialogOpen = Page.javascriptDialogOpening().then(
async dialogEvent => {
await Page.handleJavaScriptDialog({ accept });
return dialogEvent;
}
);
info("Click on the tab close icon");
tab.closeButton.click();
return onDialogOpen;
}

Просмотреть файл

@ -0,0 +1,53 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const TEST_URI = "data:text/html;charset=utf-8,default-test-page";
// Test for window.confirm(). Check that the dialog is correctly detected and that it can
// be rejected or accepted.
add_task(async function() {
const { client, tab } = await setupTestForUri(TEST_URI);
const { Page } = client;
info("Enable the page domain");
await Page.enable();
info("Create a confirm dialog to open");
const { message, type } = await createConfirmDialog(Page);
is(type, "confirm", "dialog event contains the correct type");
is(message, "confirm-1234?", "dialog event contains the correct text");
info("Accept the dialog");
await Page.handleJavaScriptDialog({ accept: true });
let isConfirmed = await getContentProperty("isConfirmed");
ok(isConfirmed, "The confirm dialog was accepted");
await createConfirmDialog(Page);
info("Trigger another confirm in the test page");
info("Reject the dialog");
await Page.handleJavaScriptDialog({ accept: false });
isConfirmed = await getContentProperty("isConfirmed");
ok(!isConfirmed, "The confirm dialog was rejected");
await client.close();
ok(true, "The client is closed");
BrowserTestUtils.removeTab(tab);
await RemoteAgent.close();
});
function createConfirmDialog(Page) {
const onDialogOpen = Page.javascriptDialogOpening();
info("Trigger a confirm in the test page");
ContentTask.spawn(gBrowser.selectedBrowser, null, () => {
content.isConfirmed = content.confirm("confirm-1234?");
});
return onDialogOpen;
}

Просмотреть файл

@ -0,0 +1,58 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const TEST_URI = "data:text/html;charset=utf-8,default-test-page";
const OTHER_URI = "data:text/html;charset=utf-8,other-test-page";
// Test that javascript dialog events are emitted by the page domain only if
// the dialog is created for the window of the target.
add_task(async function() {
const { client, tab } = await setupTestForUri(TEST_URI);
const { Page } = client;
info("Enable the page domain");
await Page.enable();
// Add a listener for dialogs on the test page.
Page.javascriptDialogOpening(() => {
ok(false, "Should never receive this event");
});
info("Open another tab");
const otherTab = await BrowserTestUtils.openNewForegroundTab(
gBrowser,
OTHER_URI
);
is(gBrowser.selectedTab, otherTab, "Selected tab is now the new tab");
// Create a promise that resolve when dialog prompt is created.
// It will also take care of closing the dialog.
const onOtherPageDialog = new Promise(r => {
Services.obs.addObserver(function onDialogLoaded(promptContainer) {
Services.obs.removeObserver(onDialogLoaded, "tabmodal-dialog-loaded");
promptContainer.querySelector(".tabmodalprompt-button0").click();
r();
}, "tabmodal-dialog-loaded");
});
info("Trigger an alert in the second page");
ContentTask.spawn(gBrowser.selectedBrowser, null, () => {
content.alert("test");
});
info("Wait for the alert to be detected and closed");
await onOtherPageDialog;
info("Call bringToFront on the test page to make sure we received");
await Page.bringToFront();
BrowserTestUtils.removeTab(otherTab);
await client.close();
ok(true, "The client is closed");
BrowserTestUtils.removeTab(tab);
await RemoteAgent.close();
});

Просмотреть файл

@ -0,0 +1,55 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const TEST_URI = "data:text/html;charset=utf-8,default-test-page";
// Test for window.prompt(). Check that the dialog is correctly detected and that it can
// be rejected or accepted, with a custom prompt text.
add_task(async function() {
const { client, tab } = await setupTestForUri(TEST_URI);
const { Page } = client;
info("Enable the page domain");
await Page.enable();
info("Create a prompt dialog to open");
const { message, type } = await createPromptDialog(Page);
is(type, "prompt", "dialog event contains the correct type");
is(message, "prompt-1234", "dialog event contains the correct text");
info("Accept the prompt");
await Page.handleJavaScriptDialog({ accept: true, promptText: "some-text" });
let promptResult = await getContentProperty("promptResult");
is(promptResult, "some-text", "The prompt text was correctly applied");
await createPromptDialog(Page);
info("Trigger another prompt in the test page");
info("Reject the prompt");
await Page.handleJavaScriptDialog({ accept: false, promptText: "new-text" });
promptResult = await getContentProperty("promptResult");
ok(!promptResult, "The prompt dialog was rejected");
await client.close();
ok(true, "The client is closed");
BrowserTestUtils.removeTab(tab);
await RemoteAgent.close();
});
function createPromptDialog(Page) {
const onDialogOpen = Page.javascriptDialogOpening();
info("Trigger a prompt in the test page");
ContentTask.spawn(gBrowser.selectedBrowser, null, () => {
content.promptResult = content.prompt("prompt-1234");
});
return onDialogOpen;
}

Просмотреть файл

@ -127,3 +127,15 @@ async function setupTestForUri(uri) {
ok(true, "CDP client has been instantiated");
return { client, tab };
}
/**
* Retrieve the value of a property on the content window.
*/
function getContentProperty(prop) {
info(`Retrieve ${prop} on the content window`);
return ContentTask.spawn(
gBrowser.selectedBrowser,
prop,
_prop => content[_prop]
);
}