From 36d22d82aa202bb199967e9512281e9a53db42c9 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 21:33:14 +0200 Subject: Adding upstream version 115.7.0esr. Signed-off-by: Daniel Baumann --- .../components/prompts/content/adjustableTitle.js | 193 +++ .../components/prompts/content/commonDialog.css | 124 ++ toolkit/components/prompts/content/commonDialog.js | 148 ++ .../components/prompts/content/commonDialog.xhtml | 103 ++ toolkit/components/prompts/content/selectDialog.js | 83 + .../components/prompts/content/selectDialog.xhtml | 21 + toolkit/components/prompts/content/tabprompts.css | 119 ++ .../components/prompts/content/tabprompts.sys.mjs | 298 ++++ toolkit/components/prompts/docs/index.rst | 10 + .../components/prompts/docs/modal-type-content.png | Bin 0 -> 35447 bytes toolkit/components/prompts/docs/modal-type-tab.png | Bin 0 -> 40928 bytes .../components/prompts/docs/modal-type-window.png | Bin 0 -> 49368 bytes toolkit/components/prompts/docs/modalTypes.rst | 95 + .../prompts/docs/nsIPromptService-reference.rst | 10 + .../components/prompts/docs/nsIPromptService.rst | 195 +++ toolkit/components/prompts/jar.mn | 13 + toolkit/components/prompts/moz.build | 24 + .../components/prompts/src/CommonDialog.sys.mjs | 381 ++++ toolkit/components/prompts/src/PromptUtils.sys.mjs | 186 ++ toolkit/components/prompts/src/Prompter.sys.mjs | 1824 ++++++++++++++++++++ toolkit/components/prompts/src/components.conf | 26 + toolkit/components/prompts/src/moz.build | 15 + toolkit/components/prompts/test/.eslintrc.js | 8 + .../prompts/test/PromptTestUtils.sys.mjs | 237 +++ .../components/prompts/test/bug619644_inner.html | 7 + .../components/prompts/test/bug625187_iframe.html | 16 + toolkit/components/prompts/test/chrome.ini | 11 + toolkit/components/prompts/test/chromeScript.js | 353 ++++ toolkit/components/prompts/test/mochitest.ini | 19 + toolkit/components/prompts/test/prompt_common.js | 445 +++++ .../components/prompts/test/test_bug619644.html | 74 + .../components/prompts/test/test_bug620145.html | 96 ++ .../components/prompts/test/test_dom_prompts.html | 207 +++ .../prompts/test/test_modal_prompts.html | 1311 ++++++++++++++ .../components/prompts/test/test_modal_select.html | 138 ++ .../prompts/test/test_subresources_prompts.html | 200 +++ 36 files changed, 6990 insertions(+) create mode 100644 toolkit/components/prompts/content/adjustableTitle.js create mode 100644 toolkit/components/prompts/content/commonDialog.css create mode 100644 toolkit/components/prompts/content/commonDialog.js create mode 100644 toolkit/components/prompts/content/commonDialog.xhtml create mode 100644 toolkit/components/prompts/content/selectDialog.js create mode 100644 toolkit/components/prompts/content/selectDialog.xhtml create mode 100644 toolkit/components/prompts/content/tabprompts.css create mode 100644 toolkit/components/prompts/content/tabprompts.sys.mjs create mode 100644 toolkit/components/prompts/docs/index.rst create mode 100644 toolkit/components/prompts/docs/modal-type-content.png create mode 100644 toolkit/components/prompts/docs/modal-type-tab.png create mode 100644 toolkit/components/prompts/docs/modal-type-window.png create mode 100644 toolkit/components/prompts/docs/modalTypes.rst create mode 100644 toolkit/components/prompts/docs/nsIPromptService-reference.rst create mode 100644 toolkit/components/prompts/docs/nsIPromptService.rst create mode 100644 toolkit/components/prompts/jar.mn create mode 100644 toolkit/components/prompts/moz.build create mode 100644 toolkit/components/prompts/src/CommonDialog.sys.mjs create mode 100644 toolkit/components/prompts/src/PromptUtils.sys.mjs create mode 100644 toolkit/components/prompts/src/Prompter.sys.mjs create mode 100644 toolkit/components/prompts/src/components.conf create mode 100644 toolkit/components/prompts/src/moz.build create mode 100644 toolkit/components/prompts/test/.eslintrc.js create mode 100644 toolkit/components/prompts/test/PromptTestUtils.sys.mjs create mode 100644 toolkit/components/prompts/test/bug619644_inner.html create mode 100644 toolkit/components/prompts/test/bug625187_iframe.html create mode 100644 toolkit/components/prompts/test/chrome.ini create mode 100644 toolkit/components/prompts/test/chromeScript.js create mode 100644 toolkit/components/prompts/test/mochitest.ini create mode 100644 toolkit/components/prompts/test/prompt_common.js create mode 100644 toolkit/components/prompts/test/test_bug619644.html create mode 100644 toolkit/components/prompts/test/test_bug620145.html create mode 100644 toolkit/components/prompts/test/test_dom_prompts.html create mode 100644 toolkit/components/prompts/test/test_modal_prompts.html create mode 100644 toolkit/components/prompts/test/test_modal_select.html create mode 100644 toolkit/components/prompts/test/test_subresources_prompts.html (limited to 'toolkit/components/prompts') diff --git a/toolkit/components/prompts/content/adjustableTitle.js b/toolkit/components/prompts/content/adjustableTitle.js new file mode 100644 index 0000000000..bd9afd909a --- /dev/null +++ b/toolkit/components/prompts/content/adjustableTitle.js @@ -0,0 +1,193 @@ +/* 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"; + +let { PromptUtils } = ChromeUtils.importESModule( + "resource://gre/modules/PromptUtils.sys.mjs" +); + +const AdjustableTitle = { + _cssSnippet: ` + #titleContainer { + /* This gets display: flex by virtue of being a row in a subdialog, from + * commonDialog.css . */ + flex-shrink: 0; + + flex-direction: row; + align-items: baseline; + + margin-inline: 4px; + /* Ensure we don't exceed the bounds of the dialog: */ + max-width: calc(100vw - 32px); + + --icon-size: 16px; + } + + #titleContainer[noicon] > .titleIcon { + display: none; + } + + .titleIcon { + width: var(--icon-size); + height: var(--icon-size); + padding-inline-end: 4px; + flex-shrink: 0; + + background-image: var(--icon-url, url("chrome://global/skin/icons/defaultFavicon.svg")); + background-size: 16px 16px; + background-origin: content-box; + background-repeat: no-repeat; + background-color: var(--in-content-page-background); + -moz-context-properties: fill; + fill: currentColor; + } + + #titleCropper:not([nomaskfade]) { + display: inline-flex; + } + + #titleCropper { + overflow: hidden; + + justify-content: right; + mask-repeat: no-repeat; + /* go from left to right with the mask: */ + --mask-dir: right; + } + + #titleContainer:not([noicon]) > #titleCropper { + /* Align the icon and text: */ + translate: 0 calc(-1px - max(.6 * var(--icon-size) - .6em, 0px)); + } + + #titleCropper[rtlorigin] { + justify-content: left; + /* go from right to left with the mask: */ + --mask-dir: left; + } + + + #titleCropper:not([nomaskfade]) #titleText { + display: inline-flex; + white-space: nowrap; + } + + #titleText { + font-weight: 600; + flex: 1 0 auto; /* Grow but do not shrink. */ + unicode-bidi: plaintext; /* Ensure we align RTL text correctly. */ + text-align: match-parent; + } + + #titleCropper[overflown] { + mask-image: linear-gradient(to var(--mask-dir), transparent, black 100px); + } + + /* hide the old title */ + #infoTitle { + display: none; + } + `, + + _insertMarkup() { + let iconEl = document.createElement("span"); + iconEl.className = "titleIcon"; + this._titleCropEl = document.createElement("span"); + this._titleCropEl.id = "titleCropper"; + this._titleEl = document.createElement("span"); + this._titleEl.id = "titleText"; + this._containerEl = document.createElement("div"); + this._containerEl.id = "titleContainer"; + this._containerEl.className = "dialogRow titleContainer"; + this._titleCropEl.append(this._titleEl); + this._containerEl.append(iconEl, this._titleCropEl); + let targetID = document.documentElement.getAttribute("headerparent"); + document.getElementById(targetID).prepend(this._containerEl); + let styleEl = document.createElement("style"); + styleEl.textContent = this._cssSnippet; + document.documentElement.prepend(styleEl); + }, + + _overflowHandler() { + requestAnimationFrame(async () => { + let isOverflown; + try { + isOverflown = await window.promiseDocumentFlushed(() => { + return ( + this._titleCropEl.getBoundingClientRect().width < + this._titleEl.getBoundingClientRect().width + ); + }); + } catch (ex) { + // In automated tests, this can fail with a DOM exception if + // the window has closed by the time layout tries to call us. + // In this case, just bail, and only log any other errors: + if ( + !DOMException.isInstance(ex) || + ex.name != "NoModificationAllowedError" + ) { + console.error(ex); + } + return; + } + this._titleCropEl.toggleAttribute("overflown", isOverflown); + if (isOverflown) { + this._titleEl.setAttribute("title", this._titleEl.textContent); + } else { + this._titleEl.removeAttribute("title"); + } + }); + }, + + _updateTitle(title) { + title = JSON.parse(title); + if (title.raw) { + this._titleEl.textContent = title.raw; + let { DIRECTION_RTL } = window.windowUtils; + this._titleCropEl.toggleAttribute( + "rtlorigin", + window.windowUtils.getDirectionFromText(title.raw) == DIRECTION_RTL + ); + } else { + document.l10n.setAttributes(this._titleEl, title.l10nId); + } + + if (!document.documentElement.hasAttribute("neediconheader")) { + this._containerEl.setAttribute("noicon", "true"); + } else if (title.shouldUseMaskFade) { + this._overflowHandler(); + } else { + this._titleCropEl.toggleAttribute("nomaskfade", true); + } + }, + + init() { + // Only run this if we're embedded and proton modals are enabled. + if (!window.docShell.chromeEventHandler) { + return; + } + + this._insertMarkup(); + let title = document.documentElement.getAttribute("headertitle"); + if (title) { + this._updateTitle(title); + } + this._mutObs = new MutationObserver(() => { + this._updateTitle(document.documentElement.getAttribute("headertitle")); + }); + this._mutObs.observe(document.documentElement, { + attributes: true, + attributeFilter: ["headertitle"], + }); + }, +}; + +document.addEventListener( + "DOMContentLoaded", + () => { + AdjustableTitle.init(); + }, + { once: true } +); diff --git a/toolkit/components/prompts/content/commonDialog.css b/toolkit/components/prompts/content/commonDialog.css new file mode 100644 index 0000000000..3521af13c6 --- /dev/null +++ b/toolkit/components/prompts/content/commonDialog.css @@ -0,0 +1,124 @@ +/* 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/. */ + +:root { + min-width: 29em; +} + +dialog[insecureauth] { + --icon-url: url("chrome://global/skin/icons/security-broken.svg"); +} + +#dialogGrid { + display: grid; + grid-template-columns: auto 1fr; + align-items: center; + max-height: 100%; +} + +.dialogRow:not([hidden]) { + display: contents; +} + +#iconContainer { + align-self: start; +} + +#infoContainer { + max-width: 45em; +} + +#infoTitle { + margin-bottom: 1em; +} + +#infoBody { + -moz-user-focus: normal; + user-select: text; + cursor: text !important; + white-space: pre-wrap; + word-break: break-word; + unicode-bidi: plaintext; + overflow-y: auto; +} + +.sizeDetermined, +.sizeDetermined::part(content-box), +.sizeDetermined #infoBody, +.sizeDetermined #infoRow, +.sizeDetermined #infoContainer { + /* Allow stuff to shrink */ + min-height: 0; +} + +.sizeDetermined #infoRow { + /* Make the info row take all the available space, potentially shrinking, + * since it's what contains the infoBody, which is scrollable */ + flex: 1; +} + +#loginLabel, #password1Label { + text-align: start; +} + +#loginTextbox, +#password1Textbox { + text-align: match-parent; +} + +/* use flexbox instead of grid: */ +dialog[subdialog], +dialog[subdialog] #dialogGrid, +dialog[subdialog] #infoContainer, +dialog[subdialog] .dialogRow:not([hidden]) { + display: flex; + flex-direction: column; + align-items: stretch; +} + +dialog[subdialog] #iconContainer { + display: none; +} + +/* Fix padding/spacing */ +dialog[subdialog] { + --grid-padding: 16px; + /* All the inner items should have 4px inline margin, leading to 1.16em spacing + * between the dialog and its contents, and 8px horizontal spacing between items. */ + padding: var(--grid-padding) calc(var(--grid-padding) - 4px); +} + +/* Use an ID selector for the dialog to win on specificity... */ +#commonDialog[subdialog] description, +#commonDialog[subdialog] checkbox { + margin: 0 4px; +} + +#commonDialog[subdialog] label { + margin: 4px; /* Labels for input boxes should get block as well as the regular inline margin. */ +} + +#commonDialog[subdialog] .checkbox-label { + /* The checkbox already has the horizontal margin, so override the rule + * above. */ + margin: 0; +} + +#commonDialog[subdialog] input { + margin: 0 4px 4px; +} + +/* Add vertical spaces between rows: */ +dialog[subdialog] .dialogRow { + margin-block: 0 var(--grid-padding); +} + +/* Fix spacing in the `prompt()` case: */ +dialog[subdialog] #loginLabel[value=""] { + display: none; +} + +dialog[subdialog][windowtype="prompt:prompt"] #infoRow { + margin-bottom: 4px; +} diff --git a/toolkit/components/prompts/content/commonDialog.js b/toolkit/components/prompts/content/commonDialog.js new file mode 100644 index 0000000000..a18b7cbd6c --- /dev/null +++ b/toolkit/components/prompts/content/commonDialog.js @@ -0,0 +1,148 @@ +/* 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/. */ + +const { CommonDialog } = ChromeUtils.importESModule( + "resource://gre/modules/CommonDialog.sys.mjs" +); + +// imported by adjustableTitle.js loaded in the same context: +/* globals PromptUtils */ + +var propBag, args, Dialog; + +// Inherit color scheme overrides from parent window. This is to inherit the +// color scheme of dark themed PBM windows. +{ + let openerColorSchemeOverride = + window.opener?.browsingContext?.top.prefersColorSchemeOverride; + if ( + openerColorSchemeOverride && + window.browsingContext == window.browsingContext.top + ) { + window.browsingContext.prefersColorSchemeOverride = + openerColorSchemeOverride; + } +} + +function commonDialogOnLoad() { + propBag = window.arguments[0] + .QueryInterface(Ci.nsIWritablePropertyBag2) + .QueryInterface(Ci.nsIWritablePropertyBag); + // Convert to a JS object + args = {}; + for (let prop of propBag.enumerator) { + args[prop.name] = prop.value; + } + + let dialog = document.getElementById("commonDialog"); + + let needIconifiedHeader = + args.modalType == Ci.nsIPrompt.MODAL_TYPE_CONTENT || + ["promptUserAndPass", "promptPassword"].includes(args.promptType) || + args.headerIconURL; + let root = document.documentElement; + if (needIconifiedHeader) { + root.setAttribute("neediconheader", "true"); + } + let title = { raw: args.title }; + let { promptPrincipal } = args; + if (promptPrincipal) { + if (promptPrincipal.isNullPrincipal) { + title = { l10nId: "common-dialog-title-null" }; + } else if (promptPrincipal.isSystemPrincipal) { + title = { l10nId: "common-dialog-title-system" }; + root.style.setProperty( + "--icon-url", + "url('chrome://branding/content/icon32.png')" + ); + } else if (promptPrincipal.addonPolicy) { + title.raw = promptPrincipal.addonPolicy.name; + } else if (promptPrincipal.isContentPrincipal) { + try { + title.raw = promptPrincipal.URI.displayHostPort; + } catch (ex) { + // hostPort getter can throw, e.g. for about URIs. + title.raw = promptPrincipal.originNoSuffix; + } + // hostPort can be empty for file URIs. + if (!title.raw) { + title.raw = promptPrincipal.prePath; + } + } else { + title = { l10nId: "common-dialog-title-unknown" }; + } + } else if (args.authOrigin) { + title = { raw: args.authOrigin }; + } + if (args.headerIconURL) { + root.style.setProperty("--icon-url", `url('${args.headerIconURL}')`); + } + // Fade and crop potentially long raw titles, e.g., origins and hostnames. + title.shouldUseMaskFade = title.raw && (args.authOrigin || promptPrincipal); + root.setAttribute("headertitle", JSON.stringify(title)); + if (args.isInsecureAuth) { + dialog.setAttribute("insecureauth", "true"); + } + + let ui = { + prompt: window, + loginContainer: document.getElementById("loginContainer"), + loginTextbox: document.getElementById("loginTextbox"), + loginLabel: document.getElementById("loginLabel"), + password1Container: document.getElementById("password1Container"), + password1Textbox: document.getElementById("password1Textbox"), + password1Label: document.getElementById("password1Label"), + infoRow: document.getElementById("infoRow"), + infoBody: document.getElementById("infoBody"), + infoTitle: document.getElementById("infoTitle"), + infoIcon: document.getElementById("infoIcon"), + checkbox: document.getElementById("checkbox"), + checkboxContainer: document.getElementById("checkboxContainer"), + button3: dialog.getButton("extra2"), + button2: dialog.getButton("extra1"), + button1: dialog.getButton("cancel"), + button0: dialog.getButton("accept"), + focusTarget: window, + }; + + Dialog = new CommonDialog(args, ui); + window.addEventListener("dialogclosing", function (aEvent) { + if (aEvent.detail?.abort) { + Dialog.abortPrompt(); + } + }); + document.addEventListener("dialogaccept", function () { + Dialog.onButton0(); + }); + document.addEventListener("dialogcancel", function () { + Dialog.onButton1(); + }); + document.addEventListener("dialogextra1", function () { + Dialog.onButton2(); + window.close(); + }); + document.addEventListener("dialogextra2", function () { + Dialog.onButton3(); + window.close(); + }); + document.subDialogSetDefaultFocus = isInitialFocus => + Dialog.setDefaultFocus(isInitialFocus); + Dialog.onLoad(dialog); + + // resize the window to the content + window.sizeToContent(); + + // If the icon hasn't loaded yet, size the window to the content again when + // it does, as its layout can change. + ui.infoIcon.addEventListener("load", () => window.sizeToContent()); + + window.getAttention(); +} + +function commonDialogOnUnload() { + // Convert args back into property bag + for (let propName in args) { + propBag.setProperty(propName, args[propName]); + } +} diff --git a/toolkit/components/prompts/content/commonDialog.xhtml b/toolkit/components/prompts/content/commonDialog.xhtml new file mode 100644 index 0000000000..02e0749a9e --- /dev/null +++ b/toolkit/components/prompts/content/commonDialog.xhtml @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+
diff --git a/toolkit/components/prompts/content/selectDialog.js b/toolkit/components/prompts/content/selectDialog.js new file mode 100644 index 0000000000..86809bc879 --- /dev/null +++ b/toolkit/components/prompts/content/selectDialog.js @@ -0,0 +1,83 @@ +/* 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/. */ + +// Defined in dialog.xml. +/* globals centerWindowOnScreen:false, moveToAlertPosition:false */ + +var propBag, listBox, args; + +function onDCL() { + propBag = window.arguments[0] + .QueryInterface(Ci.nsIWritablePropertyBag2) + .QueryInterface(Ci.nsIWritablePropertyBag); + + // Convert to a JS object + let args = {}; + for (let prop of propBag.enumerator) { + args[prop.name] = prop.value; + } + + let promptType = propBag.getProperty("promptType"); + if (promptType != "select") { + console.error("selectDialog opened for unknown type: ", promptType); + window.close(); + } + + // Default to canceled. + propBag.setProperty("ok", false); + + document.title = propBag.getProperty("title"); + + let text = propBag.getProperty("text"); + document.getElementById("info.txt").setAttribute("value", text); + + let items = propBag.getProperty("list"); + listBox = document.getElementById("list"); + + for (let i = 0; i < items.length; i++) { + let str = items[i]; + if (str == "") { + str = "<>"; + } + listBox.appendItem(str); + listBox.getItemAtIndex(i).addEventListener("dblclick", dialogDoubleClick); + } + listBox.selectedIndex = 0; +} + +function onLoad() { + listBox.focus(); + + document.addEventListener("dialogaccept", dialogOK); + // resize the window to the content + window.sizeToContent(); + + // Move to the right location + moveToAlertPosition(); + centerWindowOnScreen(); + + // play sound + try { + if (!args.openedWithTabDialog) { + Cc["@mozilla.org/sound;1"] + .getService(Ci.nsISound) + .playEventSound(Ci.nsISound.EVENT_SELECT_DIALOG_OPEN); + } + } catch (e) {} + + Services.obs.notifyObservers(window, "select-dialog-loaded"); +} + +function dialogOK() { + propBag.setProperty("selected", listBox.selectedIndex); + propBag.setProperty("ok", true); +} + +function dialogDoubleClick() { + dialogOK(); + window.close(); +} + +document.addEventListener("DOMContentLoaded", onDCL); +window.addEventListener("load", onLoad, { once: true }); diff --git a/toolkit/components/prompts/content/selectDialog.xhtml b/toolkit/components/prompts/content/selectDialog.xhtml new file mode 100644 index 0000000000..27963ebd57 --- /dev/null +++ b/toolkit/components/prompts/content/selectDialog.xhtml @@ -0,0 +1,21 @@ + + + + + + + + + + diff --git a/toolkit/components/prompts/test/bug625187_iframe.html b/toolkit/components/prompts/test/bug625187_iframe.html new file mode 100644 index 0000000000..740d59a617 --- /dev/null +++ b/toolkit/components/prompts/test/bug625187_iframe.html @@ -0,0 +1,16 @@ + + + Test for Bug 625187 - the iframe + + + +

