Bug 1615132 - Copy static analysis tests from browser/base/content/test/static. r=mkmelin
authorGeoff Lankow <geoff@darktrojan.net>
Wed, 01 Jan 2020 16:02:04 +1300
changeset 37347 e24490bb032167720b9667af2c91ac964bb654d9
parent 37346 3d6cb3e7f57414dd148679609a8a70e33fcc3426
child 37348 027b7fafc431c9cfd396f1b16235d47819b3ae2a
push id2566
push userclokep@gmail.com
push dateMon, 09 Mar 2020 19:20:31 +0000
treeherdercomm-beta@a352facfa0a4 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmkmelin
bugs1615132
Bug 1615132 - Copy static analysis tests from browser/base/content/test/static. r=mkmelin
mail/moz.build
mail/test/static/.eslintrc.js
mail/test/static/browser.ini
mail/test/static/browser_parsable_css.js
mail/test/static/browser_parsable_script.js
mail/test/static/dummy_page.html
mail/test/static/head.js
mail/test/static/moz.build
--- a/mail/moz.build
+++ b/mail/moz.build
@@ -19,11 +19,12 @@ if CONFIG['MAKENSISU']:
     DIRS += ['installer/windows']
 
 if CONFIG['MOZ_BUNDLED_FONTS']:
     DIRS += ['/%s/browser/fonts' % CONFIG['mozreltopsrcdir']]
 
 TEST_DIRS += [
     'test/browser',
     'test/marionette',
+    'test/static',
 ]
 
 FINAL_TARGET_FILES.defaults += ['app/permissions']
