diff options
Diffstat (limited to 'browser/extensions/screenshots/test')
8 files changed, 662 insertions, 0 deletions
diff --git a/browser/extensions/screenshots/test/browser/browser.ini b/browser/extensions/screenshots/test/browser/browser.ini new file mode 100644 index 0000000000..cdca87ae88 --- /dev/null +++ b/browser/extensions/screenshots/test/browser/browser.ini @@ -0,0 +1,22 @@ +[DEFAULT] +prefs = + # The Screenshots extension is disabled by default in Mochitests. We re-enable + # it here, since it's a more realistic configuration. + extensions.screenshots.disabled=false + +[browser_screenshot_button.js] +[browser_screenshots_dimensions.js] +https_first_disabled = true +# Bug 1714237 Disabled on tsan due to timeouts interacting with the UI +# Bug 1714210 Disabled on headless which doesnt support image data on the clipboard +skip-if = + headless || tsan + os == 'win' # Bug 1714295 + os == 'linux' && bits == 64 # Bug 1714295 +[browser_screenshots_download.js] +[browser_screenshots_injection.js] + +support-files = + head.js + injection-page.html + green2vh.html diff --git a/browser/extensions/screenshots/test/browser/browser_screenshot_button.js b/browser/extensions/screenshots/test/browser/browser_screenshot_button.js new file mode 100644 index 0000000000..84c5758a1d --- /dev/null +++ b/browser/extensions/screenshots/test/browser/browser_screenshot_button.js @@ -0,0 +1,75 @@ +/* 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"; + +add_task(async function testScreenshotButtonDisabled() { + info("Test the Screenshots button in the panel"); + + CustomizableUI.addWidgetToArea( + "screenshot-button", + CustomizableUI.AREA_NAVBAR + ); + + let screenshotBtn = document.getElementById("screenshot-button"); + Assert.ok(screenshotBtn, "The screenshots button was added to the nav bar"); + + await BrowserTestUtils.withNewTab("https://example.com/", () => { + Assert.equal( + screenshotBtn.disabled, + false, + "Screenshots button is enabled" + ); + }); + await BrowserTestUtils.withNewTab("about:home", () => { + Assert.equal( + screenshotBtn.disabled, + true, + "Screenshots button is now disabled" + ); + }); +}); + +add_task(async function test_disabledMultiWindow() { + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: TEST_GREEN_PAGE, + }, + async browser => { + let helper = new ScreenshotsHelper(browser); + await helper.triggerUIFromToolbar(); + + let screenshotBtn = document.getElementById("screenshot-button"); + Assert.ok( + screenshotBtn, + "The screenshots button was added to the nav bar" + ); + + info("Waiting for the preselect UI"); + await helper.waitForUIContent( + helper.selector.preselectIframe, + helper.selector.fullPageButton + ); + + let newWin = await BrowserTestUtils.openNewBrowserWindow(); + await BrowserTestUtils.closeWindow(newWin); + + let deactivatedPromise = helper.waitForToolbarButtonDeactivation(); + await deactivatedPromise; + info("Screenshots is deactivated"); + + await EventUtils.synthesizeAndWaitKey("VK_ESCAPE", {}); + await BrowserTestUtils.waitForCondition(() => { + return !screenshotBtn.disabled; + }); + + Assert.equal( + screenshotBtn.disabled, + false, + "Screenshots button is enabled" + ); + } + ); +}); diff --git a/browser/extensions/screenshots/test/browser/browser_screenshots_dimensions.js b/browser/extensions/screenshots/test/browser/browser_screenshots_dimensions.js new file mode 100644 index 0000000000..8b32d7968b --- /dev/null +++ b/browser/extensions/screenshots/test/browser/browser_screenshots_dimensions.js @@ -0,0 +1,109 @@ +add_task(async function test_fullPageScreenshot() { + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: TEST_GREEN_PAGE, + }, + async browser => { + let helper = new ScreenshotsHelper(browser); + let contentInfo = await helper.getContentDimensions(); + ok(contentInfo, "Got dimensions back from the content"); + + await helper.triggerUIFromToolbar(); + + await helper.clickUIElement( + helper.selector.preselectIframe, + helper.selector.fullPageButton + ); + + info("Waiting for the preview UI and the copy button"); + await helper.waitForUIContent( + helper.selector.previewIframe, + helper.selector.copyButton + ); + + let deactivatedPromise = helper.waitForToolbarButtonDeactivation(); + let clipboardChanged = waitForRawClipboardChange(); + + await helper.clickUIElement( + helper.selector.previewIframe, + helper.selector.copyButton + ); + + info("Waiting for clipboard change"); + await clipboardChanged; + + await deactivatedPromise; + info("Screenshots is deactivated"); + + let result = await getImageSizeFromClipboard(browser); + is( + result.width, + contentInfo.documentWidth, + "Got expected screenshot width" + ); + is( + result.height, + contentInfo.documentHeight, + "Got expected screenshot height" + ); + } + ); +}); + +add_task(async function test_visiblePageScreenshot() { + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: TEST_GREEN_PAGE, + }, + async browser => { + const DEVICE_PIXEL_RATIO = window.devicePixelRatio; + let helper = new ScreenshotsHelper(browser); + let contentInfo = await helper.getContentDimensions(); + ok(contentInfo, "Got dimensions back from the content"); + + await helper.triggerUIFromToolbar(); + + await helper.clickUIElement( + helper.selector.preselectIframe, + helper.selector.visiblePageButton + ); + + info("Waiting for the preview UI and the copy button"); + await helper.waitForUIContent( + helper.selector.previewIframe, + helper.selector.copyButton + ); + + let deactivatedPromise = helper.waitForToolbarButtonDeactivation(); + let clipboardChanged = waitForRawClipboardChange(); + + await helper.clickUIElement( + helper.selector.previewIframe, + helper.selector.copyButton + ); + + info("Waiting for clipboard change"); + await clipboardChanged; + + await deactivatedPromise; + info("Screenshots is deactivated"); + + let result = await getImageSizeFromClipboard(browser); + info("result: " + JSON.stringify(result, null, 2)); + info("contentInfo: " + JSON.stringify(contentInfo, null, 2)); + + is( + result.width, + contentInfo.documentWidth * DEVICE_PIXEL_RATIO, + "Got expected screenshot width" + ); + is( + result.height, + contentInfo.clientHeight * DEVICE_PIXEL_RATIO, + "Got expected screenshot height" + ); + } + ); +}); diff --git a/browser/extensions/screenshots/test/browser/browser_screenshots_download.js b/browser/extensions/screenshots/test/browser/browser_screenshots_download.js new file mode 100644 index 0000000000..3e32f06da6 --- /dev/null +++ b/browser/extensions/screenshots/test/browser/browser_screenshots_download.js @@ -0,0 +1,98 @@ +add_task(async function test_fullPageScreenshot() { + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: TEST_GREEN_PAGE, + }, + + async browser => { + const tests = [ + { title: "Green Page", expected: "Green Page.png" }, + { title: "\tA*B\\/+?<>\u200f\x1fC ", expected: "A B__ C.png" }, + { + title: "簡単".repeat(35), + expected: " " + "簡単".repeat(35) + ".png", + }, + { + title: "簡単".repeat(36), + expected: " " + "簡単".repeat(26) + "[...].png", + }, + { + title: "簡単".repeat(56) + "?", + expected: " " + "簡単".repeat(26) + "[...].png", + }, + ]; + + for (let test of tests) { + info("Testing with title " + test.title); + + await SpecialPowers.spawn(browser, [test.title], titleToUse => { + content.document.title = titleToUse; + }); + + let helper = new ScreenshotsHelper(browser); + await helper.triggerUIFromToolbar(); + + await helper.clickUIElement( + helper.selector.preselectIframe, + helper.selector.fullPageButton + ); + + info("Waiting for the preview UI and the download button"); + await helper.waitForUIContent( + helper.selector.previewIframe, + helper.selector.downloadButton + ); + + let publicList = await Downloads.getList(Downloads.PUBLIC); + let downloadPromise = new Promise(resolve => { + let downloadView = { + onDownloadAdded(download) { + publicList.removeView(downloadView); + resolve(download); + }, + }; + + publicList.addView(downloadView); + }); + + await helper.clickUIElement( + helper.selector.previewIframe, + helper.selector.downloadButton + ); + + let download = await downloadPromise; + let filename = PathUtils.filename(download.target.path); + ok( + filename.endsWith(test.expected), + "Used correct filename '" + + filename + + "', expected: '" + + test.expected + + "'" + ); + + await task_resetState(); + } + } + ); +}); + +// This is from browser/components/downloads/test/browser/head.js +async function task_resetState() { + let publicList = await Downloads.getList(Downloads.PUBLIC); + let downloads = await publicList.getAll(); + for (let download of downloads) { + await publicList.remove(download); + await download.finalize(true); + if (await IOUtils.exists(download.target.path)) { + if (Services.appinfo.OS === "WINNT") { + // We need to make the file writable to delete it on Windows. + await IOUtils.setPermissions(download.target.path, 0o600); + } + await IOUtils.remove(download.target.path); + } + } + + DownloadsPanel.hidePanel(); +} diff --git a/browser/extensions/screenshots/test/browser/browser_screenshots_injection.js b/browser/extensions/screenshots/test/browser/browser_screenshots_injection.js new file mode 100644 index 0000000000..dba932a81d --- /dev/null +++ b/browser/extensions/screenshots/test/browser/browser_screenshots_injection.js @@ -0,0 +1,82 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_PATH = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" +); + +/** + * Check that web content cannot break into screenshots. + */ +add_task(async function test_inject_srcdoc() { + // If Screenshots was disabled, enable it just for this test. + const addon = await AddonManager.getAddonByID("screenshots@mozilla.org"); + const isEnabled = addon.enabled; + if (!isEnabled) { + await addon.enable({ allowSystemAddons: true }); + registerCleanupFunction(async () => { + await addon.disable({ allowSystemAddons: true }); + }); + } + + await BrowserTestUtils.withNewTab( + TEST_PATH + "injection-page.html", + async browser => { + // Set up the content hijacking. Do this so we can see it without + // awaiting - the promise should never resolve. + let response = null; + let responsePromise = SpecialPowers.spawn(browser, [], () => { + return new Promise(resolve => { + // We can't pass `resolve` directly because of sandboxing. + // `responseHandler` gets invoked from the content page. + content.wrappedJSObject.responseHandler = Cu.exportFunction(function ( + arg + ) { + resolve(arg); + }, + content); + }); + }).then( + r => { + ok(false, "Should not have gotten HTML but got: " + r); + response = r; + }, + () => { + // Do nothing - we expect this to error when the test finishes + // and the actor is destroyed, while the promise still hasn't + // been resolved. We need to catch it in order not to throw + // uncaught rejection errors and inadvertently fail the test. + } + ); + + let error; + let errorPromise = new Promise(resolve => { + SpecialPowers.registerConsoleListener(msg => { + if ( + msg.message?.match(/iframe URL does not match expected blank.html/) + ) { + error = msg; + resolve(); + } + }); + }); + + // Now try to start the screenshot flow: + CustomizableUI.addWidgetToArea( + "screenshot-button", + CustomizableUI.AREA_NAVBAR + ); + + let screenshotBtn = document.getElementById("screenshot-button"); + screenshotBtn.click(); + await Promise.race([errorPromise, responsePromise]); + ok(error, "Should get the relevant error: " + error?.message); + ok(!response, "Should not get a response from the webpage."); + + SpecialPowers.postConsoleSentinel(); + } + ); +}); diff --git a/browser/extensions/screenshots/test/browser/green2vh.html b/browser/extensions/screenshots/test/browser/green2vh.html new file mode 100644 index 0000000000..bb25e2a021 --- /dev/null +++ b/browser/extensions/screenshots/test/browser/green2vh.html @@ -0,0 +1,23 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <style> + html, body { + padding: 0; + margin: 0; + } + body { + background: rgb(0,255,0); + min-height: 200vh; + } + </style> + <script> + window.addEventListener("DOMContentLoaded", () => { + console.log(window.screen); + }); + </script> +</head> +<body> +</body> +</html> diff --git a/browser/extensions/screenshots/test/browser/head.js b/browser/extensions/screenshots/test/browser/head.js new file mode 100644 index 0000000000..f999df71af --- /dev/null +++ b/browser/extensions/screenshots/test/browser/head.js @@ -0,0 +1,230 @@ +"use strict"; + +const TEST_ROOT = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "http://example.com" +); +const TEST_GREEN_PAGE = TEST_ROOT + "green2vh.html"; + +const gScreenshotUISelectors = { + preselectIframe: "#firefox-screenshots-preselection-iframe", + fullPageButton: "button.full-page", + visiblePageButton: "button.visible", + previewIframe: "#firefox-screenshots-preview-iframe", + copyButton: "button.highlight-button-copy", + downloadButton: "button.highlight-button-download", +}; + +class ScreenshotsHelper { + constructor(browser) { + this.browser = browser; + this.selector = gScreenshotUISelectors; + } + + get toolbarButton() { + return document.getElementById("screenshot-button"); + } + + async triggerUIFromToolbar() { + let button = this.toolbarButton; + ok( + BrowserTestUtils.is_visible(button), + "The screenshot toolbar button is visible" + ); + EventUtils.synthesizeMouseAtCenter(button, {}); + // Make sure the Screenshots UI is loaded before yielding + await this.waitForUIContent( + this.selector.preselectIframe, + this.selector.fullPageButton + ); + } + + async waitForUIContent(iframeSel, elemSel) { + await SpecialPowers.spawn( + this.browser, + [iframeSel, elemSel], + async function (iframeSelector, elemSelector) { + info( + `in waitForUIContent content function, iframeSelector: ${iframeSelector}, elemSelector: ${elemSelector}` + ); + let iframe; + await ContentTaskUtils.waitForCondition(() => { + iframe = content.document.querySelector(iframeSelector); + if (!iframe || !ContentTaskUtils.is_visible(iframe)) { + info("in waitForUIContent, no visible iframe yet"); + return false; + } + let elem = iframe.contentDocument.querySelector(elemSelector); + info( + "in waitForUIContent, got visible elem: " + + (elem && ContentTaskUtils.is_visible(elem)) + ); + return elem && ContentTaskUtils.is_visible(elem); + }); + // wait a frame for the screenshots UI to finish any init + await new content.Promise(res => content.requestAnimationFrame(res)); + } + ); + } + + async clickUIElement(iframeSel, elemSel) { + await SpecialPowers.spawn( + this.browser, + [iframeSel, elemSel], + async function (iframeSelector, elemSelector) { + info( + `in clickScreenshotsUIElement content function, iframeSelector: ${iframeSelector}, elemSelector: ${elemSelector}` + ); + const EventUtils = ContentTaskUtils.getEventUtils(content); + let iframe = content.document.querySelector(iframeSelector); + let elem = iframe.contentDocument.querySelector(elemSelector); + info(`Found the thing to click: ${elemSelector}: ${!!elem}`); + + EventUtils.synthesizeMouseAtCenter(elem, {}, iframe.contentWindow); + await new content.Promise(res => content.requestAnimationFrame(res)); + } + ); + } + + waitForToolbarButtonDeactivation() { + return BrowserTestUtils.waitForCondition(() => { + return !this.toolbarButton.style.cssText.includes("icon-highlight"); + }); + } + + getContentDimensions() { + return SpecialPowers.spawn(this.browser, [], async function () { + let doc = content.document; + let rect = doc.documentElement.getBoundingClientRect(); + return { + clientHeight: doc.documentElement.clientHeight, + clientWidth: doc.documentElement.clientWidth, + currentURI: doc.documentURI, + documentHeight: Math.round(rect.height), + documentWidth: Math.round(rect.width), + }; + }); + } +} + +function waitForRawClipboardChange() { + const initialClipboardData = Date.now().toString(); + SpecialPowers.clipboardCopyString(initialClipboardData); + + let promiseChanged = BrowserTestUtils.waitForCondition(() => { + let data; + try { + data = getRawClipboardData("image/png"); + } catch (e) { + console.log("Failed to get image/png clipboard data:", e); + return false; + } + return data && initialClipboardData !== data; + }); + return promiseChanged; +} + +function getRawClipboardData(flavor) { + const whichClipboard = Services.clipboard.kGlobalClipboard; + const xferable = Cc["@mozilla.org/widget/transferable;1"].createInstance( + Ci.nsITransferable + ); + xferable.init(null); + xferable.addDataFlavor(flavor); + Services.clipboard.getData(xferable, whichClipboard); + let data = {}; + try { + xferable.getTransferData(flavor, data); + } catch (e) {} + data = data.value || null; + return data; +} + +/** + * A helper that returns the size of the image that was just put into the clipboard by the + * :screenshot command. + * @return The {width, height} dimension object. + */ +async function getImageSizeFromClipboard(browser) { + let flavor = "image/png"; + let image = getRawClipboardData(flavor); + ok(image, "screenshot data exists on the clipboard"); + + // Due to the differences in how images could be stored in the clipboard the + // checks below are needed. The clipboard could already provide the image as + // byte streams or as image container. If it's not possible obtain a + // byte stream, the function throws. + + if (image instanceof Ci.imgIContainer) { + image = Cc["@mozilla.org/image/tools;1"] + .getService(Ci.imgITools) + .encodeImage(image, flavor); + } + + if (!(image instanceof Ci.nsIInputStream)) { + throw new Error("Unable to read image data"); + } + + const binaryStream = Cc["@mozilla.org/binaryinputstream;1"].createInstance( + Ci.nsIBinaryInputStream + ); + binaryStream.setInputStream(image); + const available = binaryStream.available(); + const buffer = new ArrayBuffer(available); + is( + binaryStream.readArrayBuffer(available, buffer), + available, + "Read expected amount of data" + ); + + // We are going to load the image in the content page to measure its size. + // We don't want to insert the image directly in the browser's document + // which could mess all sorts of things up + return SpecialPowers.spawn(browser, [buffer], async function (_buffer) { + const img = content.document.createElement("img"); + const loaded = new Promise(r => { + img.addEventListener("load", r, { once: true }); + }); + + const url = content.URL.createObjectURL( + new Blob([_buffer], { type: "image/png" }) + ); + + img.src = url; + content.document.documentElement.appendChild(img); + + info("Waiting for the clipboard image to load in the content page"); + await loaded; + + img.remove(); + content.URL.revokeObjectURL(url); + + // TODO: could get pixel data as well so we can check colors at specific locations + return { + width: img.width, + height: img.height, + }; + }); +} + +add_setup(async function common_initialize() { + // Ensure Screenshots is initially enabled for all tests + const addon = await AddonManager.getAddonByID("screenshots@mozilla.org"); + const isEnabled = addon.enabled; + if (!isEnabled) { + await addon.enable({ allowSystemAddons: true }); + registerCleanupFunction(async () => { + await addon.disable({ allowSystemAddons: true }); + }); + } + // Add the Screenshots button to the toolbar for all tests + CustomizableUI.addWidgetToArea( + "screenshot-button", + CustomizableUI.AREA_NAVBAR + ); + registerCleanupFunction(() => + CustomizableUI.removeWidgetFromArea("screenshot-button") + ); + let screenshotBtn = document.getElementById("screenshot-button"); + Assert.ok(screenshotBtn, "The screenshots button was added to the nav bar"); +}); diff --git a/browser/extensions/screenshots/test/browser/injection-page.html b/browser/extensions/screenshots/test/browser/injection-page.html new file mode 100644 index 0000000000..ffbda3b25b --- /dev/null +++ b/browser/extensions/screenshots/test/browser/injection-page.html @@ -0,0 +1,23 @@ +<body> +<script> +let callback = function(mutationsList, observer) { + for (let mutation of mutationsList) { + let [added] = mutation.addedNodes; + if (added instanceof HTMLIFrameElement && added.id == "firefox-screenshots-preview-iframe") { + added.srcdoc = "<html></html>"; + // Now we have to wait for the doc to be populated. + let interval = setInterval(() => { + console.log(added.contentDocument.innerHTML); + if (added.contentDocument.body.innerHTML) { + clearInterval(interval); + window.responseHandler(added.contentDocument.body.innerHTML); + } + }, 100); + observer.disconnect(); + } + } +}; +var observer = new MutationObserver(callback); +observer.observe(document.body, {childList: true}); +</script> +</body> |