зеркало из https://github.com/mozilla/gecko-dev.git
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:
Родитель
2986dc047b
Коммит
e64c2ab141
|
@ -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]
|
||||
);
|
||||
}
|
||||
|
|
Загрузка…
Ссылка в новой задаче