new file mode 100644
--- /dev/null
+++ b/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",
+  },
+};
new file mode 100644
--- /dev/null
+++ b/mail/test/static/browser.ini
@@ -0,0 +1,18 @@
+[DEFAULT]
+head = head.js
+prefs =
+  ldap_2.servers.osx.description=
+  ldap_2.servers.osx.dirType=-1
+  ldap_2.servers.osx.uri=
+  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]
new file mode 100644
--- /dev/null
+++ b/mail/test/static/browser_parsable_css.js
@@ -0,0 +1,545 @@
+/* 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\/client\/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|svg)\.css$/,
+    errorMessage: /Expected media feature name but found \u2018-moz.*/i,
+    isFromDevTools: false,
+  },
+
+  {
+    sourceName: /\b(contenteditable|EditorOverride|svg|forms|html|mathml|ua|pluginproblem)\.css$/i,
+    errorMessage: /Unknown pseudo-class.*-moz-/i,
+    isFromDevTools: false,
+  },
+  {
+    sourceName: /\b(html|mathml|ua)\.css$/i,
+    errorMessage: /Unknown property.*-moz-/i,
+    isFromDevTools: false,
+  },
+  {
+    sourceName: /(minimal-xul|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,
+  },
+  // System colors reserved to UA / chrome sheets
+  {
+    sourceName: /(?:res|gre-resources)\/forms\.css$/i,
+    errorMessage: /Expected color but found \u2018-moz.*/i,
+    platforms: ["linux"],
+    isFromDevTools: false,
+  },
+  // The '-moz-menulist-button' value is only supported in chrome and UA sheets
+  // but forms.css is loaded as a document sheet by this test.
+  // Maybe bug 1261237 will fix this?
+  {
+    sourceName: /(?:res|gre-resources)\/forms\.css$/i,
+    errorMessage: /Error in parsing value for \u2018-moz-appearance\u2019/iu,
+    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,
+  },
+];
+
+if (
+  !Services.prefs.getBoolPref(
+    "layout.css.xul-box-display-values.content.enabled"
+  )
+) {
+  // These are UA sheets which use non-content-exposed `display` values.
+  whitelist.push({
+    sourceName: /(skin\/shared\/Heartbeat|((?:res|gre-resources)\/(ua|html)))\.css$/i,
+    errorMessage: /Error in parsing value for .*\bdisplay\b/i,
+    isFromDevTools: false,
+  });
+}
+
+if (
+  !Services.prefs.getBoolPref(
+    "layout.css.line-height-moz-block-height.content.enabled"
+  )
+) {
+  // -moz-block-height is used in form controls but not exposed to the web.
+  whitelist.push({
+    sourceName: /(?:res|gre-resources)\/forms\.css$/i,
+    errorMessage: /Error in parsing value for \u2018line-height\u2019/iu,
+    isFromDevTools: false,
+  });
+}
+
+if (!Services.prefs.getBoolPref("layout.css.scrollbar-width.enabled")) {
+  whitelist.push({
+    sourceName: /(?:res|gre-resources)\/forms\.css$/i,
+    errorMessage: /Unknown property .*\bscrollbar-width\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,
+  });
+}
+
+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 = [
+  {
+    sourceName: /^chrome:\/\/calendar\/skin\/shared\/suite/,
+    isFromDevTools: false,
+  },
+  {
+    sourceName: /^chrome:\/\/messenger\/skin\/shared\/customizableui\/panelUI.css/,
+    isFromDevTools: false,
+  },
+];
+
+// 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
+ * @return 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 processCSSRules(sheet) {
+  for (let rule of sheet.cssRules) {
+    if (rule instanceof CSSConditionRule || rule instanceof CSSKeyframesRule) {
+      processCSSRules(rule);
+      continue;
+    }
+    if (!(rule instanceof CSSStyleRule) && !(rule instanceof CSSKeyframeRule)) {
+      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 "--" preceeded 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");
+      Cu.reportError(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.import(
+    "resource://gre/modules/HiddenFrame.jsm",
+    {}
+  ).HiddenFrame;
+  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:
+  iframe.remove();
+  doc.head.innerHTML = "";
+  doc = null;
+  iframe = null;
+  win = null;
+  hiddenFrame.destroy();
+  hiddenFrame = null;
+  imageURIsToReferencesMap = null;
+  customPropsToReferencesMap = null;
+});
new file mode 100644
--- /dev/null
+++ b/mail/test/static/browser_parsable_script.js
@@ -0,0 +1,168 @@
+/* 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$/,
+]);
+
+// 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
+ * @return 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
+ * @return 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 explictly 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");
+});
new file mode 100644
--- /dev/null
+++ b/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>
new file mode 100644
--- /dev/null
+++ b/mail/test/static/head.js
@@ -0,0 +1,173 @@
+/* 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"
+);
+
+var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+var { OS, require } = ChromeUtils.import("resource://gre/modules/osfile.jsm");
+
+/**
+ * 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;
+  })();
+}
+
+/**
+ * Uses OS.File.DirectoryIterator to asynchronously iterate over 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.
+ */
+function iterateOverPath(path, extensions) {
+  let iterator = new OS.File.DirectoryIterator(path);
+  let parentDir = new LocalFile(path);
+  let subdirs = [];
+  let files = [];
+
+  let pathEntryIterator = entry => {
+    if (entry.isDir) {
+      subdirs.push(entry.path);
+    } else if (extensions.some(extension => entry.name.endsWith(extension))) {
+      let file = parentDir.clone();
+      file.append(entry.name);
+      // 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 (
+      entry.name.endsWith(".ja") ||
+      entry.name.endsWith(".jar") ||
+      entry.name.endsWith(".zip") ||
+      entry.name.endsWith(".xpi")
+    ) {
+      let file = parentDir.clone();
+      file.append(entry.name);
+      for (let extension of extensions) {
+        let jarEntryIterator = generateEntriesFromJarFile(file, extension);
+        files.push(...jarEntryIterator);
+      }
+    }
+  };
+
+  return new Promise((resolve, reject) => {
+    (async function() {
+      try {
+        // Iterate through the directory
+        await iterator.forEach(pathEntryIterator);
+        resolve({ files, subdirs });
+      } catch (ex) {
+        reject(ex);
+      } finally {
+        iterator.close();
+      }
+    })();
+  });
+}
+
+/* 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);
+}
new file mode 100644
--- /dev/null
+++ b/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',
+]