+

+ + diff --git a/toolkit/components/prompts/test/chrome.ini b/toolkit/components/prompts/test/chrome.ini new file mode 100644 index 0000000000..90eee3a761 --- /dev/null +++ b/toolkit/components/prompts/test/chrome.ini @@ -0,0 +1,11 @@ +[DEFAULT] +support-files = + prompt_common.js + chromeScript.js + +[test_modal_prompts.html] +skip-if = + toolkit == 'android' #android: TIMED_OUT + os == 'linux' && (debug || asan || tsan) +[test_modal_select.html] +skip-if = toolkit == 'android' #android: TIMED_OUT diff --git a/toolkit/components/prompts/test/chromeScript.js b/toolkit/components/prompts/test/chromeScript.js new file mode 100644 index 0000000000..5bdf3606d4 --- /dev/null +++ b/toolkit/components/prompts/test/chromeScript.js @@ -0,0 +1,353 @@ +/* eslint-env mozilla/chrome-script */ + +const { clearInterval, setInterval, setTimeout } = ChromeUtils.importESModule( + "resource://gre/modules/Timer.sys.mjs" +); + +const { BrowserTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/BrowserTestUtils.sys.mjs" +); + +var tabSubDialogsEnabled = Services.prefs.getBoolPref( + "prompts.tabChromePromptSubDialog", + false +); + +var contentPromptSubdialogsEnabled = Services.prefs.getBoolPref( + "prompts.contentPromptSubDialog", + false +); + +// Define these to make EventUtils happy. +let window = this; +let parent = {}; + +let EventUtils = {}; +Services.scriptloader.loadSubScript( + "chrome://mochikit/content/tests/SimpleTest/EventUtils.js", + EventUtils +); + +addMessageListener("handlePrompt", msg => { + info("Received handlePrompt message"); + handlePromptWhenItAppears(msg.action, msg.modalType, msg.isSelect); +}); + +async function handlePromptWhenItAppears(action, modalType, isSelect) { + try { + if (!(await handlePrompt(action, modalType, isSelect))) { + setTimeout( + () => this.handlePromptWhenItAppears(action, modalType, isSelect), + 100 + ); + } + } catch (e) { + info(`handlePromptWhenItAppears: exception: ${e}`); + } +} + +function checkTabModal(prompt, browser) { + let doc = browser.ownerDocument; + + let { bottom: toolboxBottom } = doc + .getElementById("navigator-toolbox") + .getBoundingClientRect(); + + let { mainContainer } = prompt.ui; + + let { x, y } = mainContainer.getBoundingClientRect(); + ok(y > 0, "Container should have y > 0"); + // Inset by 1px since the corner point doesn't return the frame due to the + // border-radius. + is( + doc.elementFromPoint(x + 1, y + 1).parentNode, + mainContainer, + "Check tabmodalprompt is visible" + ); + + info("Click to the left of the dialog over the content area"); + isnot( + doc.elementFromPoint(x - 10, y + 50), + browser, + "Check clicks on the content area don't go to the browser" + ); + is( + doc.elementFromPoint(x - 10, y + 50), + prompt.element, + "Check clicks on the content area go to the prompt dialog background" + ); + + if (prompt.args.modalType == Ci.nsIPrompt.MODAL_TYPE_TAB) { + ok( + y <= toolboxBottom - 5, + "Dialog should overlap the toolbox by at least 5px" + ); + } else { + ok(y >= toolboxBottom, "Dialog must not overlap with toolbox."); + } + + ok( + browser.hasAttribute("tabmodalPromptShowing"), + "Check browser has @tabmodalPromptShowing" + ); +} + +async function handlePrompt(action, modalType, isSelect) { + info(`handlePrompt: modalType=${modalType}`); + + let ui; + let browserWin = Services.wm.getMostRecentWindow("navigator:browser"); + + if ( + (!contentPromptSubdialogsEnabled && + modalType === Services.prompt.MODAL_TYPE_CONTENT) || + (!tabSubDialogsEnabled && modalType === Services.prompt.MODAL_TYPE_TAB) + ) { + let gBrowser = browserWin.gBrowser; + let promptManager = gBrowser.getTabModalPromptBox(gBrowser.selectedBrowser); + let prompts = promptManager.listPrompts(); + if (!prompts.length) { + info("handlePrompt: no prompt found. retrying..."); + return false; // try again in a bit + } + + ui = prompts[0].Dialog.ui; + checkTabModal(prompts[0], gBrowser.selectedBrowser); + } else { + let doc = getDialogDoc(); + if (!doc) { + info("handlePrompt: no document found. retrying..."); + return false; // try again in a bit + } + + if (isSelect) { + ui = doc; + } else { + ui = doc.defaultView.Dialog.ui; + } + } + + let dialogClosed = BrowserTestUtils.waitForEvent( + browserWin, + "DOMModalDialogClosed" + ); + + let promptState; + if (isSelect) { + promptState = getSelectState(ui); + dismissSelect(ui, action); + } else { + promptState = getPromptState(ui); + dismissPrompt(ui, action); + } + + // Wait until the prompt has been closed before sending callback msg. + // Unless the test explicitly doesn't request a button click. + if (action.buttonClick !== "none") { + info(`handlePrompt: wait for dialogClosed`); + await dialogClosed; + } + + info(`handlePrompt: send promptHandled`); + sendAsyncMessage("promptHandled", { promptState }); + return true; +} + +function getSelectState(ui) { + let listbox = ui.getElementById("list"); + + let state = {}; + state.msg = ui.getElementById("info.txt").value; + state.selectedIndex = listbox.selectedIndex; + state.items = []; + + for (let i = 0; i < listbox.itemCount; i++) { + let item = listbox.getItemAtIndex(i).label; + state.items.push(item); + } + + return state; +} + +function getPromptState(ui) { + let state = {}; + state.msg = ui.infoBody.textContent; + state.infoRowHidden = ui.infoRow?.hidden || false; + state.titleHidden = ui.infoTitle.hidden; + state.textHidden = ui.loginContainer.hidden; + state.passHidden = ui.password1Container.hidden; + state.checkHidden = ui.checkboxContainer.hidden; + state.checkMsg = state.checkHidden ? "" : ui.checkbox.label; + state.checked = state.checkHidden ? false : ui.checkbox.checked; + // TabModalPrompts don't have an infoIcon + state.iconClass = ui.infoIcon ? ui.infoIcon.className : null; + state.textValue = ui.loginTextbox.value; + state.passValue = ui.password1Textbox.value; + + state.butt0Label = ui.button0.label; + state.butt1Label = ui.button1.label; + state.butt2Label = ui.button2.label; + + state.butt0Disabled = ui.button0.disabled; + state.butt1Disabled = ui.button1.disabled; + state.butt2Disabled = ui.button2.disabled; + + function isDefaultButton(b) { + return b.hasAttribute("default") && b.getAttribute("default") == "true"; + } + state.defButton0 = isDefaultButton(ui.button0); + state.defButton1 = isDefaultButton(ui.button1); + state.defButton2 = isDefaultButton(ui.button2); + + let e = Services.focus.focusedElement; + + if (e == null) { + state.focused = null; + } else if (ui.button0.isSameNode(e)) { + state.focused = "button0"; + } else if (ui.button1.isSameNode(e)) { + state.focused = "button1"; + } else if (ui.button2.isSameNode(e)) { + state.focused = "button2"; + } else if (e.isSameNode(ui.loginTextbox)) { + state.focused = "textField"; + } else if (e.isSameNode(ui.password1Textbox)) { + state.focused = "passField"; + } else if (ui.infoBody.isSameNode(e)) { + state.focused = "infoBody"; + } else { + state.focused = + "ERROR: unexpected element focused: " + (e ? e.localName : ""); + } + + let treeOwner = + ui.prompt && ui.prompt.docShell && ui.prompt.docShell.treeOwner; + if (treeOwner && treeOwner.QueryInterface(Ci.nsIInterfaceRequestor)) { + // Check that the dialog is modal, chrome and dependent; + // We can't just check window.opener because that'll be + // a content window, which therefore isn't exposed (it'll lie and + // be null). + let flags = treeOwner.getInterface(Ci.nsIAppWindow).chromeFlags; + state.chrome = (flags & Ci.nsIWebBrowserChrome.CHROME_OPENAS_CHROME) != 0; + state.dialog = (flags & Ci.nsIWebBrowserChrome.CHROME_OPENAS_DIALOG) != 0; + state.chromeDependent = + (flags & Ci.nsIWebBrowserChrome.CHROME_DEPENDENT) != 0; + let wbc = treeOwner.getInterface(Ci.nsIWebBrowserChrome); + state.isWindowModal = wbc.isWindowModal(); + } + + // Check the dialog is a common dialog document and has been embedded. + let isEmbedded = !!ui.prompt?.docShell?.chromeEventHandler; + let isCommonDialogDoc = + getDialogDoc()?.location.href.includes("commonDialog.xhtml"); + state.isSubDialogPrompt = isCommonDialogDoc && isEmbedded; + state.showCallerOrigin = ui.prompt.args.showCallerOrigin; + + return state; +} + +function dismissSelect(ui, action) { + let dialog = ui.getElementsByTagName("dialog")[0]; + let listbox = ui.getElementById("list"); + + if (action.selectItem) { + listbox.selectedIndex = 1; + } + + if (action.buttonClick == "ok") { + dialog.acceptDialog(); + } else if (action.buttonClick == "cancel") { + dialog.cancelDialog(); + } +} + +function dismissPrompt(ui, action) { + info(`dismissPrompt: action=${JSON.stringify(action)}`); + if (action.setCheckbox) { + // Annoyingly, the prompt code is driven by oncommand. + ui.checkbox.checked = true; + ui.checkbox.doCommand(); + } + + if ("textField" in action) { + ui.loginTextbox.setAttribute("value", action.textField); + } + + if ("passField" in action) { + ui.password1Textbox.setAttribute("value", action.passField); + } + + switch (action.buttonClick) { + case "ok": + case 0: + ui.button0.click(); + break; + case "cancel": + case 1: + ui.button1.click(); + break; + case 2: + ui.button2.click(); + break; + case "ESC": + // XXX This is assuming tab-modal. + let browserWin = Services.wm.getMostRecentWindow("navigator:browser"); + EventUtils.synthesizeKey("KEY_Escape", {}, browserWin); + break; + case "pollOK": + // Buttons are disabled at the moment, poll until they're reenabled. + // Can't use setInterval here, because the window's in a modal state + // and thus DOM events are suppressed. + let interval = setInterval(() => { + if (ui.button0.disabled) { + return; + } + ui.button0.click(); + clearInterval(interval); + }, 100); + break; + case "none": + break; + + default: + throw new Error("dismissPrompt action listed unknown button."); + } +} + +function getDialogDoc() { + // Trudge through all the open windows, until we find the one + // that has either commonDialog.xhtml or selectDialog.xhtml loaded. + // var enumerator = Services.wm.getEnumerator("navigator:browser"); + for (let { docShell } of Services.wm.getEnumerator(null)) { + var containedDocShells = docShell.getAllDocShellsInSubtree( + docShell.typeChrome, + docShell.ENUMERATE_FORWARDS + ); + for (let childDocShell of containedDocShells) { + // Get the corresponding document for this docshell + // We don't want it if it's not done loading. + if (childDocShell.busyFlags != Ci.nsIDocShell.BUSY_FLAGS_NONE) { + continue; + } + var childDoc = childDocShell.contentViewer.DOMDocument; + + if ( + childDoc.location.href != + "chrome://global/content/commonDialog.xhtml" && + childDoc.location.href != "chrome://global/content/selectDialog.xhtml" + ) { + continue; + } + + // We're expecting the dialog to be focused. If it's not yet, try later. + // (In particular, this is needed on Linux to reliably check focused elements.) + if (Services.focus.focusedWindow != childDoc.defaultView) { + continue; + } + + return childDoc; + } + } + + return null; +} diff --git a/toolkit/components/prompts/test/mochitest.ini b/toolkit/components/prompts/test/mochitest.ini new file mode 100644 index 0000000000..9334a9bdb4 --- /dev/null +++ b/toolkit/components/prompts/test/mochitest.ini @@ -0,0 +1,19 @@ +[DEFAULT] +support-files = + ../../passwordmgr/test/authenticate.sjs + bug619644_inner.html + bug625187_iframe.html + prompt_common.js + chromeScript.js + +[test_bug619644.html] +skip-if = toolkit == 'android' # No tab prompts on android +[test_bug620145.html] +skip-if = toolkit == 'android' #TIMED_OUT +[test_subresources_prompts.html] +skip-if = + toolkit == 'android' || verify + http3 +fail-if = xorigin +[test_dom_prompts.html] +skip-if = toolkit == 'android' #android: bug 1267092 diff --git a/toolkit/components/prompts/test/prompt_common.js b/toolkit/components/prompts/test/prompt_common.js new file mode 100644 index 0000000000..4b3a2262aa --- /dev/null +++ b/toolkit/components/prompts/test/prompt_common.js @@ -0,0 +1,445 @@ +const { Cc, Ci, Cu: ChromeUtils } = SpecialPowers; + +/** + * Converts a property bag to object. + * @param {nsIPropertyBag} bag - The property bag to convert + * @returns {Object} - The object representation of the nsIPropertyBag + */ +function propBagToObject(bag) { + if (!(bag instanceof Ci.nsIPropertyBag)) { + throw new TypeError("Not a property bag"); + } + let result = {}; + for (let { name, value } of bag.enumerator) { + result[name] = value; + } + return result; +} + +var modalType; +var tabSubDialogsEnabled = SpecialPowers.Services.prefs.getBoolPref( + "prompts.tabChromePromptSubDialog", + false +); +var contentSubDialogsEnabled = SpecialPowers.Services.prefs.getBoolPref( + "prompts.contentPromptSubDialog", + false +); +var isSelectDialog = false; +var isOSX = "nsILocalFileMac" in SpecialPowers.Ci; +var isE10S = SpecialPowers.Services.appinfo.processType == 2; + +var gChromeScript = SpecialPowers.loadChromeScript( + SimpleTest.getTestFileURL("chromeScript.js") +); +SimpleTest.registerCleanupFunction(() => gChromeScript.destroy()); + +async function runPromptCombinations(window, testFunc) { + let util = new PromptTestUtil(window); + let run = () => { + info( + `Running tests (modalType=${modalType}, usePromptService=${util.usePromptService}, useBrowsingContext=${util.useBrowsingContext}, useAsync=${util.useAsync})` + ); + return testFunc(util); + }; + + // Prompt service with dom window parent only supports window prompts + util.usePromptService = true; + util.useBrowsingContext = false; + util.modalType = Ci.nsIPrompt.MODAL_TYPE_WINDOW; + modalType = util.modalType; + util.useAsync = false; + await run(); + + let modalTypes = [ + Ci.nsIPrompt.MODAL_TYPE_WINDOW, + Ci.nsIPrompt.MODAL_TYPE_TAB, + Ci.nsIPrompt.MODAL_TYPE_CONTENT, + ]; + + for (let type of modalTypes) { + util.modalType = type; + modalType = type; + + // Prompt service with browsing context sync + util.usePromptService = true; + util.useBrowsingContext = true; + util.useAsync = false; + await run(); + + // Prompt service with browsing context async + util.usePromptService = true; + util.useBrowsingContext = true; + util.useAsync = true; + await run(); + + // nsIPrompt + // modalType is set via nsIWritablePropertyBag (legacy) + util.usePromptService = false; + util.useBrowsingContext = false; + util.useAsync = false; + await run(); + } +} + +class PromptTestUtil { + constructor(window) { + this.window = window; + this.browsingContext = + SpecialPowers.wrap(window).windowGlobalChild.browsingContext; + this.promptService = SpecialPowers.Services.prompt; + this.nsPrompt = Cc["@mozilla.org/prompter;1"] + .getService(Ci.nsIPromptFactory) + .getPrompt(window, Ci.nsIPrompt); + + this.usePromptService = null; + this.useBrowsingContext = null; + this.useAsync = null; + this.modalType = null; + } + + get _prompter() { + if (this.usePromptService) { + return this.promptService; + } + return this.nsPrompt; + } + + async prompt(funcName, promptArgs) { + if ( + this.useBrowsingContext == null || + this.usePromptService == null || + this.useAsync == null || + this.modalType == null + ) { + throw new Error("Not initialized"); + } + let args = []; + if (this.usePromptService) { + if (this.useBrowsingContext) { + if (this.useAsync) { + funcName = `async${funcName[0].toUpperCase()}${funcName.substring( + 1 + )}`; + } else { + funcName += "BC"; + } + args = [this.browsingContext, this.modalType]; + } else { + args = [this.window]; + } + } else { + let bag = this.nsPrompt.QueryInterface(Ci.nsIWritablePropertyBag2); + bag.setPropertyAsUint32("modalType", this.modalType); + } + // Append the prompt arguments + args = args.concat(promptArgs); + + let interfaceName = this.usePromptService ? "Services.prompt" : "prompt"; + ok( + this._prompter[funcName], + `${interfaceName} should have method ${funcName}.` + ); + + info(`Calling ${interfaceName}.${funcName}(${args})`); + let result = this._prompter[funcName](...args); + is( + this.useAsync, + result != null && + result.constructor != null && + result.constructor.name === "Promise", + "If method is async it should return a promise." + ); + + if (this.useAsync) { + let propBag = await result; + return propBag && propBagToObject(propBag); + } + return result; + } +} + +function onloadPromiseFor(id) { + var iframe = document.getElementById(id); + return new Promise(resolve => { + iframe.addEventListener( + "load", + function (e) { + resolve(true); + }, + { once: true } + ); + }); +} + +/** + * Take an action on the next prompt that appears without checking the state in advance. + * This is useful when the action doesn't depend on which prompt is shown and you + * are expecting multiple prompts at once in an indeterminate order. + * If you know the state of the prompt you expect you should use `handlePrompt` instead. + * @param {object} action defining how to handle the prompt + * @returns {Promise} resolving with the prompt state. + */ +function handlePromptWithoutChecks(action) { + return new Promise(resolve => { + gChromeScript.addMessageListener("promptHandled", function handled(msg) { + gChromeScript.removeMessageListener("promptHandled", handled); + resolve(msg.promptState); + }); + gChromeScript.sendAsyncMessage("handlePrompt", { action, modalType }); + }); +} + +async function handlePrompt(state, action) { + let actualState = await handlePromptWithoutChecks(action); + checkPromptState(actualState, state); +} + +function checkPromptState(promptState, expectedState) { + info(`checkPromptState: Expected: ${expectedState.msg}`); + // XXX check title? OS X has title in content + is(promptState.msg, expectedState.msg, "Checking expected message"); + + let isOldContentPrompt = + !promptState.isSubDialogPrompt && + modalType === Ci.nsIPrompt.MODAL_TYPE_CONTENT; + + if (isOldContentPrompt && !promptState.showCallerOrigin) { + ok( + promptState.titleHidden, + "The title should be hidden for content prompts opened with tab modal prompt." + ); + } else if ( + isOSX || + promptState.isSubDialogPrompt || + promptState.showCallerOrigin + ) { + ok( + !promptState.titleHidden, + "Checking title always visible on OS X or when opened with common dialog" + ); + } else { + is( + promptState.titleHidden, + expectedState.titleHidden, + "Checking title visibility" + ); + } + is( + promptState.textHidden, + expectedState.textHidden, + "Checking textbox visibility" + ); + is( + promptState.passHidden, + expectedState.passHidden, + "Checking passbox visibility" + ); + is( + promptState.checkHidden, + expectedState.checkHidden, + "Checking checkbox visibility" + ); + is(promptState.checkMsg, expectedState.checkMsg, "Checking checkbox label"); + is(promptState.checked, expectedState.checked, "Checking checkbox checked"); + if ( + modalType === Ci.nsIPrompt.MODAL_TYPE_WINDOW || + (modalType === Ci.nsIPrompt.MODAL_TYPE_TAB && tabSubDialogsEnabled) + ) { + is( + promptState.iconClass, + expectedState.iconClass, + "Checking expected icon CSS class" + ); + } + is(promptState.textValue, expectedState.textValue, "Checking textbox value"); + is(promptState.passValue, expectedState.passValue, "Checking passbox value"); + + if (expectedState.butt0Label) { + is( + promptState.butt0Label, + expectedState.butt0Label, + "Checking accept-button label" + ); + } + if (expectedState.butt1Label) { + is( + promptState.butt1Label, + expectedState.butt1Label, + "Checking cancel-button label" + ); + } + if (expectedState.butt2Label) { + is( + promptState.butt2Label, + expectedState.butt2Label, + "Checking extra1-button label" + ); + } + + // For prompts with a time-delay button. + if (expectedState.butt0Disabled) { + is(promptState.butt0Disabled, true, "Checking accept-button is disabled"); + is( + promptState.butt1Disabled, + false, + "Checking cancel-button isn't disabled" + ); + } + + is( + promptState.defButton0, + expectedState.defButton == "button0", + "checking button0 default" + ); + is( + promptState.defButton1, + expectedState.defButton == "button1", + "checking button1 default" + ); + is( + promptState.defButton2, + expectedState.defButton == "button2", + "checking button2 default" + ); + + if ( + isOSX && + expectedState.focused && + expectedState.focused.startsWith("button") && + !promptState.infoRowHidden + ) { + is( + promptState.focused, + "infoBody", + "buttons don't focus on OS X, but infoBody does instead" + ); + } else { + is(promptState.focused, expectedState.focused, "Checking focused element"); + } + + if (expectedState.hasOwnProperty("chrome")) { + is( + promptState.chrome, + expectedState.chrome, + "Dialog should be opened as chrome" + ); + } + if (expectedState.hasOwnProperty("dialog")) { + is( + promptState.dialog, + expectedState.dialog, + "Dialog should be opened as a dialog" + ); + } + if (expectedState.hasOwnProperty("chromeDependent")) { + is( + promptState.chromeDependent, + expectedState.chromeDependent, + "Dialog should be opened as dependent" + ); + } + if (expectedState.hasOwnProperty("isWindowModal")) { + is( + promptState.isWindowModal, + expectedState.isWindowModal, + "Dialog should be modal" + ); + } +} + +function checkEchoedAuthInfo(expectedState, browsingContext) { + return SpecialPowers.spawn( + browsingContext, + [expectedState.user, expectedState.pass], + (expectedUser, expectedPass) => { + let doc = this.content.document; + + // The server echos back the HTTP auth info it received. + let username = doc.getElementById("user").textContent; + let password = doc.getElementById("pass").textContent; + let authok = doc.getElementById("ok").textContent; + + Assert.equal(authok, "PASS", "Checking for successful authentication"); + Assert.equal(username, expectedUser, "Checking for echoed username"); + Assert.equal(password, expectedPass, "Checking for echoed password"); + } + ); +} + +/** + * Create a Proxy to relay method calls on an nsIAuthPrompt[2] prompter to a chrome script which can + * perform the calls in the parent. Out and inout params will be copied back from the parent to + * content. + * + * @param chromeScript The reference to the chrome script that will listen to `proxyPrompter` + * messages in the parent and call the `methodName` method. + * The return value from the message handler should be an object with properties: + * `rv` - containing the return value of the method call. + * `args` - containing the array of arguments passed to the method since out or inout ones could have + * been modified. + */ +function PrompterProxy(chromeScript) { + return new Proxy( + {}, + { + get(target, prop, receiver) { + return (...args) => { + // Array of indices of out/inout params to copy from the parent back to the caller. + let outParams = []; + + switch (prop) { + case "prompt": { + outParams = [/* result */ 5]; + break; + } + case "promptAuth": { + outParams = []; + break; + } + case "promptPassword": + case "asyncPromptPassword": { + outParams = [/* pwd */ 4]; + break; + } + case "promptUsernameAndPassword": + case "asyncPromptUsernameAndPassword": { + outParams = [/* user */ 4, /* pwd */ 5]; + break; + } + default: { + throw new Error("Unknown nsIAuthPrompt method"); + } + } + + let result; + chromeScript + .sendQuery("proxyPrompter", { + args, + methodName: prop, + }) + .then(val => { + result = val; + }); + SpecialPowers.Services.tm.spinEventLoopUntil( + "Test(prompt_common.js:get)", + () => result + ); + + for (let outParam of outParams) { + // Copy the out or inout param value over the original + args[outParam].value = result.args[outParam].value; + } + + if (prop == "promptAuth") { + args[2].username = result.args[2].username; + args[2].password = result.args[2].password; + args[2].domain = result.args[2].domain; + } + + return result.rv; + }; + }, + } + ); +} diff --git a/toolkit/components/prompts/test/test_bug619644.html b/toolkit/components/prompts/test/test_bug619644.html new file mode 100644 index 0000000000..2b424c71a6 --- /dev/null +++ b/toolkit/components/prompts/test/test_bug619644.html @@ -0,0 +1,74 @@ + + + + + Test for Bug 619644 + + + + + + + +Mozilla Bug 619644 +
+
+
+ + diff --git a/toolkit/components/prompts/test/test_bug620145.html b/toolkit/components/prompts/test/test_bug620145.html new file mode 100644 index 0000000000..3894a528ae --- /dev/null +++ b/toolkit/components/prompts/test/test_bug620145.html @@ -0,0 +1,96 @@ + + + Test for Bug 620145 + + + + + + + +Mozilla Bug 620145 +
+
+ +
+ This is a short piece of text used for testing that mouse selecting is + stopped when an alert appears. +
+
+ This is another short piece of text used for testing that mouse selecting is + stopped when an alert appears. +
+ + + + + + diff --git a/toolkit/components/prompts/test/test_dom_prompts.html b/toolkit/components/prompts/test/test_dom_prompts.html new file mode 100644 index 0000000000..95595b8df2 --- /dev/null +++ b/toolkit/components/prompts/test/test_dom_prompts.html @@ -0,0 +1,207 @@ + + + Test for DOM prompts + + + + + + + +
+
+ + + + + diff --git a/toolkit/components/prompts/test/test_modal_prompts.html b/toolkit/components/prompts/test/test_modal_prompts.html new file mode 100644 index 0000000000..df3438dbba --- /dev/null +++ b/toolkit/components/prompts/test/test_modal_prompts.html @@ -0,0 +1,1311 @@ + + + + + Modal Prompts Test + + + + +Prompter tests: modal prompts +

+ + + +
+
+
+ + diff --git a/toolkit/components/prompts/test/test_modal_select.html b/toolkit/components/prompts/test/test_modal_select.html new file mode 100644 index 0000000000..27688cf329 --- /dev/null +++ b/toolkit/components/prompts/test/test_modal_select.html @@ -0,0 +1,138 @@ + + + + Modal Prompts Test + + + + +Prompter tests: modal prompts +

+ + + +
+
+
+ + diff --git a/toolkit/components/prompts/test/test_subresources_prompts.html b/toolkit/components/prompts/test/test_subresources_prompts.html new file mode 100644 index 0000000000..b71ad0694e --- /dev/null +++ b/toolkit/components/prompts/test/test_subresources_prompts.html @@ -0,0 +1,200 @@ + + + Test subresources prompts (Bug 625187 and bug 1230462) + + + + + + + + +Mozilla Bug 625187 +Mozilla Bug 1230462 + +

+ + + + + + + +

+
+
+
+
-- 
cgit v1.2.3