summaryrefslogtreecommitdiffstats
path: root/toolkit/components/viewsource
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--toolkit/components/viewsource/content/viewSourceUtils.js420
-rw-r--r--toolkit/components/viewsource/jar.mn6
-rw-r--r--toolkit/components/viewsource/moz.build13
-rw-r--r--toolkit/components/viewsource/test/browser/browser.toml30
-rw-r--r--toolkit/components/viewsource/test/browser/browser_bug464222.js17
-rw-r--r--toolkit/components/viewsource/test/browser/browser_bug713810.js31
-rw-r--r--toolkit/components/viewsource/test/browser/browser_contextmenu.js115
-rw-r--r--toolkit/components/viewsource/test/browser/browser_gotoline.js37
-rw-r--r--toolkit/components/viewsource/test/browser/browser_open_docgroup.js41
-rw-r--r--toolkit/components/viewsource/test/browser/browser_partialsource.js46
-rw-r--r--toolkit/components/viewsource/test/browser/browser_srcdoc.js28
-rw-r--r--toolkit/components/viewsource/test/browser/browser_validatefilename.js68
-rw-r--r--toolkit/components/viewsource/test/browser/browser_viewsource_newwindow.js102
-rw-r--r--toolkit/components/viewsource/test/browser/browser_viewsourceprefs.js203
-rw-r--r--toolkit/components/viewsource/test/browser/file_bug464222.html1
-rw-r--r--toolkit/components/viewsource/test/browser/head.js231
-rw-r--r--toolkit/components/viewsource/test/chrome.toml4
-rw-r--r--toolkit/components/viewsource/test/file_empty.html1
-rw-r--r--toolkit/components/viewsource/test/test_bug428653.html45
19 files changed, 1439 insertions, 0 deletions
diff --git a/toolkit/components/viewsource/content/viewSourceUtils.js b/toolkit/components/viewsource/content/viewSourceUtils.js
new file mode 100644
index 0000000000..69d3eff287
--- /dev/null
+++ b/toolkit/components/viewsource/content/viewSourceUtils.js
@@ -0,0 +1,420 @@
+// -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+
+/* 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/. */
+
+/*
+ * To keep the global namespace safe, don't define global variables and
+ * functions in this file.
+ *
+ * This file silently depends on contentAreaUtils.js for getDefaultFileName
+ */
+
+ChromeUtils.defineESModuleGetters(this, {
+ PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
+});
+
+var gViewSourceUtils = {
+ mnsIWebBrowserPersist: Ci.nsIWebBrowserPersist,
+ mnsIWebProgress: Ci.nsIWebProgress,
+ mnsIWebPageDescriptor: Ci.nsIWebPageDescriptor,
+
+ // Get the ViewSource actor for a browsing context.
+ getViewSourceActor(aBrowsingContext) {
+ return aBrowsingContext.currentWindowGlobal.getActor("ViewSource");
+ },
+
+ /**
+ * Get the ViewSourcePage actor.
+ * @param object An object with `browsingContext` field
+ */
+ getPageActor({ browsingContext }) {
+ return browsingContext.currentWindowGlobal.getActor("ViewSourcePage");
+ },
+
+ /**
+ * Opens the view source window.
+ *
+ * @param aArgs (required)
+ * This Object can include the following properties:
+ *
+ * URL (required):
+ * A string URL for the page we'd like to view the source of.
+ * browser (optional):
+ * The browser containing the document that we would like to view the
+ * source of. This is required if outerWindowID is passed.
+ * outerWindowID (optional):
+ * The outerWindowID of the content window containing the document that
+ * we want to view the source of. Pass this if you want to attempt to
+ * load the document source out of the network cache.
+ * lineNumber (optional):
+ * The line number to focus on once the source is loaded.
+ */
+ async viewSource(aArgs) {
+ // Check if external view source is enabled. If so, try it. If it fails,
+ // fallback to internal view source.
+ if (Services.prefs.getBoolPref("view_source.editor.external")) {
+ try {
+ await this.openInExternalEditor(aArgs);
+ return;
+ } catch (data) {}
+ }
+ // Try existing browsers first.
+ let browserWin = Services.wm.getMostRecentWindow("navigator:browser");
+ if (browserWin && browserWin.BrowserViewSourceOfDocument) {
+ browserWin.BrowserViewSourceOfDocument(aArgs);
+ return;
+ }
+ // No browser window created yet, try to create one.
+ let utils = this;
+ Services.ww.registerNotification(function onOpen(win, topic) {
+ if (
+ win.document.documentURI !== "about:blank" ||
+ topic !== "domwindowopened"
+ ) {
+ return;
+ }
+ Services.ww.unregisterNotification(onOpen);
+ win.addEventListener(
+ "load",
+ () => {
+ aArgs.viewSourceBrowser = win.gBrowser.selectedTab.linkedBrowser;
+ utils.viewSourceInBrowser(aArgs);
+ },
+ { once: true }
+ );
+ });
+ window.top.openWebLinkIn("about:blank", "current");
+ },
+
+ /**
+ * Displays view source in the provided <browser>. This allows for non-window
+ * display methods, such as a tab from Firefox.
+ *
+ * @param aArgs
+ * An object with the following properties:
+ *
+ * URL (required):
+ * A string URL for the page we'd like to view the source of.
+ * viewSourceBrowser (required):
+ * The browser to display the view source in.
+ * browser (optional):
+ * The browser containing the document that we would like to view the
+ * source of. This is required if outerWindowID is passed.
+ * outerWindowID (optional):
+ * The outerWindowID of the content window containing the document that
+ * we want to view the source of. Pass this if you want to attempt to
+ * load the document source out of the network cache.
+ * lineNumber (optional):
+ * The line number to focus on once the source is loaded.
+ */
+ viewSourceInBrowser({
+ URL,
+ viewSourceBrowser,
+ browser,
+ outerWindowID,
+ lineNumber,
+ }) {
+ if (!URL) {
+ throw new Error("Must supply a URL when opening view source.");
+ }
+
+ if (browser) {
+ // If we're dealing with a remote browser, then the browser
+ // for view source needs to be remote as well.
+ if (viewSourceBrowser.remoteType != browser.remoteType) {
+ // In this base case, where we are handed a <browser> someone else is
+ // managing, we don't know for sure that it's safe to toggle remoteness.
+ // For view source in a window, this is overridden to actually do the
+ // flip if needed.
+ throw new Error("View source browser's remoteness mismatch");
+ }
+ } else if (outerWindowID) {
+ throw new Error("Must supply the browser if passing the outerWindowID");
+ }
+
+ let viewSourceActor = this.getViewSourceActor(
+ viewSourceBrowser.browsingContext
+ );
+ viewSourceActor.sendAsyncMessage("ViewSource:LoadSource", {
+ URL,
+ outerWindowID,
+ lineNumber,
+ });
+ },
+
+ /**
+ * Displays view source for a selection from some document in the provided
+ * <browser>. This allows for non-window display methods, such as a tab from
+ * Firefox.
+ *
+ * @param aBrowsingContext:
+ * The child browsing context containing the document to view the source of.
+ * @param aGetBrowserFn
+ * A function that will return a browser to open the source in.
+ */
+ async viewPartialSourceInBrowser(aBrowsingContext, aGetBrowserFn) {
+ let sourceActor = this.getViewSourceActor(aBrowsingContext);
+ if (sourceActor) {
+ let data = await sourceActor.sendQuery("ViewSource:GetSelection", {});
+
+ let targetActor = this.getViewSourceActor(
+ aGetBrowserFn().browsingContext
+ );
+ targetActor.sendAsyncMessage("ViewSource:LoadSourceWithSelection", data);
+ }
+ },
+
+ buildEditorArgs(aPath, aLineNumber) {
+ // Determine the command line arguments to pass to the editor.
+ // We currently support a %LINE% placeholder which is set to the passed
+ // line number (or to 0 if there's none)
+ var editorArgs = [];
+ var args = Services.prefs.getCharPref("view_source.editor.args");
+ if (args) {
+ args = args.replace("%LINE%", aLineNumber || "0");
+ // add the arguments to the array (keeping quoted strings intact)
+ const argumentRE = /"([^"]+)"|(\S+)/g;
+ while (argumentRE.test(args)) {
+ editorArgs.push(RegExp.$1 || RegExp.$2);
+ }
+ }
+ editorArgs.push(aPath);
+ return editorArgs;
+ },
+
+ /**
+ * Opens an external editor with the view source content.
+ *
+ * @param aArgs (required)
+ * This Object can include the following properties:
+ *
+ * URL (required):
+ * A string URL for the page we'd like to view the source of.
+ * browser (optional):
+ * The browser containing the document that we would like to view the
+ * source of. This is required if outerWindowID is passed.
+ * outerWindowID (optional):
+ * The outerWindowID of the content window containing the document that
+ * we want to view the source of. Pass this if you want to attempt to
+ * load the document source out of the network cache.
+ * lineNumber (optional):
+ * The line number to focus on once the source is loaded.
+ *
+ * @return Promise
+ * The promise will be resolved or rejected based on whether the
+ * external editor attempt was successful. Either way, the data object
+ * is passed along as well.
+ */
+ openInExternalEditor(aArgs) {
+ return new Promise((resolve, reject) => {
+ let data;
+ let { URL, browser, lineNumber } = aArgs;
+ data = {
+ url: URL,
+ lineNumber,
+ isPrivate: false,
+ };
+ if (browser) {
+ data.doc = {
+ characterSet: browser.characterSet,
+ contentType: browser.documentContentType,
+ title: browser.contentTitle,
+ cookieJarSettings: browser.cookieJarSettings,
+ };
+ data.isPrivate = PrivateBrowsingUtils.isBrowserPrivate(browser);
+ }
+
+ try {
+ var editor = this.getExternalViewSourceEditor();
+ if (!editor) {
+ reject(data);
+ return;
+ }
+
+ // make a uri
+ var charset = data.doc ? data.doc.characterSet : null;
+ var uri = Services.io.newURI(data.url, charset);
+ data.uri = uri;
+
+ var path;
+ var contentType = data.doc ? data.doc.contentType : null;
+ var cookieJarSettings = data.doc ? data.doc.cookieJarSettings : null;
+ if (uri.scheme == "file") {
+ // it's a local file; we can open it directly
+ path = uri.QueryInterface(Ci.nsIFileURL).file.path;
+
+ var editorArgs = this.buildEditorArgs(path, data.lineNumber);
+ editor.runw(false, editorArgs, editorArgs.length);
+ resolve(data);
+ } else {
+ // set up the progress listener with what we know so far
+ this.viewSourceProgressListener.contentLoaded = false;
+ this.viewSourceProgressListener.editor = editor;
+ this.viewSourceProgressListener.resolve = resolve;
+ this.viewSourceProgressListener.reject = reject;
+ this.viewSourceProgressListener.data = data;
+
+ // without a page descriptor, loadPage has no chance of working. download the file.
+ var file = this.getTemporaryFile(uri, data.doc, contentType);
+ this.viewSourceProgressListener.file = file;
+
+ var webBrowserPersist = Cc[
+ "@mozilla.org/embedding/browser/nsWebBrowserPersist;1"
+ ].createInstance(this.mnsIWebBrowserPersist);
+ // the default setting is to not decode. we need to decode.
+ webBrowserPersist.persistFlags =
+ this.mnsIWebBrowserPersist.PERSIST_FLAGS_REPLACE_EXISTING_FILES;
+ webBrowserPersist.progressListener = this.viewSourceProgressListener;
+ let ssm = Services.scriptSecurityManager;
+ let principal = ssm.createContentPrincipal(
+ data.uri,
+ browser.contentPrincipal.originAttributes
+ );
+ webBrowserPersist.saveURI(
+ uri,
+ principal,
+ null,
+ null,
+ cookieJarSettings,
+ null,
+ null,
+ file,
+ Ci.nsIContentPolicy.TYPE_SAVEAS_DOWNLOAD,
+ data.isPrivate
+ );
+
+ let helperService = Cc[
+ "@mozilla.org/uriloader/external-helper-app-service;1"
+ ].getService(Ci.nsPIExternalAppLauncher);
+ if (data.isPrivate) {
+ // register the file to be deleted when possible
+ helperService.deleteTemporaryPrivateFileWhenPossible(file);
+ } else {
+ // register the file to be deleted on app exit
+ helperService.deleteTemporaryFileOnExit(file);
+ }
+ }
+ } catch (ex) {
+ // we failed loading it with the external editor.
+ console.error(ex);
+ reject(data);
+ }
+ });
+ },
+
+ // Returns nsIProcess of the external view source editor or null
+ getExternalViewSourceEditor() {
+ try {
+ let viewSourceAppPath = Services.prefs.getComplexValue(
+ "view_source.editor.path",
+ Ci.nsIFile
+ );
+ let editor = Cc["@mozilla.org/process/util;1"].createInstance(
+ Ci.nsIProcess
+ );
+ editor.init(viewSourceAppPath);
+
+ return editor;
+ } catch (ex) {
+ console.error(ex);
+ }
+
+ return null;
+ },
+
+ viewSourceProgressListener: {
+ mnsIWebProgressListener: Ci.nsIWebProgressListener,
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIWebProgressListener",
+ "nsISupportsWeakReference",
+ ]),
+
+ destroy() {
+ this.editor = null;
+ this.resolve = null;
+ this.reject = null;
+ this.data = null;
+ this.file = null;
+ },
+
+ // This listener is used both for tracking the progress of an HTML parse
+ // in one case and for tracking the progress of nsIWebBrowserPersist in
+ // another case.
+ onStateChange(aProgress, aRequest, aFlag, aStatus) {
+ // once it's done loading...
+ if (aFlag & this.mnsIWebProgressListener.STATE_STOP && aStatus == 0) {
+ // We aren't waiting for the parser. Instead, we are waiting for
+ // an nsIWebBrowserPersist.
+ this.onContentLoaded();
+ }
+ return 0;
+ },
+
+ onContentLoaded() {
+ // The progress listener may call this multiple times, so be sure we only
+ // run once.
+ if (this.contentLoaded) {
+ return;
+ }
+ try {
+ if (!this.file) {
+ throw new Error("View-source progress listener should have a file!");
+ }
+
+ var editorArgs = gViewSourceUtils.buildEditorArgs(
+ this.file.path,
+ this.data.lineNumber
+ );
+ this.editor.runw(false, editorArgs, editorArgs.length);
+
+ this.contentLoaded = true;
+ this.resolve(this.data);
+ } catch (ex) {
+ // we failed loading it with the external editor.
+ console.error(ex);
+ this.reject(this.data);
+ } finally {
+ this.destroy();
+ }
+ },
+
+ editor: null,
+ resolve: null,
+ reject: null,
+ data: null,
+ file: null,
+ },
+
+ // returns an nsIFile for the passed document in the system temp directory
+ getTemporaryFile(aURI, aDocument, aContentType) {
+ // include contentAreaUtils.js in our own context when we first need it
+ if (!this._caUtils) {
+ this._caUtils = {};
+ Services.scriptloader.loadSubScript(
+ "chrome://global/content/contentAreaUtils.js",
+ this._caUtils
+ );
+ }
+
+ var fileName = this._caUtils.getDefaultFileName(
+ null,
+ aURI,
+ aDocument,
+ null
+ );
+
+ const mimeService = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService);
+ fileName = mimeService.validateFileNameForSaving(
+ fileName,
+ aContentType,
+ mimeService.VALIDATE_DEFAULT
+ );
+
+ var tempFile = Services.dirsvc.get("TmpD", Ci.nsIFile);
+ tempFile.append(fileName);
+ return tempFile;
+ },
+};
diff --git a/toolkit/components/viewsource/jar.mn b/toolkit/components/viewsource/jar.mn
new file mode 100644
index 0000000000..e483f3080a
--- /dev/null
+++ b/toolkit/components/viewsource/jar.mn
@@ -0,0 +1,6 @@
+# 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/.
+
+toolkit.jar:
+ content/global/viewSourceUtils.js (content/viewSourceUtils.js)
diff --git a/toolkit/components/viewsource/moz.build b/toolkit/components/viewsource/moz.build
new file mode 100644
index 0000000000..f8e52be577
--- /dev/null
+++ b/toolkit/components/viewsource/moz.build
@@ -0,0 +1,13 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+BROWSER_CHROME_MANIFESTS += ["test/browser/browser.toml"]
+MOCHITEST_CHROME_MANIFESTS += ["test/chrome.toml"]
+
+JAR_MANIFESTS += ["jar.mn"]
+
+with Files("**"):
+ BUG_COMPONENT = ("Toolkit", "View Source")
diff --git a/toolkit/components/viewsource/test/browser/browser.toml b/toolkit/components/viewsource/test/browser/browser.toml
new file mode 100644
index 0000000000..73e3b0b9e0
--- /dev/null
+++ b/toolkit/components/viewsource/test/browser/browser.toml
@@ -0,0 +1,30 @@
+[DEFAULT]
+support-files = [
+ "head.js",
+ "file_bug464222.html",
+]
+
+["browser_bug464222.js"]
+https_first_disabled = true
+
+["browser_bug713810.js"]
+
+["browser_contextmenu.js"]
+skip-if = ["os == 'mac' && !debug"] # Bug 1713913 - new Fission platform triage
+
+["browser_gotoline.js"]
+
+["browser_open_docgroup.js"]
+
+["browser_partialsource.js"]
+skip-if = ["os == 'mac' && !debug"] # Bug 1713913 - new Fission platform triage
+
+["browser_srcdoc.js"]
+
+["browser_validatefilename.js"]
+
+["browser_viewsource_newwindow.js"]
+https_first_disabled = true
+
+["browser_viewsourceprefs.js"]
+skip-if = ["socketprocess_networking && os == 'linux' && !debug"]
diff --git a/toolkit/components/viewsource/test/browser/browser_bug464222.js b/toolkit/components/viewsource/test/browser/browser_bug464222.js
new file mode 100644
index 0000000000..b14f35e61a
--- /dev/null
+++ b/toolkit/components/viewsource/test/browser/browser_bug464222.js
@@ -0,0 +1,17 @@
+const source =
+ "http://example.com/browser/toolkit/components/viewsource/test/browser/file_bug464222.html";
+
+add_task(async function () {
+ let viewSourceTab = await openDocumentSelect(source, "a");
+
+ let href = await SpecialPowers.spawn(
+ viewSourceTab.linkedBrowser,
+ [],
+ async function () {
+ return content.document.querySelectorAll("a[href]")[0].href;
+ }
+ );
+
+ is(href, "view-source:" + source, "Relative links broken?");
+ gBrowser.removeTab(viewSourceTab);
+});
diff --git a/toolkit/components/viewsource/test/browser/browser_bug713810.js b/toolkit/components/viewsource/test/browser/browser_bug713810.js
new file mode 100644
index 0000000000..b798f6cd10
--- /dev/null
+++ b/toolkit/components/viewsource/test/browser/browser_bug713810.js
@@ -0,0 +1,31 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const source =
+ '<html xmlns="http://www.w3.org/1999/xhtml"><body><p>This is a paragraph.</p></body></html>';
+
+add_task(async function () {
+ let viewSourceTab = await openDocumentSelect("data:text/html," + source, "p");
+ await SpecialPowers.spawn(viewSourceTab.linkedBrowser, [], async function () {
+ Assert.equal(
+ content.document.body.textContent,
+ "<p>This is a paragraph.</p>",
+ "Correct source for text/html"
+ );
+ });
+ gBrowser.removeTab(viewSourceTab);
+
+ viewSourceTab = await openDocumentSelect(
+ "data:application/xhtml+xml," + source,
+ "p"
+ );
+ await SpecialPowers.spawn(viewSourceTab.linkedBrowser, [], async function () {
+ Assert.equal(
+ content.document.body.textContent,
+ '<p xmlns="http://www.w3.org/1999/xhtml">This is a paragraph.</p>',
+ "Correct source for application/xhtml+xml"
+ );
+ });
+ gBrowser.removeTab(viewSourceTab);
+});
diff --git a/toolkit/components/viewsource/test/browser/browser_contextmenu.js b/toolkit/components/viewsource/test/browser/browser_contextmenu.js
new file mode 100644
index 0000000000..afc8ab8c84
--- /dev/null
+++ b/toolkit/components/viewsource/test/browser/browser_contextmenu.js
@@ -0,0 +1,115 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+var source =
+ "data:text/html,text<link%20href='http://example.com/'%20/>more%20text<a%20href='mailto:abc@def.ghi'>email</a>";
+var gViewSourceWindow, gContextMenu, gCopyLinkMenuItem, gCopyEmailMenuItem;
+
+var expectedData = [];
+
+add_task(async function () {
+ // Full source in view source tab
+ let newTab = await openDocument(source);
+ await onViewSourceWindowOpen(window);
+
+ let contextMenu = document.getElementById("contentAreaContextMenu");
+
+ for (let test of expectedData) {
+ await checkMenuItems(contextMenu, test[0], test[1], test[2], test[3]);
+ }
+
+ gBrowser.removeTab(newTab);
+
+ // Selection source in view source tab
+ expectedData = [];
+ newTab = await openDocumentSelect(source, "body");
+ await onViewSourceWindowOpen(window);
+
+ contextMenu = document.getElementById("contentAreaContextMenu");
+
+ for (let test of expectedData) {
+ await checkMenuItems(contextMenu, test[0], test[1], test[2], test[3]);
+ }
+
+ gBrowser.removeTab(newTab);
+});
+
+async function onViewSourceWindowOpen(aWindow) {
+ gViewSourceWindow = aWindow;
+
+ gCopyLinkMenuItem = aWindow.document.getElementById("context-copylink");
+ gCopyEmailMenuItem = aWindow.document.getElementById("context-copyemail");
+
+ let browser = gBrowser.selectedBrowser;
+ await SpecialPowers.spawn(browser, [], async function (arg) {
+ let tags = content.document.querySelectorAll("a[href]");
+ Assert.equal(
+ tags[0].href,
+ "view-source:http://example.com/",
+ "Link has correct href"
+ );
+ Assert.equal(tags[1].href, "mailto:abc@def.ghi", "Link has correct href");
+ });
+
+ expectedData.push(["a[href]", true, false, "http://example.com/"]);
+ expectedData.push(["a[href^=mailto]", false, true, "abc@def.ghi"]);
+ expectedData.push(["span", false, false, null]);
+}
+
+async function checkMenuItems(
+ contextMenu,
+ selector,
+ copyLinkExpected,
+ copyEmailExpected,
+ expectedClipboardContent
+) {
+ let browser = gBrowser.selectedBrowser;
+ await SpecialPowers.spawn(browser, [{ selector }], async function (arg) {
+ content.document.querySelector(arg.selector).scrollIntoView();
+ });
+
+ let popupShownPromise = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popupshown"
+ );
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ selector,
+ { type: "contextmenu", button: 2 },
+ browser
+ );
+ await popupShownPromise;
+
+ is(
+ gCopyLinkMenuItem.hidden,
+ !copyLinkExpected,
+ "Copy link menuitem is " + (copyLinkExpected ? "not hidden" : "hidden")
+ );
+ is(
+ gCopyEmailMenuItem.hidden,
+ !copyEmailExpected,
+ "Copy email menuitem is " + (copyEmailExpected ? "not hidden" : "hidden")
+ );
+
+ if (copyLinkExpected || copyEmailExpected) {
+ await new Promise((resolve, reject) => {
+ waitForClipboard(
+ expectedClipboardContent,
+ function () {
+ contextMenu.activateItem(
+ copyLinkExpected ? gCopyLinkMenuItem : gCopyEmailMenuItem
+ );
+ },
+ resolve,
+ reject
+ );
+ });
+ } else {
+ let popupHiddenPromise = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popuphidden"
+ );
+ contextMenu.hidePopup();
+ await popupHiddenPromise;
+ }
+}
diff --git a/toolkit/components/viewsource/test/browser/browser_gotoline.js b/toolkit/components/viewsource/test/browser/browser_gotoline.js
new file mode 100644
index 0000000000..000b2c6876
--- /dev/null
+++ b/toolkit/components/viewsource/test/browser/browser_gotoline.js
@@ -0,0 +1,37 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+var content = "line 1\nline 2\nline 3";
+
+add_task(async function () {
+ // First test with text with the text/html mimetype.
+ let tab = await openDocument("data:text/html," + encodeURIComponent(content));
+ await checkViewSource(tab);
+ gBrowser.removeTab(tab);
+
+ tab = await openDocument("data:text/plain," + encodeURIComponent(content));
+ await checkViewSource(tab);
+ gBrowser.removeTab(tab);
+});
+
+var checkViewSource = async function (aTab) {
+ let browser = aTab.linkedBrowser;
+ await SpecialPowers.spawn(browser, [content], async function (text) {
+ is(content.document.body.textContent, text, "Correct content loaded");
+ });
+
+ for (let i = 1; i <= 3; i++) {
+ browser.sendMessageToActor(
+ "ViewSource:GoToLine",
+ {
+ lineNumber: i,
+ },
+ "ViewSourcePage"
+ );
+ await SpecialPowers.spawn(browser, [i], async function (i) {
+ let selection = content.getSelection();
+ Assert.equal(selection.toString(), "line " + i, "Correct text selected");
+ });
+ }
+};
diff --git a/toolkit/components/viewsource/test/browser/browser_open_docgroup.js b/toolkit/components/viewsource/test/browser/browser_open_docgroup.js
new file mode 100644
index 0000000000..071635b998
--- /dev/null
+++ b/toolkit/components/viewsource/test/browser/browser_open_docgroup.js
@@ -0,0 +1,41 @@
+"use strict";
+
+/**
+ * Very basic smoketests for the View Source feature, which also
+ * forces on the DocGroup mismatch check that was added in
+ * bug 1340719.
+ */
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.throw_on_docgroup_mismatch.enabled", true]],
+ });
+});
+
+/**
+ * Tests that we can open View Source in a tab.
+ */
+add_task(async function test_view_source_in_tab() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "http://example.com",
+ },
+ async function (browser) {
+ let sourceTab = await openViewSourceForBrowser(browser);
+ let sourceBrowser = sourceTab.linkedBrowser;
+
+ await SpecialPowers.spawn(sourceBrowser, [], async function () {
+ Assert.equal(
+ content.document.body.id,
+ "viewsource",
+ "View source mode enabled"
+ );
+ });
+
+ BrowserTestUtils.removeTab(sourceTab);
+ }
+ );
+
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/toolkit/components/viewsource/test/browser/browser_partialsource.js b/toolkit/components/viewsource/test/browser/browser_partialsource.js
new file mode 100644
index 0000000000..d57a265b08
--- /dev/null
+++ b/toolkit/components/viewsource/test/browser/browser_partialsource.js
@@ -0,0 +1,46 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const frameSource =
+ "<a href='about:mozilla'>some text</a><a id='other' href='about:about'>other text</a>";
+const sources = [
+ `<html><iframe id="f" srcdoc="${frameSource}"></iframe></html>`,
+ `<html><iframe id="f" src="https://example.com/document-builder.sjs?html=${frameSource}"></iframe></html>`,
+];
+
+add_task(async function partial_source() {
+ for (let source of sources) {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "data:text/html," + source
+ );
+
+ let frameBC = gBrowser.selectedBrowser.browsingContext.children[0];
+
+ await SpecialPowers.spawn(frameBC, [], () => {
+ let element = content.document.getElementById("other");
+ content.focus();
+ content.getSelection().selectAllChildren(element);
+ });
+
+ let sourceTab = await openViewPartialSource("#other", frameBC);
+
+ let browser = gBrowser.selectedBrowser;
+ let textContent = await SpecialPowers.spawn(browser, [], async function () {
+ return content.document.body.textContent;
+ });
+ is(
+ textContent,
+ '<a id="other" href="about:about">other text</a>',
+ "Correct content loaded"
+ );
+ let selection = await SpecialPowers.spawn(browser, [], async function () {
+ return String(content.getSelection());
+ });
+ is(selection, "other text", "Correct text selected");
+
+ gBrowser.removeTab(sourceTab);
+ gBrowser.removeTab(tab);
+ }
+});
diff --git a/toolkit/components/viewsource/test/browser/browser_srcdoc.js b/toolkit/components/viewsource/test/browser/browser_srcdoc.js
new file mode 100644
index 0000000000..fbb07dd0ad
--- /dev/null
+++ b/toolkit/components/viewsource/test/browser/browser_srcdoc.js
@@ -0,0 +1,28 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const frameSource = `<a href="about:mozilla">good</a>`;
+const source = `<html><iframe srcdoc='${frameSource}' id="f"></iframe></html>`;
+
+add_task(async function () {
+ let url = `data:text/html,${source}`;
+ await BrowserTestUtils.withNewTab({ gBrowser, url }, checkFrameSource);
+});
+
+async function checkFrameSource() {
+ let sourceTab = await openViewFrameSourceTab("#f");
+ registerCleanupFunction(function () {
+ gBrowser.removeTab(sourceTab);
+ });
+
+ let browser = gBrowser.selectedBrowser;
+ let textContent = await SpecialPowers.spawn(browser, [], async function () {
+ return content.document.body.textContent;
+ });
+ is(textContent, frameSource, "Correct content loaded");
+ let id = await SpecialPowers.spawn(browser, [], async function () {
+ return content.document.body.id;
+ });
+ is(id, "viewsource", "View source mode enabled");
+}
diff --git a/toolkit/components/viewsource/test/browser/browser_validatefilename.js b/toolkit/components/viewsource/test/browser/browser_validatefilename.js
new file mode 100644
index 0000000000..57699df733
--- /dev/null
+++ b/toolkit/components/viewsource/test/browser/browser_validatefilename.js
@@ -0,0 +1,68 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function () {
+ let tests = [
+ {
+ uri: "data:text/html,Test",
+ basename: "Untitled",
+ },
+ {
+ uri: "data:text/html,<title>Hello There</title>Test",
+ basename: "Hello There",
+ },
+ ];
+
+ for (let test of tests) {
+ await BrowserTestUtils.withNewTab(test.uri, async browser => {
+ let doc = {
+ characterSet: browser.characterSet,
+ contentType: browser.documentContentType,
+ title: browser.contentTitle,
+ };
+
+ let fl = gViewSourceUtils.getTemporaryFile(
+ browser.currentURI,
+ doc,
+ "text/html"
+ );
+ // Some versions of Windows will crop the extension to three characters so allow both forms.
+ ok(
+ fl.leafName == test.basename + ".htm" ||
+ fl.leafName == test.basename + ".html",
+ "filename title for " + test.basename + " html"
+ );
+
+ doc.contentType = "application/xhtml+xml";
+ fl = gViewSourceUtils.getTemporaryFile(
+ browser.currentURI,
+ doc,
+ "application/xhtml+xml"
+ );
+ ok(
+ fl.leafName == test.basename + ".xht" ||
+ fl.leafName == test.basename + ".xhtml",
+ "filename title for " + test.basename + " xhtml"
+ );
+ });
+ }
+
+ let fl = gViewSourceUtils.getTemporaryFile(
+ Services.io.newURI("http://www.example.com/simple"),
+ null,
+ "text/html"
+ );
+ ok(
+ fl.leafName == "simple.htm" || fl.leafName == "simple.html",
+ "filename title for simple"
+ );
+
+ fl = gViewSourceUtils.getTemporaryFile(
+ Services.io.newURI("http://www.example.com/samplefile.txt"),
+ null,
+ "text/html"
+ );
+ is(fl.leafName, "samplefile.txt", "filename title for samplefile");
+});
diff --git a/toolkit/components/viewsource/test/browser/browser_viewsource_newwindow.js b/toolkit/components/viewsource/test/browser/browser_viewsource_newwindow.js
new file mode 100644
index 0000000000..8a38d94719
--- /dev/null
+++ b/toolkit/components/viewsource/test/browser/browser_viewsource_newwindow.js
@@ -0,0 +1,102 @@
+/**
+ * Waits for a View Source window to be opened at a particular
+ * URL.
+ *
+ * @param {string} expectedURL The view-source: URL that's expected.
+ * @resolves {DOM Window} The window that was opened.
+ * @returns {Promise}
+ */
+async function waitForNewViewSourceWindow(expectedURL) {
+ let win = await BrowserTestUtils.domWindowOpened();
+ await BrowserTestUtils.waitForEvent(win, "EndSwapDocShells", true);
+ let browser = win.gBrowser.selectedBrowser;
+ if (browser.currentURI.spec != expectedURL) {
+ await BrowserTestUtils.browserLoaded(browser, false, expectedURL);
+ }
+ return win;
+}
+
+/**
+ * When view_source.tab is set to false, view source should
+ * open in new browser window instead of new tab.
+ */
+add_task(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["view_source.tab", false]],
+ });
+
+ const PAGE = "http://example.com/";
+ await BrowserTestUtils.withNewTab(
+ {
+ url: PAGE,
+ gBrowser,
+ },
+ async browser => {
+ let winPromise = waitForNewViewSourceWindow("view-source:" + PAGE);
+ BrowserViewSource(browser);
+ let win = await winPromise;
+
+ ok(win, "View Source opened up in a new window.");
+ await BrowserTestUtils.closeWindow(win);
+ }
+ );
+});
+
+/**
+ * When view_source.tab is set to false, view partial source
+ * should open up in new browser window instead of new tab.
+ */
+add_task(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["view_source.tab", false]],
+ });
+
+ const para = "<p>test</p>";
+ const source = `<html><body>${para}</body></html>`;
+ await BrowserTestUtils.withNewTab(
+ {
+ url: "data:text/html," + source,
+ gBrowser,
+ },
+ async browser => {
+ let winPromise = waitForNewViewSourceWindow(
+ "view-source:data:text/html;charset=utf-8,%3Cp%3E%EF%B7%90test%EF%B7%AF%3C%2Fp%3E"
+ );
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ async function (arg) {
+ let element = content.document.querySelector("p");
+ content.getSelection().selectAllChildren(element);
+ }
+ );
+
+ let contentAreaContextMenuPopup = document.getElementById(
+ "contentAreaContextMenu"
+ );
+ let popupShownPromise = BrowserTestUtils.waitForEvent(
+ contentAreaContextMenuPopup,
+ "popupshown"
+ );
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "p",
+ { type: "contextmenu", button: 2 },
+ gBrowser.selectedBrowser
+ );
+ await popupShownPromise;
+
+ let popupHiddenPromise = BrowserTestUtils.waitForEvent(
+ contentAreaContextMenuPopup,
+ "popuphidden"
+ );
+ let item = document.getElementById("context-viewpartialsource-selection");
+ contentAreaContextMenuPopup.activateItem(item);
+ await popupHiddenPromise;
+ dump("Before winPromise");
+ let win = await winPromise;
+ dump("After winPromise");
+ ok(win, "View Partial Source opened up in a new window.");
+ await BrowserTestUtils.closeWindow(win);
+ }
+ );
+});
diff --git a/toolkit/components/viewsource/test/browser/browser_viewsourceprefs.js b/toolkit/components/viewsource/test/browser/browser_viewsourceprefs.js
new file mode 100644
index 0000000000..e155a9c87e
--- /dev/null
+++ b/toolkit/components/viewsource/test/browser/browser_viewsourceprefs.js
@@ -0,0 +1,203 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+var plaintextURL = "data:text/plain,hello+world";
+var htmlURL = "about:mozilla";
+
+add_setup(async function () {
+ registerCleanupFunction(function () {
+ SpecialPowers.clearUserPref("view_source.tab_size");
+ SpecialPowers.clearUserPref("view_source.wrap_long_lines");
+ SpecialPowers.clearUserPref("view_source.syntax_highlight");
+ });
+});
+
+add_task(async function () {
+ await exercisePrefs(plaintextURL, false);
+ await exercisePrefs(htmlURL, true);
+});
+
+const contextMenu = document.getElementById("contentAreaContextMenu");
+async function openContextMenu(browser) {
+ info("Opening context menu");
+ const popupShownPromise = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popupshown"
+ );
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "html",
+ { type: "contextmenu", button: 2 },
+ browser
+ );
+ await popupShownPromise;
+ info("Opened context menu");
+}
+
+async function closeContextMenu() {
+ const popupHiddenPromise = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popuphidden"
+ );
+ contextMenu.hidePopup();
+ await popupHiddenPromise;
+}
+
+async function simulateClick(id) {
+ const popupHiddenPromise = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popuphidden"
+ );
+ contextMenu.activateItem(document.getElementById(id));
+ await popupHiddenPromise;
+}
+
+function getAttribute(id, attribute) {
+ let item = document.getElementById(id);
+ return item.getAttribute(attribute);
+}
+
+var exercisePrefs = async function (source, highlightable) {
+ let tab = await openDocument(source);
+ let browser = tab.linkedBrowser;
+
+ const wrapMenuItem = "context-viewsource-wrapLongLines";
+ const syntaxMenuItem = "context-viewsource-highlightSyntax";
+
+ // Test the default states of these menu items.
+ await checkStyle(browser, "-moz-tab-size", 4);
+ await openContextMenu(browser);
+ await checkStyle(browser, "white-space", "pre");
+ await checkHighlight(browser, highlightable);
+ is(
+ getAttribute(wrapMenuItem, "checked"),
+ "false",
+ "Wrap menu item not checked by default"
+ );
+ is(
+ getAttribute(syntaxMenuItem, "checked"),
+ "true",
+ "Syntax menu item checked by default"
+ );
+ await closeContextMenu();
+
+ // Next, test that the Wrap Long Lines menu item works.
+ let prefReady = waitForPrefChange("view_source.wrap_long_lines");
+ await openContextMenu(browser);
+ await simulateClick(wrapMenuItem);
+ await openContextMenu(browser);
+ await checkStyle(browser, "white-space", "pre-wrap");
+ is(getAttribute(wrapMenuItem, "checked"), "true", "Wrap menu item checked");
+ await prefReady;
+ is(
+ SpecialPowers.getBoolPref("view_source.wrap_long_lines"),
+ true,
+ "Wrap pref set"
+ );
+ await closeContextMenu();
+
+ prefReady = waitForPrefChange("view_source.wrap_long_lines");
+ await openContextMenu(browser);
+ await simulateClick(wrapMenuItem);
+ await openContextMenu(browser);
+ await checkStyle(browser, "white-space", "pre");
+ is(
+ getAttribute(wrapMenuItem, "checked"),
+ "false",
+ "Wrap menu item unchecked"
+ );
+ await prefReady;
+ is(
+ SpecialPowers.getBoolPref("view_source.wrap_long_lines"),
+ false,
+ "Wrap pref set"
+ );
+ await closeContextMenu();
+
+ // Check that the Syntax Highlighting menu item works.
+ prefReady = waitForPrefChange("view_source.syntax_highlight");
+ await openContextMenu(browser);
+ await simulateClick(syntaxMenuItem);
+ await openContextMenu(browser);
+ await checkHighlight(browser, false);
+ is(
+ getAttribute(syntaxMenuItem, "checked"),
+ "false",
+ "Syntax menu item unchecked"
+ );
+ await prefReady;
+ is(
+ SpecialPowers.getBoolPref("view_source.syntax_highlight"),
+ false,
+ "Syntax highlighting pref set"
+ );
+ await closeContextMenu();
+
+ prefReady = waitForPrefChange("view_source.syntax_highlight");
+ await openContextMenu(browser);
+ await simulateClick(syntaxMenuItem);
+ await openContextMenu(browser);
+ await checkHighlight(browser, highlightable);
+ is(
+ getAttribute(syntaxMenuItem, "checked"),
+ "true",
+ "Syntax menu item checked"
+ );
+ await prefReady;
+ is(
+ SpecialPowers.getBoolPref("view_source.syntax_highlight"),
+ true,
+ "Syntax highlighting pref set"
+ );
+ await closeContextMenu();
+ gBrowser.removeTab(tab);
+
+ // Open a new view-source window to check that the prefs are obeyed.
+ SpecialPowers.setIntPref("view_source.tab_size", 2);
+ SpecialPowers.setBoolPref("view_source.wrap_long_lines", true);
+ SpecialPowers.setBoolPref("view_source.syntax_highlight", false);
+
+ tab = await openDocument(source);
+ browser = tab.linkedBrowser;
+
+ await checkStyle(browser, "-moz-tab-size", 2);
+ await openContextMenu(browser);
+ await checkStyle(browser, "white-space", "pre-wrap");
+ await checkHighlight(browser, false);
+ is(getAttribute(wrapMenuItem, "checked"), "true", "Wrap menu item checked");
+ is(
+ getAttribute(syntaxMenuItem, "checked"),
+ "false",
+ "Syntax menu item unchecked"
+ );
+
+ SpecialPowers.clearUserPref("view_source.tab_size");
+ SpecialPowers.clearUserPref("view_source.wrap_long_lines");
+ SpecialPowers.clearUserPref("view_source.syntax_highlight");
+
+ await closeContextMenu();
+ gBrowser.removeTab(tab);
+};
+
+var checkStyle = async function (browser, styleProperty, expected) {
+ let value = await SpecialPowers.spawn(
+ browser,
+ [styleProperty],
+ async function (styleProperty) {
+ let style = content.getComputedStyle(content.document.body);
+ return style.getPropertyValue(styleProperty);
+ }
+ );
+ is(value, "" + expected, "Correct value of " + styleProperty);
+};
+
+var checkHighlight = async function (browser, expected) {
+ let highlighted = await SpecialPowers.spawn(browser, [], async function () {
+ let spans = content.document.getElementsByTagName("span");
+ return Array.prototype.some.call(spans, span => {
+ let style = content.getComputedStyle(span);
+ return style.getPropertyValue("color") !== "rgb(0, 0, 0)";
+ });
+ });
+ is(highlighted, expected, "Syntax highlighting " + (expected ? "on" : "off"));
+};
diff --git a/toolkit/components/viewsource/test/browser/file_bug464222.html b/toolkit/components/viewsource/test/browser/file_bug464222.html
new file mode 100644
index 0000000000..f3b00949c7
--- /dev/null
+++ b/toolkit/components/viewsource/test/browser/file_bug464222.html
@@ -0,0 +1 @@
+<a href="file_bug464222.html">I'm a link</a>
diff --git a/toolkit/components/viewsource/test/browser/head.js b/toolkit/components/viewsource/test/browser/head.js
new file mode 100644
index 0000000000..a75c6a8658
--- /dev/null
+++ b/toolkit/components/viewsource/test/browser/head.js
@@ -0,0 +1,231 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const { Preferences } = ChromeUtils.importESModule(
+ "resource://gre/modules/Preferences.sys.mjs"
+);
+
+/**
+ * Wait for view source tab after calling given function to open it.
+ *
+ * @param open - a function to open view source.
+ * @returns the new tab which shows the source.
+ */
+async function waitForViewSourceTab(open) {
+ let sourceLoadedPromise;
+ let tabPromise;
+
+ tabPromise = new Promise(resolve => {
+ gBrowser.tabContainer.addEventListener(
+ "TabOpen",
+ event => {
+ let tab = event.target;
+ sourceLoadedPromise = waitForSourceLoaded(tab);
+ resolve(tab);
+ },
+ { once: true }
+ );
+ });
+
+ await open();
+
+ let tab = await tabPromise;
+ await sourceLoadedPromise;
+ return tab;
+}
+
+/**
+ * Opens view source for a browser.
+ *
+ * @param browser - the <xul:browser> to open view source for.
+ * @returns the new tab which shows the source.
+ */
+function openViewSourceForBrowser(browser) {
+ return waitForViewSourceTab(() => {
+ window.BrowserViewSource(browser);
+ });
+}
+
+/**
+ * Opens a view source tab. (View Source)
+ * within the currently selected browser in gBrowser.
+ *
+ * @returns the new tab which shows the source.
+ */
+async function openViewSource() {
+ let contentAreaContextMenuPopup = document.getElementById(
+ "contentAreaContextMenu"
+ );
+ let popupShownPromise = BrowserTestUtils.waitForEvent(
+ contentAreaContextMenuPopup,
+ "popupshown"
+ );
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "body",
+ { type: "contextmenu", button: 2 },
+ gBrowser.selectedBrowser
+ );
+ await popupShownPromise;
+
+ return waitForViewSourceTab(async () => {
+ let popupHiddenPromise = BrowserTestUtils.waitForEvent(
+ contentAreaContextMenuPopup,
+ "popuphidden"
+ );
+ contentAreaContextMenuPopup.activateItem(
+ document.getElementById("context-viewsource")
+ );
+ await popupHiddenPromise;
+ });
+}
+
+/**
+ * Opens a view source tab for a selection (View Selection Source)
+ * within the currently selected browser in gBrowser.
+ *
+ * @param aCSSSelector - used to specify a node within the selection to
+ * view the source of. It is expected that this node is
+ * within an existing selection.
+ * @param aBrowsingContext - browsing context containing a subframe (optional).
+ * @returns the new tab which shows the source.
+ */
+async function openViewPartialSource(
+ aCSSSelector,
+ aBrowsingContext = gBrowser.selectedBrowser
+) {
+ let contentAreaContextMenuPopup = document.getElementById(
+ "contentAreaContextMenu"
+ );
+ let popupShownPromise = BrowserTestUtils.waitForEvent(
+ contentAreaContextMenuPopup,
+ "popupshown"
+ );
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ aCSSSelector,
+ { type: "contextmenu", button: 2 },
+ aBrowsingContext
+ );
+ await popupShownPromise;
+
+ return waitForViewSourceTab(async () => {
+ let popupHiddenPromise = BrowserTestUtils.waitForEvent(
+ contentAreaContextMenuPopup,
+ "popuphidden"
+ );
+ let item = document.getElementById("context-viewpartialsource-selection");
+ contentAreaContextMenuPopup.activateItem(item);
+ await popupHiddenPromise;
+ });
+}
+
+/**
+ * Opens a view source tab for a frame (View Frame Source) within the
+ * currently selected browser in gBrowser.
+ *
+ * @param aCSSSelector - used to specify the frame to view the source of.
+ * @returns the new tab which shows the source.
+ */
+async function openViewFrameSourceTab(aCSSSelector) {
+ let contentAreaContextMenuPopup = document.getElementById(
+ "contentAreaContextMenu"
+ );
+ let popupShownPromise = BrowserTestUtils.waitForEvent(
+ contentAreaContextMenuPopup,
+ "popupshown"
+ );
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ aCSSSelector,
+ { type: "contextmenu", button: 2 },
+ gBrowser.selectedBrowser
+ );
+ await popupShownPromise;
+
+ let frameContextMenu = document.getElementById("frame");
+ popupShownPromise = BrowserTestUtils.waitForEvent(
+ frameContextMenu,
+ "popupshown"
+ );
+ frameContextMenu.openMenu(true);
+ await popupShownPromise;
+
+ return waitForViewSourceTab(async () => {
+ let popupHiddenPromise = BrowserTestUtils.waitForEvent(
+ frameContextMenu,
+ "popuphidden"
+ );
+ let item = document.getElementById("context-viewframesource");
+ frameContextMenu.menupopup.activateItem(item);
+ await popupHiddenPromise;
+ });
+}
+
+/**
+ * For a given view source tab, wait for the source loading step to
+ * complete.
+ */
+function waitForSourceLoaded(tab) {
+ return BrowserTestUtils.waitForContentEvent(
+ tab.linkedBrowser,
+ "pageshow",
+ false,
+ event => String(event.target.location).startsWith("view-source")
+ );
+}
+
+/**
+ * Open a new document in a new tab, select part of it, and view the source of
+ * that selection. The document is not closed afterwards.
+ *
+ * @param aURI - url to load
+ * @param aCSSSelector - used to specify a node to select. All of this node's
+ * children will be selected.
+ * @returns the new tab which shows the source.
+ */
+async function openDocumentSelect(aURI, aCSSSelector) {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, aURI);
+ registerCleanupFunction(function () {
+ gBrowser.removeTab(tab);
+ });
+
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [{ selector: aCSSSelector }],
+ async function (arg) {
+ let element = content.document.querySelector(arg.selector);
+ content.getSelection().selectAllChildren(element);
+ }
+ );
+
+ return openViewPartialSource(aCSSSelector);
+}
+
+/**
+ * Open a new document in a new tab and view the source of whole page.
+ * The document is not closed afterwards.
+ *
+ * @param aURI - url to load
+ * @returns the new tab which shows the source.
+ */
+async function openDocument(aURI) {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, aURI);
+ registerCleanupFunction(function () {
+ gBrowser.removeTab(tab);
+ });
+
+ return openViewSource();
+}
+
+function pushPrefs(...aPrefs) {
+ return SpecialPowers.pushPrefEnv({ set: aPrefs });
+}
+
+function waitForPrefChange(pref) {
+ let deferred = Promise.withResolvers();
+ let observer = () => {
+ Preferences.ignore(pref, observer);
+ deferred.resolve();
+ };
+ Preferences.observe(pref, observer);
+ return deferred.promise;
+}
diff --git a/toolkit/components/viewsource/test/chrome.toml b/toolkit/components/viewsource/test/chrome.toml
new file mode 100644
index 0000000000..8d2b09ad49
--- /dev/null
+++ b/toolkit/components/viewsource/test/chrome.toml
@@ -0,0 +1,4 @@
+[DEFAULT]
+
+["test_bug428653.html"]
+support-files = ["file_empty.html"]
diff --git a/toolkit/components/viewsource/test/file_empty.html b/toolkit/components/viewsource/test/file_empty.html
new file mode 100644
index 0000000000..495c23ec8a
--- /dev/null
+++ b/toolkit/components/viewsource/test/file_empty.html
@@ -0,0 +1 @@
+<!DOCTYPE html><html><body></body></html>
diff --git a/toolkit/components/viewsource/test/test_bug428653.html b/toolkit/components/viewsource/test/test_bug428653.html
new file mode 100644
index 0000000000..f800cb8f5f
--- /dev/null
+++ b/toolkit/components/viewsource/test/test_bug428653.html
@@ -0,0 +1,45 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=428653
+-->
+<head>
+ <title>View Source Test (bug 428653)</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+ <iframe id="content" src="http://example.org/tests/toolkit/components/viewsource/test/file_empty.html"></iframe>
+
+ <script type="application/javascript">
+ /*
+ Test that we can't call the content browser's document.open() over Xrays.
+ See the security checks in nsHTMLDocument::Open, which make sure that the
+ entry global's principal matches that of the document.
+ */
+ SimpleTest.waitForExplicitFinish();
+
+ addLoadEvent(function testDocumentOpen() {
+ var browser = document.getElementById("content");
+ ok(browser, "got browser");
+ var doc = browser.contentDocument;
+ ok(doc, "got content document");
+
+ var opened = false;
+ try {
+ doc.open("text/html", "replace");
+ opened = true;
+ } catch (e) {
+ is(e.name, "SecurityError", "Unexpected exception");
+ }
+ is(opened, false, "Shouldn't have opened document");
+
+ doc.wrappedJSObject.open("text/html", "replace");
+ ok(true, "Should be able to open document via Xray Waiver");
+
+ SimpleTest.finish();
+ });
+ </script>
+</body>
+</html>