diff options
Diffstat (limited to '')
21 files changed, 2026 insertions, 0 deletions
diff --git a/browser/base/content/test/tabPrompts/auth-route.sjs b/browser/base/content/test/tabPrompts/auth-route.sjs new file mode 100644 index 0000000000..4f113a8add --- /dev/null +++ b/browser/base/content/test/tabPrompts/auth-route.sjs @@ -0,0 +1,28 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +function handleRequest(request, response) { + let body; + // guest:guest + let expectedHeader = "Basic Z3Vlc3Q6Z3Vlc3Q="; + // correct login credentials provided + if ( + request.hasHeader("Authorization") && + request.getHeader("Authorization") == expectedHeader + ) { + response.setStatusLine(request.httpVersion, 200, "OK, authorized"); + response.setHeader("Content-Type", "text", false); + + body = "success"; + } else { + // incorrect credentials + response.setStatusLine(request.httpVersion, 401, "Unauthorized"); + response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false); + response.setHeader("Content-Type", "text", false); + + body = "failed"; + } + response.bodyOutputStream.write(body, body.length); +} diff --git a/browser/base/content/test/tabPrompts/browser.ini b/browser/base/content/test/tabPrompts/browser.ini new file mode 100644 index 0000000000..2834b6d2af --- /dev/null +++ b/browser/base/content/test/tabPrompts/browser.ini @@ -0,0 +1,30 @@ +[browser_abort_when_in_modal_state.js] +[browser_auth_spoofing_protection.js] +support-files = + redirect-crossDomain.html + redirect-sameDomain.html + auth-route.sjs +[browser_auth_spoofing_url_copy.js] +support-files = + redirect-crossDomain.html + auth-route.sjs +[browser_auth_spoofing_url_drag_and_drop.js] +support-files = + redirect-crossDomain.html + redirect-sameDomain.html + auth-route.sjs +[browser_beforeunload_urlbar.js] +support-files = file_beforeunload_stop.html +[browser_closeTabSpecificPanels.js] +skip-if = verify && debug && (os == 'linux') +[browser_confirmFolderUpload.js] +[browser_contentOrigins.js] +support-files = file_beforeunload_stop.html +[browser_multiplePrompts.js] +[browser_openPromptInBackgroundTab.js] +https_first_disabled = true +support-files = openPromptOffTimeout.html +[browser_promptFocus.js] +[browser_prompt_closed_window.js] +[browser_switchTabPermissionPrompt.js] +[browser_windowPrompt.js] diff --git a/browser/base/content/test/tabPrompts/browser_abort_when_in_modal_state.js b/browser/base/content/test/tabPrompts/browser_abort_when_in_modal_state.js new file mode 100644 index 0000000000..cb3a1f72d6 --- /dev/null +++ b/browser/base/content/test/tabPrompts/browser_abort_when_in_modal_state.js @@ -0,0 +1,60 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { PromiseTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PromiseTestUtils.sys.mjs" +); + +/** + * Check that if we're using a window-modal prompt, + * the next synchronous window-internal modal prompt aborts rather than + * leaving us in a deadlock about how to enter modal state. + */ +add_task(async function test_check_multiple_prompts() { + await SpecialPowers.pushPrefEnv({ + set: [["prompts.windowPromptSubDialog", true]], + }); + let container = document.getElementById("window-modal-dialog"); + let dialogPromise = BrowserTestUtils.promiseAlertDialogOpen(); + + let firstDialogClosedPromise = new Promise(resolve => { + // Avoid blocking the test on the (sync) alert by sticking it in a timeout: + setTimeout(() => { + Services.prompt.alertBC( + window.browsingContext, + Ci.nsIPrompt.MODAL_TYPE_WINDOW, + "Some title", + "some message" + ); + resolve(); + }, 0); + }); + let dialogWin = await dialogPromise; + + // Check circumstances of opening. + ok( + !dialogWin.docShell.chromeEventHandler, + "Should not have embedded the dialog." + ); + + PromiseTestUtils.expectUncaughtRejection(/could not be shown/); + let rv = Services.prompt.confirm( + window, + "I should not appear", + "because another prompt was open" + ); + is(rv, false, "Prompt should have been canceled."); + + info("Accepting dialog"); + dialogWin.document.querySelector("dialog").acceptDialog(); + + await firstDialogClosedPromise; + + await BrowserTestUtils.waitForMutationCondition( + container, + { childList: true, attributes: true }, + () => !container.hasChildNodes() && !container.open + ); +}); diff --git a/browser/base/content/test/tabPrompts/browser_auth_spoofing_protection.js b/browser/base/content/test/tabPrompts/browser_auth_spoofing_protection.js new file mode 100644 index 0000000000..ca139df8e4 --- /dev/null +++ b/browser/base/content/test/tabPrompts/browser_auth_spoofing_protection.js @@ -0,0 +1,232 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +let TEST_PATH = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" +); + +let TEST_PATH_AUTH = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.org" +); + +const CROSS_DOMAIN_URL = TEST_PATH + "redirect-crossDomain.html"; + +const SAME_DOMAIN_URL = TEST_PATH + "redirect-sameDomain.html"; + +const AUTH_URL = TEST_PATH_AUTH + "auth-route.sjs"; + +/** + * Opens a new tab with a url that ether redirects us cross or same domain + * + * @param {Boolean} doConfirmPrompt - true if we want to test the case when the user accepts the prompt, + * false if we want to test the case when the user cancels the prompt. + * @param {Boolean} crossDomain - if true we will open a url that redirects us to a cross domain url, + * if false, we will open a url that redirects us to a same domain url + * @param {Boolean} prefEnabled true will enable "privacy.authPromptSpoofingProtection", + * false will disable the pref + */ +async function trigger401AndHandle(doConfirmPrompt, crossDomain, prefEnabled) { + await SpecialPowers.pushPrefEnv({ + set: [["privacy.authPromptSpoofingProtection", prefEnabled]], + }); + let url = crossDomain ? CROSS_DOMAIN_URL : SAME_DOMAIN_URL; + let dialogShown = waitForDialog(doConfirmPrompt, crossDomain, prefEnabled); + await BrowserTestUtils.withNewTab(url, async function () { + await dialogShown; + }); + await new Promise(resolve => { + Services.clearData.deleteData( + Ci.nsIClearDataService.CLEAR_AUTH_CACHE, + resolve + ); + }); +} + +async function waitForDialog(doConfirmPrompt, crossDomain, prefEnabled) { + await TestUtils.topicObserved("common-dialog-loaded"); + let dialog = gBrowser.getTabDialogBox(gBrowser.selectedBrowser) + ._tabDialogManager._topDialog; + let dialogDocument = dialog._frame.contentDocument; + if (crossDomain) { + if (prefEnabled) { + Assert.equal( + dialog._overlay.getAttribute("hideContent"), + "true", + "Dialog overlay hides the current sites content" + ); + Assert.equal( + window.gURLBar.value, + AUTH_URL, + "Correct location is provided by the prompt" + ); + Assert.equal( + window.gBrowser.selectedTab.label, + "example.org", + "Tab title is manipulated" + ); + // switch to another tab and make sure we dont mess up this new tabs url bar and tab title + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "https://example.org:443" + ); + Assert.equal( + window.gURLBar.value, + "https://example.org", + "No location is provided by the prompt, correct location is displayed" + ); + Assert.equal( + window.gBrowser.selectedTab.label, + "mochitest index /", + "Tab title is not manipulated" + ); + // switch back to our tab with the prompt and make sure the url bar state and tab title is still there + BrowserTestUtils.removeTab(tab); + Assert.equal( + window.gURLBar.value, + AUTH_URL, + "Correct location is provided by the prompt" + ); + Assert.equal( + window.gBrowser.selectedTab.label, + "example.org", + "Tab title is manipulated" + ); + // make sure a value that the user types in has a higher priority than our prompts location + gBrowser.selectedBrowser.userTypedValue = "user value"; + gURLBar.setURI(); + Assert.equal( + window.gURLBar.value, + "user value", + "User typed value is shown" + ); + // if the user clears the url bar we again fall back to the location of the prompt if we trigger setURI by a tab switch + gBrowser.selectedBrowser.userTypedValue = ""; + gURLBar.setURI(null, true); + Assert.equal( + window.gURLBar.value, + AUTH_URL, + "Correct location is provided by the prompt" + ); + // Cross domain and pref is not enabled + } else { + Assert.equal( + dialog._overlay.getAttribute("hideContent"), + "", + "Dialog overlay does not hide the current sites content" + ); + Assert.equal( + window.gURLBar.value, + CROSS_DOMAIN_URL, + "No location is provided by the prompt, correct location is displayed" + ); + Assert.equal( + window.gBrowser.selectedTab.label, + "example.com", + "Tab title is not manipulated" + ); + } + // same domain + } else { + Assert.equal( + dialog._overlay.getAttribute("hideContent"), + "", + "Dialog overlay does not hide the current sites content" + ); + Assert.equal( + window.gURLBar.value, + SAME_DOMAIN_URL, + "No location is provided by the prompt, correct location is displayed" + ); + Assert.equal( + window.gBrowser.selectedTab.label, + "example.com", + "Tab title is not manipulated" + ); + } + + let onDialogClosed = BrowserTestUtils.waitForEvent( + window, + "DOMModalDialogClosed" + ); + if (doConfirmPrompt) { + dialogDocument.getElementById("loginTextbox").value = "guest"; + dialogDocument.getElementById("password1Textbox").value = "guest"; + dialogDocument.getElementById("commonDialog").acceptDialog(); + } else { + dialogDocument.getElementById("commonDialog").cancelDialog(); + } + + // wait for the dialog to be closed to check that the URLBar state is reset + await onDialogClosed; + // Due to bug 1812014, the url bar will be clear if we have set its value to "" while the prompt was open + // so we trigger a tab switch again to have the uri displayed to be able to check its value + gURLBar.setURI(null, true); + Assert.equal( + window.gURLBar.value, + crossDomain ? CROSS_DOMAIN_URL : SAME_DOMAIN_URL, + "No location is provided by the prompt" + ); + Assert.equal( + window.gBrowser.selectedTab.label, + "example.com", + "Tab title is not manipulated" + ); +} + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["privacy.authPromptSpoofingProtection", true]], + }); +}); + +/** + * Tests that the 401 auth spoofing mechanisms apply if the 401 is from a different base domain than the current sites, + * canceling the prompt + */ +add_task(async function testCrossDomainCancelPrefEnabled() { + await trigger401AndHandle(false, true, true); +}); + +/** + * Tests that the 401 auth spoofing mechanisms apply if the 401 is from a different base domain than the current sites, + * accepting the prompt + */ +add_task(async function testCrossDomainAcceptPrefEnabled() { + await trigger401AndHandle(true, true, true); +}); + +/** + * Tests that the 401 auth spoofing mechanisms do not apply if "privacy.authPromptSpoofingProtection" is not set to true + * canceling the prompt + */ +add_task(async function testCrossDomainCancelPrefDisabled() { + await trigger401AndHandle(false, true, false); +}); + +/** + * Tests that the 401 auth spoofing mechanisms do not apply if "privacy.authPromptSpoofingProtection" is not set to true, + * accepting the prompt + */ +add_task(async function testCrossDomainAcceptPrefDisabled() { + await trigger401AndHandle(true, true, false); +}); + +/** + * Tests that the 401 auth spoofing mechanisms are not triggered by a 401 within the same base domain as the current sites, + * canceling the prompt + */ +add_task(async function testSameDomainCancelPrefEnabled() { + await trigger401AndHandle(false, false, true); +}); + +/** + * Tests that the 401 auth spoofing mechanisms are not triggered by a 401 within the same base domain as the current sites, + * accepting the prompt + */ +add_task(async function testSameDomainAcceptPrefEnabled() { + await trigger401AndHandle(true, false, true); +}); diff --git a/browser/base/content/test/tabPrompts/browser_auth_spoofing_url_copy.js b/browser/base/content/test/tabPrompts/browser_auth_spoofing_url_copy.js new file mode 100644 index 0000000000..5bea05020e --- /dev/null +++ b/browser/base/content/test/tabPrompts/browser_auth_spoofing_url_copy.js @@ -0,0 +1,95 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +let TEST_PATH = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" +); + +let TEST_PATH_AUTH = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.org" +); + +const CROSS_DOMAIN_URL = TEST_PATH + "redirect-crossDomain.html"; + +const AUTH_URL = TEST_PATH_AUTH + "auth-route.sjs"; + +/** + * Opens a new tab with a url that redirects us cross domain + * tests that auth anti-spoofing mechanisms cover url copy while prompt is open + * + */ +async function trigger401AndHandle() { + await SpecialPowers.pushPrefEnv({ + set: [["privacy.authPromptSpoofingProtection", true]], + }); + let dialogShown = waitForDialogAndCopyURL(); + await BrowserTestUtils.withNewTab(CROSS_DOMAIN_URL, async function () { + await dialogShown; + }); + await new Promise(resolve => { + Services.clearData.deleteData( + Ci.nsIClearDataService.CLEAR_AUTH_CACHE, + resolve + ); + }); +} + +async function waitForDialogAndCopyURL() { + await TestUtils.topicObserved("common-dialog-loaded"); + let dialog = gBrowser.getTabDialogBox(gBrowser.selectedBrowser) + ._tabDialogManager._topDialog; + let dialogDocument = dialog._frame.contentDocument; + + //select the whole URL + gURLBar.focus(); + await SimpleTest.promiseClipboardChange(AUTH_URL, () => { + Assert.equal(gURLBar.value, AUTH_URL, "url bar copy value set"); + gURLBar.select(); + goDoCommand("cmd_copy"); + }); + + // select only part of the URL + gURLBar.focus(); + let endOfSelectionRange = AUTH_URL.indexOf("/auth-route.sjs"); + await SimpleTest.promiseClipboardChange( + AUTH_URL.substring(0, endOfSelectionRange), + () => { + Assert.equal(gURLBar.value, AUTH_URL, "url bar copy value set"); + gURLBar.selectionStart = 0; + gURLBar.selectionEnd = endOfSelectionRange; + goDoCommand("cmd_copy"); + } + ); + let onDialogClosed = BrowserTestUtils.waitForEvent( + window, + "DOMModalDialogClosed" + ); + dialogDocument.getElementById("commonDialog").cancelDialog(); + + await onDialogClosed; + Assert.equal( + window.gURLBar.value, + CROSS_DOMAIN_URL, + "No location is provided by the prompt" + ); + + //select the whole URL after URL is reset to normal + gURLBar.focus(); + await SimpleTest.promiseClipboardChange(CROSS_DOMAIN_URL, () => { + Assert.equal(gURLBar.value, CROSS_DOMAIN_URL, "url bar copy value set"); + gURLBar.select(); + goDoCommand("cmd_copy"); + }); +} + +/** + * Tests that the 401 auth spoofing mechanisms covers the url bar copy action properly, + * canceling the prompt + */ +add_task(async function testUrlCopy() { + await trigger401AndHandle(); +}); diff --git a/browser/base/content/test/tabPrompts/browser_auth_spoofing_url_drag_and_drop.js b/browser/base/content/test/tabPrompts/browser_auth_spoofing_url_drag_and_drop.js new file mode 100644 index 0000000000..564f1fa9a7 --- /dev/null +++ b/browser/base/content/test/tabPrompts/browser_auth_spoofing_url_drag_and_drop.js @@ -0,0 +1,93 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +let TEST_PATH = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" +); + +let TEST_PATH_AUTH = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.org" +); + +const CROSS_DOMAIN_URL = TEST_PATH + "redirect-crossDomain.html"; + +const SAME_DOMAIN_URL = TEST_PATH + "redirect-sameDomain.html"; + +const AUTH_URL = TEST_PATH_AUTH + "auth-route.sjs"; + +/** + * Opens a new tab with a url that ether redirects us cross or same domain + * + * @param {Boolean} crossDomain - if true we will open a url that redirects us to a cross domain url, + * if false, we will open a url that redirects us to a same domain url + */ +async function trigger401AndHandle(crossDomain) { + let dialogShown = waitForDialogAndDragNDropURL(crossDomain); + await BrowserTestUtils.withNewTab( + crossDomain ? CROSS_DOMAIN_URL : SAME_DOMAIN_URL, + async function () { + await dialogShown; + } + ); + await new Promise(resolve => { + Services.clearData.deleteData( + Ci.nsIClearDataService.CLEAR_AUTH_CACHE, + resolve + ); + }); +} + +async function waitForDialogAndDragNDropURL(crossDomain) { + await TestUtils.topicObserved("common-dialog-loaded"); + let dialog = gBrowser.getTabDialogBox(gBrowser.selectedBrowser) + ._tabDialogManager._topDialog; + let dialogDocument = dialog._frame.contentDocument; + + let urlbar = document.getElementById("urlbar-input"); + let dataTran = new DataTransfer(); + let urlEvent = new DragEvent("dragstart", { dataTransfer: dataTran }); + let urlBarContainer = document.getElementById("urlbar-input-container"); + urlBarContainer.click(); + // trigger a drag event in the gUrlBar + urlbar.dispatchEvent(urlEvent); + // this should set some propperties on our event, like the url we are dragging + if (crossDomain) { + is( + urlEvent.dataTransfer.getData("text/plain"), + AUTH_URL, + "correct cross Domain URL is dragged over" + ); + } else { + is( + urlEvent.dataTransfer.getData("text/plain"), + SAME_DOMAIN_URL, + "correct same domain URL is dragged over" + ); + } + + let onDialogClosed = BrowserTestUtils.waitForEvent( + window, + "DOMModalDialogClosed" + ); + dialogDocument.getElementById("commonDialog").cancelDialog(); + + await onDialogClosed; +} + +/** + * Tests that the 401 auth spoofing mechanisms covers the url bar drag and drop action propperly, + */ +add_task(async function testUrlDragAndDrop() { + await trigger401AndHandle(true); +}); + +/** + * Tests that the 401 auth spoofing mechanisms do not apply to the url bar drag and drop action if the 401 is not from a different base domain, + */ +add_task(async function testUrlDragAndDrop() { + await trigger401AndHandle(false); +}); diff --git a/browser/base/content/test/tabPrompts/browser_beforeunload_urlbar.js b/browser/base/content/test/tabPrompts/browser_beforeunload_urlbar.js new file mode 100644 index 0000000000..cb53783c99 --- /dev/null +++ b/browser/base/content/test/tabPrompts/browser_beforeunload_urlbar.js @@ -0,0 +1,75 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_ROOT = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://example.com" +); + +const CONTENT_PROMPT_SUBDIALOG = Services.prefs.getBoolPref( + "prompts.contentPromptSubDialog", + false +); + +add_task(async function test_beforeunload_stay_clears_urlbar() { + await SpecialPowers.pushPrefEnv({ + set: [["dom.require_user_interaction_for_beforeunload", false]], + }); + + const TEST_URL = TEST_ROOT + "file_beforeunload_stop.html"; + await BrowserTestUtils.withNewTab(TEST_URL, async function (browser) { + gURLBar.focus(); + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + const inputValue = "http://example.org/?q=typed"; + gURLBar.inputField.value = inputValue.slice(0, -1); + EventUtils.sendString(inputValue.slice(-1)); + + if (CONTENT_PROMPT_SUBDIALOG) { + let promptOpenedPromise = + BrowserTestUtils.promiseAlertDialogOpen("cancel"); + EventUtils.synthesizeKey("VK_RETURN"); + await promptOpenedPromise; + await TestUtils.waitForTick(); + } else { + let promptOpenedPromise = TestUtils.topicObserved( + "tabmodal-dialog-loaded" + ); + EventUtils.synthesizeKey("VK_RETURN"); + await promptOpenedPromise; + let promptElement = browser.parentNode.querySelector("tabmodalprompt"); + + // Click the cancel button + promptElement.querySelector(".tabmodalprompt-button1").click(); + await TestUtils.waitForCondition( + () => promptElement.parentNode == null, + "tabprompt should be removed" + ); + } + + // Can't just compare directly with TEST_URL because the URL may be trimmed. + // Just need it to not be the example.org thing we typed in. + ok( + gURLBar.value.endsWith("_stop.html"), + "Url bar should be reset to point to the stop html file" + ); + ok( + gURLBar.value.includes("example.com"), + "Url bar should be reset to example.com" + ); + // Check the lock/identity icons are back: + is( + gURLBar.textbox.getAttribute("pageproxystate"), + "valid", + "Should be in valid pageproxy state." + ); + + // Now we need to get rid of the handler to avoid the prompt coming up when trying to close the + // tab when we exit `withNewTab`. :-) + await SpecialPowers.spawn(browser, [], function () { + content.window.onbeforeunload = null; + }); + }); +}); diff --git a/browser/base/content/test/tabPrompts/browser_closeTabSpecificPanels.js b/browser/base/content/test/tabPrompts/browser_closeTabSpecificPanels.js new file mode 100644 index 0000000000..8d789ad512 --- /dev/null +++ b/browser/base/content/test/tabPrompts/browser_closeTabSpecificPanels.js @@ -0,0 +1,53 @@ +"use strict"; + +/* + * This test creates multiple panels, one that has been tagged as specific to its tab's content + * and one that isn't. When a tab loses focus, panel specific to that tab should close. + * The non-specific panel should remain open. + * + */ + +add_task(async function () { + let tab1 = BrowserTestUtils.addTab(gBrowser, "http://mochi.test:8888/#0"); + let tab2 = BrowserTestUtils.addTab(gBrowser, "http://mochi.test:8888/#1"); + let specificPanel = document.createXULElement("panel"); + specificPanel.setAttribute("tabspecific", "true"); + specificPanel.setAttribute("noautohide", "true"); + let generalPanel = document.createXULElement("panel"); + generalPanel.setAttribute("noautohide", "true"); + let anchor = document.getElementById(CustomizableUI.AREA_NAVBAR); + + anchor.appendChild(specificPanel); + anchor.appendChild(generalPanel); + is(specificPanel.state, "closed", "specificPanel starts as closed"); + is(generalPanel.state, "closed", "generalPanel starts as closed"); + + let specificPanelPromise = BrowserTestUtils.waitForEvent( + specificPanel, + "popupshown" + ); + specificPanel.openPopupAtScreen(210, 210); + await specificPanelPromise; + is(specificPanel.state, "open", "specificPanel has been opened"); + + let generalPanelPromise = BrowserTestUtils.waitForEvent( + generalPanel, + "popupshown" + ); + generalPanel.openPopupAtScreen(510, 510); + await generalPanelPromise; + is(generalPanel.state, "open", "generalPanel has been opened"); + + gBrowser.tabContainer.advanceSelectedTab(-1, true); + is( + specificPanel.state, + "closed", + "specificPanel panel is closed after its tab loses focus" + ); + is(generalPanel.state, "open", "generalPanel is still open after tab switch"); + + specificPanel.remove(); + generalPanel.remove(); + gBrowser.removeTab(tab1); + gBrowser.removeTab(tab2); +}); diff --git a/browser/base/content/test/tabPrompts/browser_confirmFolderUpload.js b/browser/base/content/test/tabPrompts/browser_confirmFolderUpload.js new file mode 100644 index 0000000000..62b0ed4f2b --- /dev/null +++ b/browser/base/content/test/tabPrompts/browser_confirmFolderUpload.js @@ -0,0 +1,141 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { PromptTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PromptTestUtils.sys.mjs" +); + +/** + * Create a temporary test directory that will be cleaned up on test shutdown. + * @returns {String} - absolute directory path. + */ +function getTestDirectory() { + let tmpDir = Services.dirsvc.get("TmpD", Ci.nsIFile); + tmpDir.append("testdir"); + if (!tmpDir.exists()) { + tmpDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755); + registerCleanupFunction(() => { + tmpDir.remove(true); + }); + } + + let file1 = tmpDir.clone(); + file1.append("foo.txt"); + if (!file1.exists()) { + file1.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600); + } + + let file2 = tmpDir.clone(); + file2.append("bar.txt"); + if (!file2.exists()) { + file2.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600); + } + + return tmpDir.path; +} + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + // Allow using our MockFilePicker in the content process. + ["dom.filesystem.pathcheck.disabled", true], + ["dom.webkitBlink.dirPicker.enabled", true], + ], + }); +}); + +/** + * Create a file input, select a folder and wait for the upload confirmation + * prompt to open. + * @param {boolean} confirmUpload - Whether to accept (true) or cancel the + * prompt (false). + * @returns {Promise} - Resolves once the prompt has been closed. + */ +async function testUploadPrompt(confirmUpload) { + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + await BrowserTestUtils.withNewTab("http://example.com", async browser => { + // Create file input element + await ContentTask.spawn(browser, null, () => { + let input = content.document.createElement("input"); + input.id = "filepicker"; + input.setAttribute("type", "file"); + input.setAttribute("webkitdirectory", ""); + content.document.body.appendChild(input); + }); + + // If we're confirming the dialog, register a "change" listener on the + // file input. + let changePromise; + if (confirmUpload) { + changePromise = ContentTask.spawn(browser, null, async () => { + let input = content.document.getElementById("filepicker"); + return ContentTaskUtils.waitForEvent(input, "change").then( + e => e.target.files.length + ); + }); + } + + // Register prompt promise + let promptPromise = PromptTestUtils.waitForPrompt(browser, { + modalType: Services.prompt.MODAL_TYPE_TAB, + promptType: "confirmEx", + }); + + // Open filepicker + let path = getTestDirectory(); + await ContentTask.spawn(browser, { path }, args => { + let MockFilePicker = content.SpecialPowers.MockFilePicker; + MockFilePicker.init( + content, + "A Mock File Picker", + content.SpecialPowers.Ci.nsIFilePicker.modeGetFolder + ); + MockFilePicker.useDirectory(args.path); + + let input = content.document.getElementById("filepicker"); + input.click(); + }); + + // Wait for confirmation prompt + let prompt = await promptPromise; + ok(prompt, "Shown upload confirmation prompt"); + is(prompt.ui.button0.label, "Upload", "Accept button label"); + ok(prompt.ui.button1.hasAttribute("default"), "Cancel is default button"); + + // Close confirmation prompt + await PromptTestUtils.handlePrompt(prompt, { + buttonNumClick: confirmUpload ? 0 : 1, + }); + + // If we accepted, wait for the input elements "change" event + if (changePromise) { + let fileCount = await changePromise; + is(fileCount, 2, "Should have selected 2 files"); + } else { + let fileCount = await ContentTask.spawn(browser, null, () => { + return content.document.getElementById("filepicker").files.length; + }); + + is(fileCount, 0, "Should not have selected any files"); + } + + // Cleanup + await ContentTask.spawn(browser, null, () => { + content.SpecialPowers.MockFilePicker.cleanup(); + }); + }); +} + +// Tests the confirmation prompt that shows after the user picked a folder. + +// Confirm the prompt +add_task(async function test_confirm() { + await testUploadPrompt(true); +}); + +// Cancel the prompt +add_task(async function test_cancel() { + await testUploadPrompt(false); +}); diff --git a/browser/base/content/test/tabPrompts/browser_contentOrigins.js b/browser/base/content/test/tabPrompts/browser_contentOrigins.js new file mode 100644 index 0000000000..d39c8a3f67 --- /dev/null +++ b/browser/base/content/test/tabPrompts/browser_contentOrigins.js @@ -0,0 +1,217 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { PromptTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PromptTestUtils.sys.mjs" +); + +let { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +const TEST_ROOT = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" +); + +async function checkAlert( + pageToLoad, + expectedTitle, + expectedIcon = "chrome://global/skin/icons/defaultFavicon.svg" +) { + function openFn(browser) { + return SpecialPowers.spawn(browser, [], () => { + if (content.document.nodePrincipal.isSystemPrincipal) { + // Can't eval in privileged contexts due to CSP, just call directly: + content.alert("Test"); + } else { + // Eval everywhere else so it gets the principal of the loaded page. + content.eval("alert('Test')"); + } + }); + } + return checkDialog(pageToLoad, openFn, expectedTitle, expectedIcon); +} + +async function checkBeforeunload( + pageToLoad, + expectedTitle, + expectedIcon = "chrome://global/skin/icons/defaultFavicon.svg" +) { + async function openFn(browser) { + let tab = gBrowser.getTabForBrowser(browser); + await BrowserTestUtils.synthesizeMouseAtCenter( + "body", + {}, + browser.browsingContext + ); + return gBrowser.removeTab(tab); // trigger beforeunload. + } + return checkDialog(pageToLoad, openFn, expectedTitle, expectedIcon); +} + +async function checkDialog( + pageToLoad, + openFn, + expectedTitle, + expectedIcon, + modalType = Ci.nsIPrompt.MODAL_TYPE_CONTENT +) { + return BrowserTestUtils.withNewTab(pageToLoad, async browser => { + let promptPromise = PromptTestUtils.waitForPrompt(browser, { + modalType, + }); + let spawnPromise = openFn(browser); + let dialog = await promptPromise; + + let doc = dialog.ui.prompt.document; + let titleEl = doc.getElementById("titleText"); + if (expectedTitle.value) { + is(titleEl.textContent, expectedTitle.value, "Title should match."); + } else { + is( + titleEl.dataset.l10nId, + expectedTitle.l10nId, + "Title l10n id should match." + ); + } + ok( + !titleEl.parentNode.hasAttribute("overflown"), + "Title should fit without overflowing." + ); + + ok(BrowserTestUtils.is_visible(titleEl), "New title should be shown."); + ok( + BrowserTestUtils.is_hidden(doc.getElementById("infoTitle")), + "Old title should be hidden." + ); + let iconCS = doc.ownerGlobal.getComputedStyle( + doc.querySelector(".titleIcon") + ); + is( + iconCS.backgroundImage, + `url("${expectedIcon}")`, + "Icon is as expected." + ); + + // This is not particularly neat, but we want to also test overflow + // Our test systems don't have hosts that long, so just fake it: + if (browser.currentURI.asciiHost == "example.com") { + let longerDomain = "extravagantly.long.".repeat(10) + "example.com"; + doc.documentElement.setAttribute( + "headertitle", + JSON.stringify({ raw: longerDomain, shouldUseMaskFade: true }) + ); + info("Wait for the prompt title to update."); + await BrowserTestUtils.waitForMutationCondition( + titleEl, + { characterData: true, attributes: true }, + () => + titleEl.textContent == longerDomain && + titleEl.parentNode.hasAttribute("overflown") + ); + is(titleEl.textContent, longerDomain, "The longer domain is reflected."); + ok( + titleEl.parentNode.hasAttribute("overflown"), + "The domain should overflow." + ); + } + + // Close the prompt again. + await PromptTestUtils.handlePrompt(dialog); + // The alert in the content process was sync, we need to make sure it gets + // cleaned up, but couldn't await it above because that'd hang the test! + await spawnPromise; + }); +} + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["prompts.contentPromptSubDialog", true], + ["prompts.modalType.httpAuth", Ci.nsIPrompt.MODAL_TYPE_TAB], + ["prompts.tabChromePromptSubDialog", true], + ], + }); +}); + +add_task(async function test_check_prompt_origin_display() { + await checkAlert("https://example.com/", { value: "example.com" }); + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + await checkAlert("http://example.com/", { value: "example.com" }); + await checkAlert("data:text/html,<body>", { + l10nId: "common-dialog-title-null", + }); + + let homeDir = Services.dirsvc.get("Home", Ci.nsIFile); + let fileURI = Services.io.newFileURI(homeDir).spec; + await checkAlert(fileURI, { value: "file://" }); + + await checkAlert( + "about:config", + { l10nId: "common-dialog-title-system" }, + "chrome://branding/content/icon32.png" + ); + + await checkBeforeunload(TEST_ROOT + "file_beforeunload_stop.html", { + value: "example.com", + }); +}); + +add_task(async function test_check_auth() { + let server = new HttpServer(); + registerCleanupFunction(() => { + return new Promise(resolve => { + server.stop(() => { + server = null; + resolve(); + }); + }); + }); + + function forbiddenHandler(meta, res) { + res.setStatusLine(meta.httpVersion, 401, "Unauthorized"); + res.setHeader("WWW-Authenticate", 'Basic realm="Realm"'); + } + function pageHandler(meta, res) { + res.setStatusLine(meta.httpVersion, 200, "OK"); + res.setHeader("Content-Type", "text/html"); + let body = "<html><body></body></html>"; + res.bodyOutputStream.write(body, body.length); + } + server.registerPathHandler("/forbidden", forbiddenHandler); + server.registerPathHandler("/page", pageHandler); + server.start(-1); + + const HOST = `localhost:${server.identity.primaryPort}`; + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + const AUTH_URI = `http://${HOST}/forbidden`; + + // Try a simple load: + await checkDialog( + "https://example.com/", + browser => BrowserTestUtils.loadURIString(browser, AUTH_URI), + HOST, + "chrome://global/skin/icons/defaultFavicon.svg", + Ci.nsIPrompt.MODAL_TYPE_TAB + ); + + let subframeLoad = function (browser) { + return SpecialPowers.spawn(browser, [AUTH_URI], uri => { + let f = content.document.createElement("iframe"); + f.src = uri; + content.document.body.appendChild(f); + }); + }; + + // Try x-origin subframe: + await checkDialog( + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://example.org/1", + subframeLoad, + HOST, + /* Because this is x-origin, we expect a different icon: */ + "chrome://global/skin/icons/security-broken.svg", + Ci.nsIPrompt.MODAL_TYPE_TAB + ); +}); diff --git a/browser/base/content/test/tabPrompts/browser_multiplePrompts.js b/browser/base/content/test/tabPrompts/browser_multiplePrompts.js new file mode 100644 index 0000000000..597b7dfd6f --- /dev/null +++ b/browser/base/content/test/tabPrompts/browser_multiplePrompts.js @@ -0,0 +1,171 @@ +"use strict"; + +const CONTENT_PROMPT_SUBDIALOG = Services.prefs.getBoolPref( + "prompts.contentPromptSubDialog", + false +); + +/** + * Goes through a stacked series of dialogs opened with + * CONTENT_PROMPT_SUBDIALOG set to true, and ensures that + * the oldest one is front-most and has the right type. It + * then closes the oldest to newest dialog. + * + * @param {Element} tab The <tab> that has had content dialogs opened + * for it. + * @param {Number} promptCount How many dialogs we expected to have been + * opened. + * + * @return {Promise} + * @resolves {undefined} Once the dialogs have all been closed. + */ +async function closeDialogs(tab, dialogCount) { + let dialogElementsCount = dialogCount; + let dialogs = + tab.linkedBrowser.tabDialogBox.getContentDialogManager().dialogs; + + is( + dialogs.length, + dialogElementsCount, + "There should be " + dialogElementsCount + " dialog(s)." + ); + + let i = dialogElementsCount - 1; + for (let dialog of dialogs) { + dialog.focus(true); + await dialog._dialogReady; + + let dialogWindow = dialog.frameContentWindow; + let expectedType = ["alert", "prompt", "confirm"][i % 3]; + + is( + dialogWindow.Dialog.args.text, + expectedType + " countdown #" + i, + "The #" + i + " alert should be labelled as such." + ); + i--; + + dialogWindow.Dialog.ui.button0.click(); + + // The click is handled async; wait for an event loop turn for that to + // happen. + await new Promise(function (resolve) { + Services.tm.dispatchToMainThread(resolve); + }); + } + + dialogs = tab.linkedBrowser.tabDialogBox.getContentDialogManager().dialogs; + is(dialogs.length, 0, "Dialogs should all be dismissed."); +} + +/** + * Goes through a stacked series of tabprompt modals opened with + * CONTENT_PROMPT_SUBDIALOG set to false, and ensures that + * the oldest one is front-most and has the right type. It also + * ensures that the other tabprompt modals are hidden. It + * then closes the oldest to newest dialog. + * + * @param {Element} tab The <tab> that has had tabprompt modals opened + * for it. + * @param {Number} promptCount How many modals we expected to have been + * opened. + * + * @return {Promise} + * @resolves {undefined} Once the modals have all been closed. + */ +async function closeTabModals(tab, promptCount) { + let promptElementsCount = promptCount; + while (promptElementsCount--) { + let promptElements = + tab.linkedBrowser.parentNode.querySelectorAll("tabmodalprompt"); + is( + promptElements.length, + promptElementsCount + 1, + "There should be " + (promptElementsCount + 1) + " prompt(s)." + ); + // The oldest should be the first. + let i = 0; + + for (let promptElement of promptElements) { + let prompt = tab.linkedBrowser.tabModalPromptBox.getPrompt(promptElement); + let expectedType = ["alert", "prompt", "confirm"][i % 3]; + is( + prompt.Dialog.args.text, + expectedType + " countdown #" + i, + "The #" + i + " alert should be labelled as such." + ); + if (i !== promptElementsCount) { + is(prompt.element.hidden, true, "This prompt should be hidden."); + i++; + continue; + } + + is(prompt.element.hidden, false, "The last prompt should not be hidden."); + prompt.onButtonClick(0); + + // The click is handled async; wait for an event loop turn for that to + // happen. + await new Promise(function (resolve) { + Services.tm.dispatchToMainThread(resolve); + }); + } + } + + let promptElements = + tab.linkedBrowser.parentNode.querySelectorAll("tabmodalprompt"); + is(promptElements.length, 0, "Prompts should all be dismissed."); +} + +/* + * This test triggers multiple alerts on one single tab, because it"s possible + * for web content to do so. The behavior is described in bug 1266353. + * + * We assert the presentation of the multiple alerts, ensuring we show only + * the oldest one. + */ +add_task(async function () { + const PROMPTCOUNT = 9; + + let unopenedPromptCount = PROMPTCOUNT; + + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://example.com", + true + ); + info("Tab loaded"); + + let promptsOpenedPromise = BrowserTestUtils.waitForEvent( + tab.linkedBrowser, + "DOMWillOpenModalDialog", + false, + () => { + unopenedPromptCount--; + return unopenedPromptCount == 0; + } + ); + + await SpecialPowers.spawn(tab.linkedBrowser, [PROMPTCOUNT], maxPrompts => { + var i = maxPrompts; + let fns = ["alert", "prompt", "confirm"]; + function openDialog() { + i--; + if (i) { + SpecialPowers.Services.tm.dispatchToMainThread(openDialog); + } + content[fns[i % 3]](fns[i % 3] + " countdown #" + i); + } + SpecialPowers.Services.tm.dispatchToMainThread(openDialog); + }); + + await promptsOpenedPromise; + + if (CONTENT_PROMPT_SUBDIALOG) { + await closeDialogs(tab, PROMPTCOUNT); + } else { + await closeTabModals(tab, PROMPTCOUNT); + } + + BrowserTestUtils.removeTab(tab); +}); diff --git a/browser/base/content/test/tabPrompts/browser_openPromptInBackgroundTab.js b/browser/base/content/test/tabPrompts/browser_openPromptInBackgroundTab.js new file mode 100644 index 0000000000..d95faa9665 --- /dev/null +++ b/browser/base/content/test/tabPrompts/browser_openPromptInBackgroundTab.js @@ -0,0 +1,262 @@ +"use strict"; + +const { PermissionTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PermissionTestUtils.sys.mjs" +); + +const ROOT = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content/", + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://example.com/" +); +let pageWithAlert = ROOT + "openPromptOffTimeout.html"; + +registerCleanupFunction(function () { + Services.perms.removeAll(); +}); + +/* + * This test opens a tab that alerts when it is hidden. We then switch away + * from the tab, and check that by default the tab is not automatically + * re-selected. We also check that a checkbox appears in the alert that allows + * the user to enable this automatically re-selecting. We then check that + * checking the checkbox does actually enable that behaviour. + */ +add_task(async function test_old_modal_ui() { + // We're intentionally testing the old modal mechanism, so disable the new one. + await SpecialPowers.pushPrefEnv({ + set: [["prompts.contentPromptSubDialog", false]], + }); + + let firstTab = gBrowser.selectedTab; + // load page that opens prompt when page is hidden + let openedTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + pageWithAlert, + true + ); + let openedTabGotAttentionPromise = BrowserTestUtils.waitForAttribute( + "attention", + openedTab + ); + // switch away from that tab again - this triggers the alert. + await BrowserTestUtils.switchTab(gBrowser, firstTab); + // ... but that's async on e10s... + await openedTabGotAttentionPromise; + // check for attention attribute + is( + openedTab.hasAttribute("attention"), + true, + "Tab with alert should have 'attention' attribute." + ); + ok(!openedTab.selected, "Tab with alert should not be selected"); + + // switch tab back, and check the checkbox is displayed: + await BrowserTestUtils.switchTab(gBrowser, openedTab); + // check the prompt is there, and the extra row is present + let promptElements = + openedTab.linkedBrowser.parentNode.querySelectorAll("tabmodalprompt"); + is(promptElements.length, 1, "There should be 1 prompt"); + let ourPromptElement = promptElements[0]; + let checkbox = ourPromptElement.querySelector( + "checkbox[label*='example.com']" + ); + ok(checkbox, "The checkbox should be there"); + ok(!checkbox.checked, "Checkbox shouldn't be checked"); + // tick box and accept dialog + checkbox.checked = true; + let ourPrompt = + openedTab.linkedBrowser.tabModalPromptBox.getPrompt(ourPromptElement); + ourPrompt.onButtonClick(0); + // Wait for that click to actually be handled completely. + await new Promise(function (resolve) { + Services.tm.dispatchToMainThread(resolve); + }); + // check permission is set + is( + Services.perms.ALLOW_ACTION, + PermissionTestUtils.testPermission(pageWithAlert, "focus-tab-by-prompt"), + "Tab switching should now be allowed" + ); + + // Check if the control center shows the correct permission. + let shown = BrowserTestUtils.waitForEvent( + window, + "popupshown", + true, + event => event.target == gPermissionPanel._permissionPopup + ); + gPermissionPanel._identityPermissionBox.click(); + await shown; + let labelText = SitePermissions.getPermissionLabel("focus-tab-by-prompt"); + let permissionsList = document.getElementById( + "permission-popup-permission-list" + ); + let label = permissionsList.querySelector( + ".permission-popup-permission-label" + ); + is(label.textContent, labelText); + gPermissionPanel._permissionPopup.hidePopup(); + + // Check if the identity icon signals granted permission. + ok( + gPermissionPanel._identityPermissionBox.hasAttribute("hasPermissions"), + "identity-box signals granted permissions" + ); + + let openedTabSelectedPromise = BrowserTestUtils.waitForAttribute( + "selected", + openedTab, + "true" + ); + // switch to other tab again + await BrowserTestUtils.switchTab(gBrowser, firstTab); + + // This is sync in non-e10s, but in e10s we need to wait for this, so yield anyway. + // Note that the switchTab promise doesn't actually guarantee anything about *which* + // tab ends up as selected when its event fires, so using that here wouldn't work. + await openedTabSelectedPromise; + // should be switched back + ok(openedTab.selected, "Ta-dah, the other tab should now be selected again!"); + + // In e10s, with the conformant promise scheduling, we have to wait for next tick + // to ensure that the prompt is open before removing the opened tab, because the + // promise callback of 'openedTabSelectedPromise' could be done at the middle of + // RemotePrompt.openTabPrompt() while 'DOMModalDialogClosed' event is fired. + await TestUtils.waitForTick(); + + BrowserTestUtils.removeTab(openedTab); +}); + +add_task(async function test_new_modal_ui() { + // We're intentionally testing the new modal mechanism, so make sure it's enabled. + await SpecialPowers.pushPrefEnv({ + set: [["prompts.contentPromptSubDialog", true]], + }); + + // Make sure we clear the focus tab permission set in the previous test + PermissionTestUtils.remove(pageWithAlert, "focus-tab-by-prompt"); + + let firstTab = gBrowser.selectedTab; + // load page that opens prompt when page is hidden + let openedTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + pageWithAlert, + true + ); + let openedTabGotAttentionPromise = BrowserTestUtils.waitForAttribute( + "attention", + openedTab + ); + // switch away from that tab again - this triggers the alert. + await BrowserTestUtils.switchTab(gBrowser, firstTab); + // ... but that's async on e10s... + await openedTabGotAttentionPromise; + // check for attention attribute + is( + openedTab.hasAttribute("attention"), + true, + "Tab with alert should have 'attention' attribute." + ); + ok(!openedTab.selected, "Tab with alert should not be selected"); + + // switch tab back, and check the checkbox is displayed: + await BrowserTestUtils.switchTab(gBrowser, openedTab); + // check the prompt is there + let promptElements = openedTab.linkedBrowser.parentNode.querySelectorAll( + ".content-prompt-dialog" + ); + + let dialogBox = gBrowser.getTabDialogBox(openedTab.linkedBrowser); + let contentPromptManager = dialogBox.getContentDialogManager(); + is(promptElements.length, 1, "There should be 1 prompt"); + is( + contentPromptManager._dialogs.length, + 1, + "Content prompt manager should have 1 dialog box." + ); + + // make sure the checkbox appears and that the permission for allowing tab switching + // is set when the checkbox is tickted and the dialog is accepted + let dialog = contentPromptManager._dialogs[0]; + + await dialog._dialogReady; + + let dialogDoc = dialog._frame.contentWindow.document; + let checkbox = dialogDoc.querySelector("checkbox[label*='example.com']"); + let button = dialogDoc.querySelector("#commonDialog").getButton("accept"); + + ok(checkbox, "The checkbox should be there"); + ok(!checkbox.checked, "Checkbox shouldn't be checked"); + + // tick box and accept dialog + checkbox.checked = true; + button.click(); + // Wait for that click to actually be handled completely. + await new Promise(function (resolve) { + Services.tm.dispatchToMainThread(resolve); + }); + + ok(!contentPromptManager._dialogs.length, "Dialog should be closed"); + + // check permission is set + is( + Services.perms.ALLOW_ACTION, + PermissionTestUtils.testPermission(pageWithAlert, "focus-tab-by-prompt"), + "Tab switching should now be allowed" + ); + + // Check if the control center shows the correct permission. + let shown = BrowserTestUtils.waitForEvent( + window, + "popupshown", + true, + event => event.target == gPermissionPanel._permissionPopup + ); + gPermissionPanel._identityPermissionBox.click(); + await shown; + let labelText = SitePermissions.getPermissionLabel("focus-tab-by-prompt"); + let permissionsList = document.getElementById( + "permission-popup-permission-list" + ); + let label = permissionsList.querySelector( + ".permission-popup-permission-label" + ); + is(label.textContent, labelText); + gPermissionPanel.hidePopup(); + + // Check if the identity icon signals granted permission. + ok( + gPermissionPanel._identityPermissionBox.hasAttribute("hasPermissions"), + "identity-permission-box signals granted permissions" + ); + + let openedTabSelectedPromise = BrowserTestUtils.waitForAttribute( + "selected", + openedTab, + "true" + ); + + // switch to other tab again + await BrowserTestUtils.switchTab(gBrowser, firstTab); + + // This is sync in non-e10s, but in e10s we need to wait for this, so yield anyway. + // Note that the switchTab promise doesn't actually guarantee anything about *which* + // tab ends up as selected when its event fires, so using that here wouldn't work. + await openedTabSelectedPromise; + + ok(contentPromptManager._dialogs.length === 1, "Dialog opened."); + dialog = contentPromptManager._dialogs[0]; + await dialog._dialogReady; + + // should be switched back + ok(openedTab.selected, "Ta-dah, the other tab should now be selected again!"); + + // In e10s, with the conformant promise scheduling, we have to wait for next tick + // to ensure that the prompt is open before removing the opened tab, because the + // promise callback of 'openedTabSelectedPromise' could be done at the middle of + // RemotePrompt.openTabPrompt() while 'DOMModalDialogClosed' event is fired. + // await TestUtils.waitForTick(); + + await BrowserTestUtils.removeTab(openedTab); +}); diff --git a/browser/base/content/test/tabPrompts/browser_promptFocus.js b/browser/base/content/test/tabPrompts/browser_promptFocus.js new file mode 100644 index 0000000000..89ca064c10 --- /dev/null +++ b/browser/base/content/test/tabPrompts/browser_promptFocus.js @@ -0,0 +1,170 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { PromptTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PromptTestUtils.sys.mjs" +); + +// MacOS has different default focus behavior for prompts. +const isMacOS = Services.appinfo.OS === "Darwin"; + +/** + * Tests that prompts are focused when switching tabs. + */ +add_task(async function test_tabdialogbox_tab_switch_focus() { + // Open 3 tabs + let tabPromises = []; + for (let i = 0; i < 3; i += 1) { + tabPromises.push( + BrowserTestUtils.openNewForegroundTab( + gBrowser, + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://example.com", + true + ) + ); + } + // Wait for tabs to be ready + let tabs = await Promise.all(tabPromises); + let [tabA, tabB, tabC] = tabs; + + // Spawn two prompts, which have different default focus as determined by + // CommonDialog#setDefaultFocus. + let openPromise = PromptTestUtils.waitForPrompt(tabA.linkedBrowser, { + modalType: Services.prompt.MODAL_TYPE_TAB, + promptType: "confirm", + }); + Services.prompt.asyncConfirm( + tabA.linkedBrowser.browsingContext, + Services.prompt.MODAL_TYPE_TAB, + null, + "prompt A" + ); + let promptA = await openPromise; + + openPromise = PromptTestUtils.waitForPrompt(tabB.linkedBrowser, { + modalType: Services.prompt.MODAL_TYPE_TAB, + promptType: "promptPassword", + }); + Services.prompt.asyncPromptPassword( + tabB.linkedBrowser.browsingContext, + Services.prompt.MODAL_TYPE_TAB, + null, + "prompt B", + "", + null, + false + ); + let promptB = await openPromise; + + // Switch tabs and check if the correct element was focused. + + // Switch back to the third tab which doesn't have a prompt. + await BrowserTestUtils.switchTab(gBrowser, tabC); + is( + Services.focus.focusedElement, + tabC.linkedBrowser, + "Tab without prompt should have focus on browser." + ); + + // Switch to first tab which has prompt + await BrowserTestUtils.switchTab(gBrowser, tabA); + + if (isMacOS) { + is( + Services.focus.focusedElement, + promptA.ui.infoBody, + "Tab with prompt should have focus on body." + ); + } else { + is( + Services.focus.focusedElement, + promptA.ui.button0, + "Tab with prompt should have focus on default button." + ); + } + + await PromptTestUtils.handlePrompt(promptA); + + // Switch to second tab which has prompt + await BrowserTestUtils.switchTab(gBrowser, tabB); + is( + Services.focus.focusedElement, + promptB.ui.password1Textbox, + "Tab with password prompt should have focus on password field." + ); + await PromptTestUtils.handlePrompt(promptB); + + // Cleanup + tabs.forEach(tab => { + BrowserTestUtils.removeTab(tab); + }); +}); + +/** + * Tests that an alert prompt has focus on the default element. + * @param {CommonDialog} prompt - Prompt to test focus for. + * @param {number} index - Index of the prompt to log. + */ +function testAlertPromptFocus(prompt, index) { + if (isMacOS) { + is( + Services.focus.focusedElement, + prompt.ui.infoBody, + `Prompt #${index} should have focus on body.` + ); + } else { + is( + Services.focus.focusedElement, + prompt.ui.button0, + `Prompt #${index} should have focus on default button.` + ); + } +} + +/** + * Test that we set the correct focus when queuing multiple prompts. + */ +add_task(async function test_tabdialogbox_prompt_queue_focus() { + await BrowserTestUtils.withNewTab(gBrowser, async browser => { + const PROMPT_COUNT = 10; + + let firstPromptPromise = PromptTestUtils.waitForPrompt(browser, { + modalType: Services.prompt.MODAL_TYPE_TAB, + promptType: "alert", + }); + + for (let i = 0; i < PROMPT_COUNT; i += 1) { + Services.prompt.asyncAlert( + browser.browsingContext, + Services.prompt.MODAL_TYPE_TAB, + null, + "prompt " + i + ); + } + + // Close prompts one by one and check focus. + let nextPromptPromise = firstPromptPromise; + for (let i = 0; i < PROMPT_COUNT; i += 1) { + let p = await nextPromptPromise; + testAlertPromptFocus(p, i); + + if (i < PROMPT_COUNT - 1) { + nextPromptPromise = PromptTestUtils.waitForPrompt(browser, { + modalType: Services.prompt.MODAL_TYPE_TAB, + promptType: "alert", + }); + } + await PromptTestUtils.handlePrompt(p); + } + + // All prompts are closed, focus should be back on the browser. + is( + Services.focus.focusedElement, + browser, + "Tab without prompts should have focus on browser." + ); + }); +}); diff --git a/browser/base/content/test/tabPrompts/browser_prompt_closed_window.js b/browser/base/content/test/tabPrompts/browser_prompt_closed_window.js new file mode 100644 index 0000000000..4db3286691 --- /dev/null +++ b/browser/base/content/test/tabPrompts/browser_prompt_closed_window.js @@ -0,0 +1,40 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Check that if we loop prompts from a closed tab, they don't + * start showing up as window prompts. + */ +add_task(async function test_closed_tab_doesnt_show_prompt() { + let newWin = await BrowserTestUtils.openNewBrowserWindow(); + + // Get a promise for the initial, in-tab prompt: + let promptPromise = BrowserTestUtils.promiseAlertDialogOpen(); + await ContentTask.spawn(newWin.gBrowser.selectedBrowser, [], function () { + // Don't want to block, so use setTimeout with 0 timeout: + content.setTimeout( + () => + content.eval( + 'let i = 0; while (!prompt("Prompts a lot!") && i++ < 10);' + ), + 0 + ); + }); + // wait for the first prompt to have appeared: + await promptPromise; + + // Now close the containing tab, and check for windowed prompts appearing. + let opened = false; + let obs = () => { + opened = true; + }; + Services.obs.addObserver(obs, "domwindowopened"); + registerCleanupFunction(() => + Services.obs.removeObserver(obs, "domwindowopened") + ); + await BrowserTestUtils.closeWindow(newWin); + + ok(!opened, "Should not have opened a prompt when closing the main window."); +}); diff --git a/browser/base/content/test/tabPrompts/browser_switchTabPermissionPrompt.js b/browser/base/content/test/tabPrompts/browser_switchTabPermissionPrompt.js new file mode 100644 index 0000000000..e803869b92 --- /dev/null +++ b/browser/base/content/test/tabPrompts/browser_switchTabPermissionPrompt.js @@ -0,0 +1,41 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function test_check_file_prompt() { + let initialTab = gBrowser.selectedTab; + await BrowserTestUtils.withNewTab("about:blank", async browser => { + await BrowserTestUtils.switchTab(gBrowser, initialTab); + + let testHelper = async function (uri, expectedValue) { + BrowserTestUtils.loadURIString(browser, uri); + await BrowserTestUtils.browserLoaded(browser, false, uri); + let dialogFinishedShowing = TestUtils.topicObserved( + "common-dialog-loaded" + ); + await SpecialPowers.spawn(browser, [], () => { + content.setTimeout(() => { + content.alert("Hello"); + }, 0); + }); + + let [dialogWin] = await dialogFinishedShowing; + let checkbox = dialogWin.document.getElementById("checkbox"); + info("Got: " + checkbox.label); + ok( + checkbox.label.includes(expectedValue), + `Checkbox label should mention domain (${expectedValue}).` + ); + + dialogWin.document.querySelector("dialog").acceptDialog(); + }; + + await testHelper("https://example.com/1", "example.com"); + await testHelper("about:robots", "about:"); + let file = Services.io.newFileURI( + Services.dirsvc.get("Desk", Ci.nsIFile) + ).spec; + await testHelper(file, "file://"); + }); +}); diff --git a/browser/base/content/test/tabPrompts/browser_windowPrompt.js b/browser/base/content/test/tabPrompts/browser_windowPrompt.js new file mode 100644 index 0000000000..535142f485 --- /dev/null +++ b/browser/base/content/test/tabPrompts/browser_windowPrompt.js @@ -0,0 +1,259 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Check that the in-window modal dialogs work correctly. + */ +add_task(async function test_check_window_modal_prompt_service() { + await SpecialPowers.pushPrefEnv({ + set: [["prompts.windowPromptSubDialog", true]], + }); + let dialogPromise = BrowserTestUtils.promiseAlertDialogOpen(); + // Avoid blocking the test on the (sync) alert by sticking it in a timeout: + setTimeout( + () => Services.prompt.alert(window, "Some title", "some message"), + 0 + ); + let dialogWin = await dialogPromise; + + // Check dialog content: + is( + dialogWin.document.getElementById("infoTitle").textContent, + "Some title", + "Title should be correct." + ); + ok( + !dialogWin.document.getElementById("infoTitle").hidden, + "Title should be shown." + ); + is( + dialogWin.document.getElementById("infoBody").textContent, + "some message", + "Body text should be correct." + ); + + // Check circumstances of opening. + ok( + dialogWin?.docShell?.chromeEventHandler, + "Should have embedded the dialog." + ); + for (let menu of document.querySelectorAll("menubar > menu")) { + ok(menu.disabled, `Menu ${menu.id} should be disabled.`); + } + + let container = dialogWin.docShell.chromeEventHandler.closest("dialog"); + let closedPromise = BrowserTestUtils.waitForMutationCondition( + container, + { childList: true, attributes: true }, + () => !container.hasChildNodes() && !container.open + ); + + EventUtils.sendKey("ESCAPE"); + await closedPromise; + + await BrowserTestUtils.waitForMutationCondition( + document.querySelector("menubar > menu"), + { attributes: true }, + () => !document.querySelector("menubar > menu").disabled + ); + + // Check we cleaned up: + for (let menu of document.querySelectorAll("menubar > menu")) { + ok(!menu.disabled, `Menu ${menu.id} should not be disabled anymore.`); + } +}); + +/** + * Check that the dialog's own closing methods being invoked don't break things. + */ +add_task(async function test_check_window_modal_prompt_service() { + await SpecialPowers.pushPrefEnv({ + set: [["prompts.windowPromptSubDialog", true]], + }); + let dialogPromise = BrowserTestUtils.promiseAlertDialogOpen(); + // Avoid blocking the test on the (sync) alert by sticking it in a timeout: + setTimeout( + () => Services.prompt.alert(window, "Some title", "some message"), + 0 + ); + let dialogWin = await dialogPromise; + + // Check circumstances of opening. + ok( + dialogWin?.docShell?.chromeEventHandler, + "Should have embedded the dialog." + ); + + let container = dialogWin.docShell.chromeEventHandler.closest("dialog"); + let closedPromise = BrowserTestUtils.waitForMutationCondition( + container, + { childList: true, attributes: true }, + () => !container.hasChildNodes() && !container.open + ); + + // This can also be invoked by the user if the escape key is handled + // outside of our embedded dialog. + container.close(); + await closedPromise; + + // Check we cleaned up: + for (let menu of document.querySelectorAll("menubar > menu")) { + ok(!menu.disabled, `Menu ${menu.id} should not be disabled anymore.`); + } +}); + +add_task(async function test_check_multiple_prompts() { + await SpecialPowers.pushPrefEnv({ + set: [["prompts.windowPromptSubDialog", true]], + }); + let container = document.getElementById("window-modal-dialog"); + let dialogPromise = BrowserTestUtils.promiseAlertDialogOpen(); + + let firstDialogClosedPromise = new Promise(resolve => { + // Avoid blocking the test on the (sync) alert by sticking it in a timeout: + setTimeout(() => { + Services.prompt.alert(window, "Some title", "some message"); + resolve(); + }, 0); + }); + let dialogWin = await dialogPromise; + + // Check circumstances of opening. + ok( + dialogWin?.docShell?.chromeEventHandler, + "Should have embedded the dialog." + ); + is(container.childElementCount, 1, "Should only have 1 dialog in the DOM."); + + let secondDialogClosedPromise = new Promise(resolve => { + // Avoid blocking the test on the (sync) alert by sticking it in a timeout: + setTimeout(() => { + Services.prompt.alert(window, "Another title", "another message"); + resolve(); + }, 0); + }); + + dialogPromise = BrowserTestUtils.promiseAlertDialogOpen(); + + info("Accepting dialog"); + dialogWin.document.querySelector("dialog").acceptDialog(); + + let oldWin = dialogWin; + + info("Second dialog should automatically come up."); + dialogWin = await dialogPromise; + + isnot(oldWin, dialogWin, "Opened a new dialog."); + ok(container.open, "Dialog should be open."); + + info("Now close the second dialog."); + dialogWin.document.querySelector("dialog").acceptDialog(); + + await firstDialogClosedPromise; + await secondDialogClosedPromise; + + await BrowserTestUtils.waitForMutationCondition( + container, + { childList: true, attributes: true }, + () => !container.hasChildNodes() && !container.open + ); + // Check we cleaned up: + for (let menu of document.querySelectorAll("menubar > menu")) { + ok(!menu.disabled, `Menu ${menu.id} should not be disabled anymore.`); + } +}); + +/** + * Check that the in-window modal dialogs un-minimizes windows when necessary. + */ +add_task(async function test_check_minimize_response() { + // Window minimization doesn't necessarily work on Linux... + if (AppConstants.platform == "linux") { + return; + } + await SpecialPowers.pushPrefEnv({ + set: [["prompts.windowPromptSubDialog", true]], + }); + + let promiseSizeModeChange = BrowserTestUtils.waitForEvent( + window, + "sizemodechange" + ); + window.minimize(); + await promiseSizeModeChange; + is(window.windowState, window.STATE_MINIMIZED, "Should be minimized."); + + promiseSizeModeChange = BrowserTestUtils.waitForEvent( + window, + "sizemodechange" + ); + let dialogPromise = BrowserTestUtils.promiseAlertDialogOpen(); + // Use an async alert to avoid blocking. + Services.prompt.asyncAlert( + window.browsingContext, + Ci.nsIPrompt.MODAL_TYPE_INTERNAL_WINDOW, + "Some title", + "some message" + ); + let dialogWin = await dialogPromise; + await promiseSizeModeChange; + + isnot( + window.windowState, + window.STATE_MINIMIZED, + "Should no longer be minimized." + ); + + // Check dialog content: + is( + dialogWin.document.getElementById("infoTitle").textContent, + "Some title", + "Title should be correct." + ); + + let container = dialogWin.docShell.chromeEventHandler.closest("dialog"); + let closedPromise = BrowserTestUtils.waitForMutationCondition( + container, + { childList: true, attributes: true }, + () => !container.hasChildNodes() && !container.open + ); + + EventUtils.sendKey("ESCAPE"); + await closedPromise; + + await BrowserTestUtils.waitForMutationCondition( + document.querySelector("menubar > menu"), + { attributes: true }, + () => !document.querySelector("menubar > menu").disabled + ); +}); + +/** + * Tests that we get a closed callback even when closing the prompt before the + * underlying SubDialog has fully opened. + */ +add_task(async function test_closed_callback() { + await SpecialPowers.pushPrefEnv({ + set: [["prompts.windowPromptSubDialog", true]], + }); + + let promptClosedPromise = Services.prompt.asyncAlert( + window.browsingContext, + Services.prompt.MODAL_TYPE_INTERNAL_WINDOW, + "Hello", + "Hello, World!" + ); + + let dialog = gDialogBox._dialog; + ok(dialog, "gDialogBox should have a dialog"); + + // Directly close the dialog without waiting for it to initialize. + dialog.close(); + + info("Waiting for prompt close"); + await promptClosedPromise; + + ok(!gDialogBox._dialog, "gDialogBox should no longer have a dialog"); +}); diff --git a/browser/base/content/test/tabPrompts/file_beforeunload_stop.html b/browser/base/content/test/tabPrompts/file_beforeunload_stop.html new file mode 100644 index 0000000000..7273e60c65 --- /dev/null +++ b/browser/base/content/test/tabPrompts/file_beforeunload_stop.html @@ -0,0 +1,8 @@ +<body> + <p>I will ask not to be closed.</p> + <script> + window.onbeforeunload = function() { + return "true"; + }; + </script> +</body> diff --git a/browser/base/content/test/tabPrompts/openPromptOffTimeout.html b/browser/base/content/test/tabPrompts/openPromptOffTimeout.html new file mode 100644 index 0000000000..5dfd8cbeff --- /dev/null +++ b/browser/base/content/test/tabPrompts/openPromptOffTimeout.html @@ -0,0 +1,10 @@ +<body> +This page opens an alert box when the page is hidden. +<script> +document.addEventListener("visibilitychange", () => { + if (document.hidden) { + alert("You hid my page!"); + } +}); +</script> +</body> diff --git a/browser/base/content/test/tabPrompts/redirect-crossDomain-tabTitle-update.html b/browser/base/content/test/tabPrompts/redirect-crossDomain-tabTitle-update.html new file mode 100644 index 0000000000..773b3e47d9 --- /dev/null +++ b/browser/base/content/test/tabPrompts/redirect-crossDomain-tabTitle-update.html @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="UTF-8"> + <title>example.com</title> + </head> + <body> + I am a friendly test page! + <script> + document.title="tab title update 1"; + window.location.href="https://example.org:443/browser/browser/base/content/test/tabPrompts/auth-route.sjs"; + document.title ="tab title update 2"; + </script> + </body> +</html> diff --git a/browser/base/content/test/tabPrompts/redirect-crossDomain.html b/browser/base/content/test/tabPrompts/redirect-crossDomain.html new file mode 100644 index 0000000000..ebae2c060a --- /dev/null +++ b/browser/base/content/test/tabPrompts/redirect-crossDomain.html @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="UTF-8"> + <title>example.com</title> + </head> + <body> + I am a friendly test page! + <script> + window.location.href="https://example.org:443/browser/browser/base/content/test/tabPrompts/auth-route.sjs"; + </script> + </body> +</html> diff --git a/browser/base/content/test/tabPrompts/redirect-sameDomain.html b/browser/base/content/test/tabPrompts/redirect-sameDomain.html new file mode 100644 index 0000000000..2e50689d1e --- /dev/null +++ b/browser/base/content/test/tabPrompts/redirect-sameDomain.html @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="UTF-8"> + <title>example.com</title> + </head> + <body> + I am a friendly test page! + <script> + window.location.href="https://test1.example.com:443/browser/browser/base/content/test/tabPrompts/auth-route.sjs"; + </script> + </body> +</html> |