summaryrefslogtreecommitdiffstats
path: root/comm/mail/test/static
diff options
context:
space:
mode:
Diffstat (limited to 'comm/mail/test/static')
-rw-r--r--comm/mail/test/static/.eslintrc.js11
-rw-r--r--comm/mail/test/static/browser.ini15
-rw-r--r--comm/mail/test/static/browser_parsable_css.js573
-rw-r--r--comm/mail/test/static/browser_parsable_script.js169
-rw-r--r--comm/mail/test/static/dummy_page.html9
-rw-r--r--comm/mail/test/static/head.js158
-rw-r--r--comm/mail/test/static/moz.build8
7 files changed, 943 insertions, 0 deletions
diff --git a/comm/mail/test/static/.eslintrc.js b/comm/mail/test/static/.eslintrc.js
new file mode 100644
index 0000000000..2faf85ea4e
--- /dev/null
+++ b/comm/mail/test/static/.eslintrc.js
@@ -0,0 +1,11 @@
+"use strict";
+
+const browserTestConfig = require("eslint-plugin-mozilla/lib/configs/browser-test.js");
+
+module.exports = {
+ ...browserTestConfig,
+ rules: {
+ ...browserTestConfig.rules,
+ "func-names": "off",
+ },
+};
diff --git a/comm/mail/test/static/browser.ini b/comm/mail/test/static/browser.ini
new file mode 100644
index 0000000000..d1a6656170
--- /dev/null
+++ b/comm/mail/test/static/browser.ini
@@ -0,0 +1,15 @@
+[DEFAULT]
+head = head.js
+prefs =
+ mail.provider.suppress_dialog_on_startup=true
+ mail.spotlight.firstRunDone=true
+ mail.winsearch.firstRunDone=true
+ mailnews.start_page.override_url=about:blank
+ mailnews.start_page.url=about:blank
+skip-if = debug
+subsuite = thunderbird
+
+[browser_parsable_css.js]
+support-files =
+ dummy_page.html
+[browser_parsable_script.js]
diff --git a/comm/mail/test/static/browser_parsable_css.js b/comm/mail/test/static/browser_parsable_css.js
new file mode 100644
index 0000000000..6e2269e9b9
--- /dev/null
+++ b/comm/mail/test/static/browser_parsable_css.js
@@ -0,0 +1,573 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+SimpleTest.requestCompleteLog();
+
+/* This list allows pre-existing or 'unfixable' CSS issues to remain, while we
+ * detect newly occurring issues in shipping CSS. It is a list of objects
+ * specifying conditions under which an error should be ignored.
+ *
+ * Every property of the objects in it needs to consist of a regular expression
+ * matching the offending error. If an object has multiple regex criteria, they
+ * ALL need to match an error in order for that error not to cause a test
+ * failure. */
+let whitelist = [
+ // CodeMirror is imported as-is, see bug 1004423.
+ { sourceName: /codemirror\.css$/i, isFromDevTools: true },
+ {
+ sourceName: /devtools\/content\/debugger\/src\/components\/([A-z\/]+).css/i,
+ isFromDevTools: true,
+ },
+ // Highlighter CSS uses a UA-only pseudo-class, see bug 985597.
+ {
+ sourceName: /highlighters\.css$/i,
+ errorMessage: /Unknown pseudo-class.*moz-native-anonymous/i,
+ isFromDevTools: true,
+ },
+ // UA-only media features.
+ {
+ sourceName: /\b(autocomplete-item)\.css$/,
+ errorMessage: /Expected media feature name but found \u2018-moz.*/i,
+ isFromDevTools: false,
+ platforms: ["windows"],
+ },
+ {
+ sourceName:
+ /\b(contenteditable|EditorOverride|svg|forms|html|mathml|ua)\.css$/i,
+ errorMessage: /Unknown pseudo-class.*-moz-/i,
+ isFromDevTools: false,
+ },
+ {
+ sourceName:
+ /\b(scrollbars|xul|html|mathml|ua|forms|svg|manageDialog|autocomplete-item-shared|formautofill)\.css$/i,
+ errorMessage: /Unknown property.*-moz-/i,
+ isFromDevTools: false,
+ },
+ {
+ sourceName: /(scrollbars|xul)\.css$/i,
+ errorMessage: /Unknown pseudo-class.*-moz-/i,
+ isFromDevTools: false,
+ },
+ // Reserved to UA sheets unless layout.css.overflow-clip-box.enabled flipped to true.
+ {
+ sourceName: /(?:res|gre-resources)\/forms\.css$/i,
+ errorMessage: /Unknown property.*overflow-clip-box/i,
+ isFromDevTools: false,
+ },
+ // These variables are declared somewhere else, and error when we load the
+ // files directly. They're all marked intermittent because their appearance
+ // in the error console seems to not be consistent.
+ {
+ sourceName: /jsonview\/css\/general\.css$/i,
+ intermittent: true,
+ errorMessage: /Property contained reference to invalid variable.*color/i,
+ isFromDevTools: true,
+ },
+ // PDF.js uses a property that is currently only supported in chrome.
+ {
+ sourceName: /web\/viewer\.css$/i,
+ errorMessage:
+ /Unknown property ‘text-size-adjust’\. {2}Declaration dropped\./i,
+ isFromDevTools: false,
+ },
+ // PDF.js uses a property that is currently only supported in chrome.
+ {
+ sourceName: /web\/viewer\.css$/i,
+ errorMessage:
+ /Unknown property ‘forced-color-adjust’\. {2}Declaration dropped\./i,
+ isFromDevTools: false,
+ },
+ {
+ sourceName: /overlay\.css$/i,
+ errorMessage: /Unknown pseudo-class.*moz-native-anonymous/i,
+ isFromDevTools: false,
+ },
+];
+
+if (!Services.prefs.getBoolPref("layout.css.color-mix.enabled")) {
+ // Reserved to UA sheets unless layout.css.color-mix.enabled flipped to true.
+ whitelist.push({
+ sourceName: /\b(autocomplete-item)\.css$/,
+ errorMessage: /Expected color but found \u2018color-mix\u2019./i,
+ isFromDevTools: false,
+ platforms: ["windows"],
+ });
+}
+
+if (!Services.prefs.getBoolPref("layout.css.math-depth.enabled")) {
+ // mathml.css UA sheet rule for math-depth.
+ whitelist.push({
+ sourceName: /\b(scrollbars|mathml)\.css$/i,
+ errorMessage: /Unknown property .*\bmath-depth\b/i,
+ isFromDevTools: false,
+ });
+}
+
+if (!Services.prefs.getBoolPref("layout.css.math-style.enabled")) {
+ // mathml.css UA sheet rule for math-style.
+ whitelist.push({
+ sourceName: /(?:res|gre-resources)\/mathml\.css$/i,
+ errorMessage: /Unknown property .*\bmath-style\b/i,
+ isFromDevTools: false,
+ });
+}
+
+if (!Services.prefs.getBoolPref("layout.css.scroll-anchoring.enabled")) {
+ whitelist.push({
+ sourceName: /webconsole\.css$/i,
+ errorMessage: /Unknown property .*\boverflow-anchor\b/i,
+ isFromDevTools: true,
+ });
+}
+
+if (!Services.prefs.getBoolPref("layout.css.forced-colors.enabled")) {
+ whitelist.push({
+ sourceName: /pdf\.js\/web\/viewer\.css$/,
+ errorMessage: /Expected media feature name but found ‘forced-colors’*/i,
+ isFromDevTools: false,
+ });
+}
+
+let propNameWhitelist = [
+ // These custom properties are retrieved directly from CSSOM
+ // in videocontrols.xml to get pre-defined style instead of computed
+ // dimensions, which is why they are not referenced by CSS.
+ { propName: "--clickToPlay-width", isFromDevTools: false },
+ { propName: "--playButton-width", isFromDevTools: false },
+ { propName: "--muteButton-width", isFromDevTools: false },
+ { propName: "--castingButton-width", isFromDevTools: false },
+ { propName: "--closedCaptionButton-width", isFromDevTools: false },
+ { propName: "--fullscreenButton-width", isFromDevTools: false },
+ { propName: "--durationSpan-width", isFromDevTools: false },
+ { propName: "--durationSpan-width-long", isFromDevTools: false },
+ { propName: "--positionDurationBox-width", isFromDevTools: false },
+ { propName: "--positionDurationBox-width-long", isFromDevTools: false },
+
+ // These variables are used in a shorthand, but the CSS parser deletes the values
+ // when expanding the shorthands. See https://github.com/w3c/csswg-drafts/issues/2515
+ { propName: "--bezier-diagonal-color", isFromDevTools: true },
+ { propName: "--bezier-grid-color", isFromDevTools: true },
+];
+
+let thunderbirdWhitelist = [];
+
+// Add suffix to stylesheets' URI so that we always load them here and
+// have them parsed. Add a random number so that even if we run this
+// test multiple times, it would be unlikely to affect each other.
+const kPathSuffix = "?always-parse-css-" + Math.random();
+
+function dumpWhitelistItem(item) {
+ return JSON.stringify(item, (key, value) => {
+ return value instanceof RegExp ? value.toString() : value;
+ });
+}
+
+/**
+ * Check if an error should be ignored due to matching one of the whitelist
+ * objects defined in whitelist
+ *
+ * @param aErrorObject the error to check
+ * @returns true if the error should be ignored, false otherwise.
+ */
+function ignoredError(aErrorObject) {
+ for (let list of [whitelist, thunderbirdWhitelist]) {
+ for (let whitelistItem of list) {
+ let matches = true;
+ let catchAll = true;
+ for (let prop of ["sourceName", "errorMessage"]) {
+ if (whitelistItem.hasOwnProperty(prop)) {
+ catchAll = false;
+ if (!whitelistItem[prop].test(aErrorObject[prop] || "")) {
+ matches = false;
+ break;
+ }
+ }
+ }
+ if (catchAll) {
+ ok(
+ false,
+ "A whitelist item is catching all errors. " +
+ dumpWhitelistItem(whitelistItem)
+ );
+ continue;
+ }
+ if (matches) {
+ whitelistItem.used = true;
+ let { sourceName, errorMessage } = aErrorObject;
+ info(
+ `Ignored error "${errorMessage}" on ${sourceName} ` +
+ "because of whitelist item " +
+ dumpWhitelistItem(whitelistItem)
+ );
+ return true;
+ }
+ }
+ }
+ return false;
+}
+
+var gChromeReg = Cc["@mozilla.org/chrome/chrome-registry;1"].getService(
+ Ci.nsIChromeRegistry
+);
+var gChromeMap = new Map();
+
+var resHandler = Services.io
+ .getProtocolHandler("resource")
+ .QueryInterface(Ci.nsIResProtocolHandler);
+var gResourceMap = [];
+function trackResourcePrefix(prefix) {
+ let uri = Services.io.newURI("resource://" + prefix + "/");
+ gResourceMap.unshift([prefix, resHandler.resolveURI(uri)]);
+}
+trackResourcePrefix("gre");
+trackResourcePrefix("app");
+
+function getBaseUriForChromeUri(chromeUri) {
+ let chromeFile = chromeUri + "gobbledygooknonexistentfile.reallynothere";
+ let uri = Services.io.newURI(chromeFile);
+ let fileUri = gChromeReg.convertChromeURL(uri);
+ return fileUri.resolve(".");
+}
+
+function parseManifest(manifestUri) {
+ return fetchFile(manifestUri.spec).then(data => {
+ for (let line of data.split("\n")) {
+ let [type, ...argv] = line.split(/\s+/);
+ if (type == "content" || type == "skin") {
+ let chromeUri = `chrome://${argv[0]}/${type}/`;
+ gChromeMap.set(getBaseUriForChromeUri(chromeUri), chromeUri);
+ } else if (type == "resource") {
+ trackResourcePrefix(argv[0]);
+ }
+ }
+ });
+}
+
+function convertToCodeURI(fileUri) {
+ let baseUri = fileUri;
+ let path = "";
+ while (true) {
+ let slashPos = baseUri.lastIndexOf("/", baseUri.length - 2);
+ if (slashPos <= 0) {
+ // File not accessible from chrome protocol, try resource://
+ for (let res of gResourceMap) {
+ if (fileUri.startsWith(res[1])) {
+ return fileUri.replace(res[1], "resource://" + res[0] + "/");
+ }
+ }
+ // Give up and return the original URL.
+ return fileUri;
+ }
+ path = baseUri.slice(slashPos + 1) + path;
+ baseUri = baseUri.slice(0, slashPos + 1);
+ if (gChromeMap.has(baseUri)) {
+ return gChromeMap.get(baseUri) + path;
+ }
+ }
+}
+
+function messageIsCSSError(msg) {
+ // Only care about CSS errors generated by our iframe:
+ if (
+ msg instanceof Ci.nsIScriptError &&
+ msg.category.includes("CSS") &&
+ msg.sourceName.endsWith(kPathSuffix)
+ ) {
+ let sourceName = msg.sourceName.slice(0, -kPathSuffix.length);
+ let msgInfo = { sourceName, errorMessage: msg.errorMessage };
+ // Check if this error is whitelisted in whitelist
+ if (!ignoredError(msgInfo)) {
+ ok(false, `Got error message for ${sourceName}: ${msg.errorMessage}`);
+ return true;
+ }
+ }
+ return false;
+}
+
+let imageURIsToReferencesMap = new Map();
+let customPropsToReferencesMap = new Map();
+
+function neverMatches(mediaList) {
+ const perPlatformMediaQueryMap = {
+ macosx: ["(-moz-platform: macos)"],
+ win: [
+ "(-moz-platform: windows)",
+ "(-moz-platform: windows-win7)",
+ "(-moz-platform: windows-win8)",
+ "(-moz-platform: windows-win10)",
+ ],
+ linux: ["(-moz-platform: linux)"],
+ android: ["(-moz-platform: android)"],
+ };
+ for (let platform in perPlatformMediaQueryMap) {
+ if (platform === AppConstants.platform) {
+ continue;
+ }
+ if (perPlatformMediaQueryMap[platform].includes(mediaList.mediaText)) {
+ // This query only matches on another platform that isn't ours.
+ return true;
+ }
+ }
+ return false;
+}
+
+function processCSSRules(sheet) {
+ for (let rule of sheet.cssRules) {
+ if (rule.media && neverMatches(rule.media)) {
+ continue;
+ }
+ if (
+ CSSConditionRule.isInstance(rule) ||
+ CSSKeyframesRule.isInstance(rule)
+ ) {
+ processCSSRules(rule);
+ continue;
+ }
+ if (!CSSStyleRule.isInstance(rule) && !CSSKeyframeRule.isInstance(rule)) {
+ continue;
+ }
+
+ // Extract urls from the css text.
+ // Note: CSSRule.cssText always has double quotes around URLs even
+ // when the original CSS file didn't.
+ let urls = rule.cssText.match(/url\("[^"]*"\)/g);
+ // Extract props by searching all "--" preceded by "var(" or a non-word
+ // character.
+ let props = rule.cssText.match(/(var\(|\W)(--[\w\-]+)/g);
+ if (!urls && !props) {
+ continue;
+ }
+
+ for (let url of urls || []) {
+ // Remove the url(" prefix and the ") suffix.
+ url = url.replace(/url\("(.*)"\)/, "$1");
+ if (url.startsWith("data:")) {
+ continue;
+ }
+
+ // Make the url absolute and remove the ref.
+ let baseURI = Services.io.newURI(rule.parentStyleSheet.href);
+ url = Services.io.newURI(url, null, baseURI).specIgnoringRef;
+
+ // Store the image url along with the css file referencing it.
+ let baseUrl = baseURI.spec.split("?always-parse-css")[0];
+ if (!imageURIsToReferencesMap.has(url)) {
+ imageURIsToReferencesMap.set(url, new Set([baseUrl]));
+ } else {
+ imageURIsToReferencesMap.get(url).add(baseUrl);
+ }
+ }
+
+ for (let prop of props || []) {
+ if (prop.startsWith("var(")) {
+ prop = prop.substring(4);
+ let prevValue = customPropsToReferencesMap.get(prop) || 0;
+ customPropsToReferencesMap.set(prop, prevValue + 1);
+ } else {
+ // Remove the extra non-word character captured by the regular
+ // expression.
+ prop = prop.substring(1);
+ if (!customPropsToReferencesMap.has(prop)) {
+ customPropsToReferencesMap.set(prop, undefined);
+ }
+ }
+ }
+ }
+}
+
+function chromeFileExists(aURI) {
+ let available = 0;
+ try {
+ let channel = NetUtil.newChannel({
+ uri: aURI,
+ loadUsingSystemPrincipal: true,
+ });
+ let stream = channel.open();
+ let sstream = Cc["@mozilla.org/scriptableinputstream;1"].createInstance(
+ Ci.nsIScriptableInputStream
+ );
+ sstream.init(stream);
+ available = sstream.available();
+ sstream.close();
+ } catch (e) {
+ if (e.result != Cr.NS_ERROR_FILE_NOT_FOUND) {
+ dump("Checking " + aURI + ": " + e + "\n");
+ console.error(e);
+ }
+ }
+ return available > 0;
+}
+
+add_task(async function checkAllTheCSS() {
+ // Since we later in this test use Services.console.getMessageArray(),
+ // better to not have some messages from previous tests in the array.
+ Services.console.reset();
+
+ let appDir = Services.dirsvc.get("GreD", Ci.nsIFile);
+ // This asynchronously produces a list of URLs (sadly, mostly sync on our
+ // test infrastructure because it runs against jarfiles there, and
+ // our zipreader APIs are all sync)
+ let uris = await generateURIsFromDirTree(appDir, [".css", ".manifest"]);
+
+ // Create a clean iframe to load all the files into. This needs to live at a
+ // chrome URI so that it's allowed to load and parse any styles.
+ let testFile = getRootDirectory(gTestPath) + "dummy_page.html";
+ let { HiddenFrame } = ChromeUtils.importESModule(
+ "resource://gre/modules/HiddenFrame.sys.mjs"
+ );
+ let hiddenFrame = new HiddenFrame();
+ let win = await hiddenFrame.get();
+ let iframe = win.document.createElementNS(
+ "http://www.w3.org/1999/xhtml",
+ "html:iframe"
+ );
+ win.document.documentElement.appendChild(iframe);
+ let iframeLoaded = BrowserTestUtils.waitForEvent(iframe, "load", true);
+ iframe.contentWindow.location = testFile;
+ await iframeLoaded;
+ let doc = iframe.contentWindow.document;
+ iframe.contentWindow.docShell.cssErrorReportingEnabled = true;
+
+ // Parse and remove all manifests from the list.
+ // NOTE that this must be done before filtering out devtools paths
+ // so that all chrome paths can be recorded.
+ let manifestURIs = [];
+ uris = uris.filter(uri => {
+ if (uri.pathQueryRef.endsWith(".manifest")) {
+ manifestURIs.push(uri);
+ return false;
+ }
+ return true;
+ });
+ // Wait for all manifest to be parsed
+ await throttledMapPromises(manifestURIs, parseManifest);
+
+ // filter out either the devtools paths or the non-devtools paths:
+ let isDevtools = SimpleTest.harnessParameters.subsuite == "devtools";
+ let devtoolsPathBits = ["devtools"];
+ uris = uris.filter(
+ uri => isDevtools == devtoolsPathBits.some(path => uri.spec.includes(path))
+ );
+
+ let loadCSS = chromeUri =>
+ new Promise(resolve => {
+ let linkEl, onLoad, onError;
+ onLoad = e => {
+ processCSSRules(linkEl.sheet);
+ resolve();
+ linkEl.removeEventListener("load", onLoad);
+ linkEl.removeEventListener("error", onError);
+ };
+ onError = e => {
+ ok(
+ false,
+ "Loading " + linkEl.getAttribute("href") + " threw an error!"
+ );
+ resolve();
+ linkEl.removeEventListener("load", onLoad);
+ linkEl.removeEventListener("error", onError);
+ };
+ linkEl = doc.createElement("link");
+ linkEl.setAttribute("rel", "stylesheet");
+ linkEl.setAttribute("type", "text/css");
+ linkEl.addEventListener("load", onLoad);
+ linkEl.addEventListener("error", onError);
+ linkEl.setAttribute("href", chromeUri + kPathSuffix);
+ doc.head.appendChild(linkEl);
+ });
+
+ // We build a list of promises that get resolved when their respective
+ // files have loaded and produced no errors.
+ const kInContentCommonCSS = "chrome://global/skin/in-content/common.css";
+ let allPromises = uris
+ .map(uri => convertToCodeURI(uri.spec))
+ .filter(uri => uri !== kInContentCommonCSS);
+
+ // Make sure chrome://global/skin/in-content/common.css is loaded before other
+ // stylesheets in order to guarantee the --in-content variables can be
+ // correctly referenced.
+ if (allPromises.length !== uris.length) {
+ await loadCSS(kInContentCommonCSS);
+ }
+
+ // Wait for all the files to have actually loaded:
+ await throttledMapPromises(allPromises, loadCSS);
+
+ // Check if all the files referenced from CSS actually exist.
+ for (let [image, references] of imageURIsToReferencesMap) {
+ if (!chromeFileExists(image)) {
+ for (let ref of references) {
+ let whitelisted = false;
+ for (let whitelistItem of thunderbirdWhitelist) {
+ if (whitelistItem.sourceName.test(ref)) {
+ whitelistItem.used = true;
+ whitelisted = true;
+ info("missing " + image + " referenced from " + ref);
+ break;
+ }
+ }
+ if (!whitelisted) {
+ ok(false, "missing " + image + " referenced from " + ref);
+ }
+ }
+ }
+ }
+
+ // Check if all the properties that are defined are referenced.
+ for (let [prop, refCount] of customPropsToReferencesMap) {
+ if (!refCount) {
+ let ignored = false;
+ for (let item of propNameWhitelist) {
+ if (item.propName == prop && isDevtools == item.isFromDevTools) {
+ item.used = true;
+ if (
+ !item.platforms ||
+ item.platforms.includes(AppConstants.platform)
+ ) {
+ ignored = true;
+ }
+ break;
+ }
+ }
+ if (!ignored) {
+ info("custom property `" + prop + "` is not referenced");
+ }
+ }
+ }
+
+ let messages = Services.console.getMessageArray();
+ // Count errors (the test output will list actual issues for us, as well
+ // as the ok(false) in messageIsCSSError.
+ let errors = messages.filter(messageIsCSSError);
+ is(
+ errors.length,
+ 0,
+ "All the styles (" + allPromises.length + ") loaded without errors."
+ );
+
+ // Confirm that all whitelist rules have been used.
+ function checkWhitelist(list) {
+ for (let item of list) {
+ if (
+ !item.used &&
+ isDevtools == item.isFromDevTools &&
+ (!item.platforms || item.platforms.includes(AppConstants.platform)) &&
+ !item.intermittent
+ ) {
+ ok(false, "Unused whitelist item: " + dumpWhitelistItem(item));
+ }
+ }
+ }
+ checkWhitelist(thunderbirdWhitelist);
+
+ // Clean up to avoid leaks:
+ doc.head.innerHTML = "";
+ doc = null;
+ iframe.remove();
+ iframe = null;
+ win = null;
+ hiddenFrame.destroy();
+ hiddenFrame = null;
+ imageURIsToReferencesMap = null;
+ customPropsToReferencesMap = null;
+});
diff --git a/comm/mail/test/static/browser_parsable_script.js b/comm/mail/test/static/browser_parsable_script.js
new file mode 100644
index 0000000000..bc46465b3d
--- /dev/null
+++ b/comm/mail/test/static/browser_parsable_script.js
@@ -0,0 +1,169 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* This list allows pre-existing or 'unfixable' JS issues to remain, while we
+ * detect newly occurring issues in shipping JS. It is a list of regexes
+ * matching files which have errors:
+ */
+
+requestLongerTimeout(2);
+SimpleTest.requestCompleteLog();
+
+const kWhitelist = new Set([
+ /browser\/content\/browser\/places\/controller.js$/,
+]);
+
+const kESModuleList = new Set([
+ /browser\/res\/payments\/(components|containers|mixins)\/.*\.js$/,
+ /browser\/res\/payments\/paymentRequest\.js$/,
+ /browser\/res\/payments\/PaymentsStore\.js$/,
+ /browser\/aboutlogins\/components\/.*\.js$/,
+ /browser\/aboutlogins\/.*\.js$/,
+ /browser\/protections.js$/,
+ /browser\/lockwise-card.js$/,
+ /browser\/monitor-card.js$/,
+ /browser\/proxy-card.js$/,
+ /toolkit\/content\/global\/certviewer\/components\/.*\.js$/,
+ /toolkit\/content\/global\/certviewer\/.*\.js$/,
+ /chrome\/pdfjs\/content\/web\/.*\.js$/,
+]);
+
+// Normally we would use reflect.jsm to get Reflect.parse. However, if
+// we do that, then all the AST data is allocated in reflect.jsm's
+// zone. That exposes a bug in our GC. The GC collects reflect.jsm's
+// zone but not the zone in which our test code lives (since no new
+// data is being allocated in it). The cross-compartment wrappers in
+// our zone that point to the AST data never get collected, and so the
+// AST data itself is never collected. We need to GC both zones at
+// once to fix the problem.
+const init = Cc["@mozilla.org/jsreflect;1"].createInstance();
+init();
+
+/**
+ * Check if an error should be ignored due to matching one of the whitelist
+ * objects defined in kWhitelist
+ *
+ * @param uri the uri to check against the whitelist
+ * @returns true if the uri should be skipped, false otherwise.
+ */
+function uriIsWhiteListed(uri) {
+ for (let whitelistItem of kWhitelist) {
+ if (whitelistItem.test(uri.spec)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+/**
+ * Check if a URI should be parsed as an ES module.
+ *
+ * @param uri the uri to check against the ES module list
+ * @returns true if the uri should be parsed as a module, otherwise parse it as a script.
+ */
+function uriIsESModule(uri) {
+ for (let whitelistItem of kESModuleList) {
+ if (whitelistItem.test(uri.spec)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+function parsePromise(uri, parseTarget) {
+ let promise = new Promise((resolve, reject) => {
+ let xhr = new XMLHttpRequest();
+ xhr.open("GET", uri, true);
+ xhr.onreadystatechange = function () {
+ if (this.readyState == this.DONE) {
+ let scriptText = this.responseText;
+ try {
+ info(`Checking ${parseTarget} ${uri}`);
+ let parseOpts = {
+ source: uri,
+ target: parseTarget,
+ };
+ Reflect.parse(scriptText, parseOpts);
+ resolve(true);
+ } catch (ex) {
+ let errorMsg = "Script error reading " + uri + ": " + ex;
+ ok(false, errorMsg);
+ resolve(false);
+ }
+ }
+ };
+ xhr.onerror = error => {
+ ok(false, "XHR error reading " + uri + ": " + error);
+ resolve(false);
+ };
+ xhr.overrideMimeType("application/javascript");
+ xhr.send(null);
+ });
+ return promise;
+}
+
+add_task(async function checkAllTheJS() {
+ // In debug builds, even on a fast machine, collecting the file list may take
+ // more than 30 seconds, and parsing all files may take four more minutes.
+ // For this reason, this test must be explicitly requested in debug builds by
+ // using the "--setpref parse=<filter>" argument to mach. You can specify:
+ // - A case-sensitive substring of the file name to test (slow).
+ // - A single absolute URI printed out by a previous run (fast).
+ // - An empty string to run the test on all files (slowest).
+ let parseRequested = Services.prefs.prefHasUserValue("parse");
+ let parseValue = parseRequested && Services.prefs.getCharPref("parse");
+ if (SpecialPowers.isDebugBuild) {
+ if (!parseRequested) {
+ ok(
+ true,
+ "Test disabled on debug build. To run, execute: ./mach" +
+ " mochitest-browser --setpref parse=<case_sensitive_filter>" +
+ " browser/base/content/test/general/browser_parsable_script.js"
+ );
+ return;
+ }
+ // Request a 15 minutes timeout (30 seconds * 30) for debug builds.
+ requestLongerTimeout(30);
+ }
+
+ let uris;
+ // If an absolute URI is specified on the command line, use it immediately.
+ if (parseValue && parseValue.includes(":")) {
+ uris = [NetUtil.newURI(parseValue)];
+ } else {
+ let appDir = Services.dirsvc.get("GreD", Ci.nsIFile);
+ // This asynchronously produces a list of URLs (sadly, mostly sync on our
+ // test infrastructure because it runs against jarfiles there, and
+ // our zipreader APIs are all sync)
+ let startTimeMs = Date.now();
+ info("Collecting URIs");
+ uris = await generateURIsFromDirTree(appDir, [".js", ".jsm"]);
+ info("Collected URIs in " + (Date.now() - startTimeMs) + "ms");
+
+ // Apply the filter specified on the command line, if any.
+ if (parseValue) {
+ uris = uris.filter(uri => {
+ if (uri.spec.includes(parseValue)) {
+ return true;
+ }
+ info("Not checking filtered out " + uri.spec);
+ return false;
+ });
+ }
+ }
+
+ // We create an array of promises so we can parallelize all our parsing
+ // and file loading activity:
+ await throttledMapPromises(uris, uri => {
+ if (uriIsWhiteListed(uri)) {
+ info("Not checking whitelisted " + uri.spec);
+ return undefined;
+ }
+ let target = "script";
+ if (uriIsESModule(uri)) {
+ target = "module";
+ }
+ return parsePromise(uri.spec, target);
+ });
+ ok(true, "All files parsed");
+});
diff --git a/comm/mail/test/static/dummy_page.html b/comm/mail/test/static/dummy_page.html
new file mode 100644
index 0000000000..1a87e28408
--- /dev/null
+++ b/comm/mail/test/static/dummy_page.html
@@ -0,0 +1,9 @@
+<html>
+<head>
+<title>Dummy test page</title>
+<meta http-equiv="Content-Type" content="text/html;charset=utf-8"></meta>
+</head>
+<body>
+<p>Dummy test page</p>
+</body>
+</html>
diff --git a/comm/mail/test/static/head.js b/comm/mail/test/static/head.js
new file mode 100644
index 0000000000..a1cce8735a
--- /dev/null
+++ b/comm/mail/test/static/head.js
@@ -0,0 +1,158 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* Shorthand constructors to construct an nsI(Local)File and zip reader: */
+const LocalFile = new Components.Constructor(
+ "@mozilla.org/file/local;1",
+ Ci.nsIFile,
+ "initWithPath"
+);
+const ZipReader = new Components.Constructor(
+ "@mozilla.org/libjar/zip-reader;1",
+ "nsIZipReader",
+ "open"
+);
+
+/**
+ * Returns a promise that is resolved with a list of files that have one of the
+ * extensions passed, represented by their nsIURI objects, which exist inside
+ * the directory passed.
+ *
+ * @param dir the directory which to scan for files (nsIFile)
+ * @param extensions the extensions of files we're interested in (Array).
+ */
+function generateURIsFromDirTree(dir, extensions) {
+ if (!Array.isArray(extensions)) {
+ extensions = [extensions];
+ }
+ let dirQueue = [dir.path];
+ return (async function () {
+ let rv = [];
+ while (dirQueue.length) {
+ let nextDir = dirQueue.shift();
+ let { subdirs, files } = await iterateOverPath(nextDir, extensions);
+ dirQueue.push(...subdirs);
+ rv.push(...files);
+ }
+ return rv;
+ })();
+}
+
+/**
+ * Iterates over all entries of a directory.
+ * It returns a promise that is resolved with an object with two properties:
+ * - files: an array of nsIURIs corresponding to files that match the extensions passed
+ * - subdirs: an array of paths for subdirectories we need to recurse into
+ * (handled by generateURIsFromDirTree above)
+ *
+ * @param path the path to check (string)
+ * @param extensions the file extensions we're interested in.
+ */
+async function iterateOverPath(path, extensions) {
+ let parentDir = new LocalFile(path);
+ let subdirs = [];
+ let files = [];
+
+ // Iterate through the directory
+ for (let childPath of await IOUtils.getChildren(path)) {
+ let stat = await IOUtils.stat(childPath);
+ if (stat.type === "directory") {
+ subdirs.push(childPath);
+ } else if (extensions.some(extension => childPath.endsWith(extension))) {
+ let file = parentDir.clone();
+ file.append(PathUtils.filename(childPath));
+ // the build system might leave dead symlinks hanging around, which are
+ // returned as part of the directory iterator, but don't actually exist:
+ if (file.exists()) {
+ let uriSpec = getURLForFile(file);
+ files.push(Services.io.newURI(uriSpec));
+ }
+ } else if (
+ childPath.endsWith(".ja") ||
+ childPath.endsWith(".jar") ||
+ childPath.endsWith(".zip") ||
+ childPath.endsWith(".xpi")
+ ) {
+ let file = parentDir.clone();
+ file.append(PathUtils.filename(childPath));
+ for (let extension of extensions) {
+ let jarEntryIterator = generateEntriesFromJarFile(file, extension);
+ files.push(...jarEntryIterator);
+ }
+ }
+ }
+ return { files, subdirs };
+}
+
+/* Helper function to generate a URI spec (NB: not an nsIURI yet!)
+ * given an nsIFile object */
+function getURLForFile(file) {
+ let fileHandler = Services.io.getProtocolHandler("file");
+ fileHandler = fileHandler.QueryInterface(Ci.nsIFileProtocolHandler);
+ return fileHandler.getURLSpecFromActualFile(file);
+}
+
+/**
+ * A generator that generates nsIURIs for particular files found in jar files
+ * like omni.ja.
+ *
+ * @param jarFile an nsIFile object for the jar file that needs checking.
+ * @param extension the extension we're interested in.
+ */
+function* generateEntriesFromJarFile(jarFile, extension) {
+ let zr = new ZipReader(jarFile);
+ const kURIStart = getURLForFile(jarFile);
+
+ for (let entry of zr.findEntries("*" + extension + "$")) {
+ // Ignore the JS cache which is stored in omni.ja
+ if (entry.startsWith("jsloader") || entry.startsWith("jssubloader")) {
+ continue;
+ }
+ let entryURISpec = "jar:" + kURIStart + "!/" + entry;
+ yield Services.io.newURI(entryURISpec);
+ }
+ zr.close();
+}
+
+function fetchFile(uri) {
+ return new Promise((resolve, reject) => {
+ let xhr = new XMLHttpRequest();
+ xhr.responseType = "text";
+ xhr.open("GET", uri, true);
+ xhr.onreadystatechange = function () {
+ if (this.readyState != this.DONE) {
+ return;
+ }
+ try {
+ resolve(this.responseText);
+ } catch (ex) {
+ ok(false, `Script error reading ${uri}: ${ex}`);
+ resolve("");
+ }
+ };
+ xhr.onerror = error => {
+ ok(false, `XHR error reading ${uri}: ${error}`);
+ resolve("");
+ };
+ xhr.send(null);
+ });
+}
+
+async function throttledMapPromises(iterable, task, limit = 64) {
+ let promises = new Set();
+ for (let data of iterable) {
+ while (promises.size >= limit) {
+ await Promise.race(promises);
+ }
+
+ let promise = task(data);
+ if (promise) {
+ promise.finally(() => promises.delete(promise));
+ promises.add(promise);
+ }
+ }
+
+ await Promise.all(promises);
+}
diff --git a/comm/mail/test/static/moz.build b/comm/mail/test/static/moz.build
new file mode 100644
index 0000000000..168aad77c5
--- /dev/null
+++ b/comm/mail/test/static/moz.build
@@ -0,0 +1,8 @@
+# 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 += [
+ "browser.ini",
+]