Bug 971096 - add test to parse all our CSS and check for obvious issues, r=mconley
authorGijs Kruitbosch <gijskruitbosch@gmail.com>
Thu, 01 May 2014 13:04:47 +0100
changeset 181677 9c0ab8f376e62960523efac5ac98c58e48242abd
parent 181676 706bc42060aac8d53f18e8d37bbffabb089a0f71
child 181678 a38146bf6d189c919f7ef1ee7f99d0fb42d048db
push id272
push userpvanderbeken@mozilla.com
push dateMon, 05 May 2014 16:31:18 +0000
reviewersmconley
bugs971096
milestone32.0a1
Bug 971096 - add test to parse all our CSS and check for obvious issues, r=mconley
browser/base/content/test/general/browser.ini
browser/base/content/test/general/browser_parsable_css.js
--- a/browser/base/content/test/general/browser.ini
+++ b/browser/base/content/test/general/browser.ini
@@ -323,16 +323,19 @@ skip-if = e10s # Bug ?????? - uncaught e
 [browser_offlineQuotaNotification.js]
 skip-if = e10s # Bug ?????? - test directly manipulates content (gBrowser.selectedBrowser.contentWindow.applicationCache.oncached = function() {...})
 [browser_overflowScroll.js]
 skip-if = e10s # Bug 921905 - pinTab/unpinTab fail in e10s
 [browser_pageInfo.js]
 skip-if = e10s # Bug 866413 - PageInfo doesn't work in e10s
 [browser_page_style_menu.js]
 skip-if = e10s # Bug ?????? - test directly manipulates content
+
+[browser_parsable_css.js]
+
 [browser_pinnedTabs.js]
 skip-if = e10s # Bug 921905 - pinTab/unpinTab fail in e10s
 [browser_plainTextLinks.js]
 skip-if = e10s # Bug ?????? - test directly manipulates content (creates and fetches elements directly from content document)
 [browser_popupNotification.js]
 skip-if = toolkit == "windows" || e10s # Disabled on Windows due to frequent failures (bugs 825739, 841341) / e10s - Bug ?????? - popup notification test probably confused re content process notifications etc
 [browser_popupUI.js]
 skip-if = e10s # Bug ?????? - test directly manipulates content (tries to get a popup element directly from content)
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/general/browser_parsable_css.js
@@ -0,0 +1,218 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* 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. */
+const kWhitelist = [
+  {sourceName: /cleopatra.*(tree|ui)\.css/i}, /* Cleopatra is imported as-is, see bug 1004421 */
+  {sourceName: /codemirror\.css/i}, /* CodeMirror is imported as-is, see bug 1004423 */
+  {sourceName: /web\/viewer\.css/i, errorMessage: /Unknown pseudo-class.*(fullscreen|selection)/i }, /* PDFjs is futureproofing its pseudoselectors, and those rules are dropped. */
+  {sourceName: /aboutaccounts\/(main|normalize)\.css/i}, /* Tracked in bug 1004428 */
+];
+
+/**
+ * Check if an error should be ignored due to matching one of the whitelist
+ * objects defined in kWhitelist
+ *
+ * @param aErrorObject the error to check
+ * @return true if the error should be ignored, false otherwise.
+ */
+function ignoredError(aErrorObject) {
+  for (let whitelistItem of kWhitelist) {
+    let matches = true;
+    for (let prop in whitelistItem) {
+      if (!whitelistItem[prop].test(aErrorObject[prop] || "")) {
+        matches = false;
+        break;
+      }
+    }
+    if (matches) {
+      return true;
+    }
+  }
+  return false;
+}
+
+
+/**
+ * Returns a promise that is resolved with a list of CSS files to check,
+ * represented by their nsIURI objects.
+ *
+ * @param appDir the application directory to scan for CSS files (nsIFile)
+ */
+function generateURIsFromDirTree(appDir) {
+  let rv = [];
+  let dirQueue = [appDir.path];
+  return Task.spawn(function*() {
+    while (dirQueue.length) {
+      let nextDir = dirQueue.shift();
+      let {subdirs, cssfiles} = yield iterateOverPath(nextDir);
+      dirQueue = dirQueue.concat(subdirs);
+      rv = rv.concat(cssfiles);
+    }
+    return rv;
+  });
+}
+
+/* Shorthand constructor to construct an nsI(Local)File */
+let LocalFile = Components.Constructor("@mozilla.org/file/local;1", Ci.nsIFile, "initWithPath");
+
+/**
+ * Uses OS.File.DirectoryIterator to asynchronously iterate over a directory.
+ * It returns a promise that is resolved with an object with two properties:
+ *  - cssfiles: an array of nsIURIs corresponding to CSS that needs checking
+ *  - subdirs: an array of paths for subdirectories we need to recurse into
+ *             (handled by generateURIsFromDirTree above)
+ *
+ * @param path the path to check (string)
+ */
+function iterateOverPath(path) {
+  let iterator = new OS.File.DirectoryIterator(path);
+  let parentDir = new LocalFile(path);
+  let subdirs = [];
+  let cssfiles = [];
+  // Iterate through the directory
+  let promise = iterator.forEach(
+    function onEntry(entry) {
+      if (entry.isDir) {
+        let subdir = parentDir.clone();
+        subdir.append(entry.name);
+        subdirs.push(subdir.path);
+      } else if (entry.name.endsWith(".css")) {
+        let file = parentDir.clone();
+        file.append(entry.name);
+        let uriSpec = getURLForFile(file);
+        cssfiles.push(Services.io.newURI(uriSpec, null, null));
+      } else if (entry.name.endsWith(".ja")) {
+        let file = parentDir.clone();
+        file.append(entry.name);
+        let subentries = [uri for (uri of generateEntriesFromJarFile(file))];
+        cssfiles = cssfiles.concat(subentries);
+      }
+    }
+  );
+
+  let outerPromise = Promise.defer();
+  promise.then(function() {
+    outerPromise.resolve({cssfiles: cssfiles, subdirs: subdirs});
+    iterator.close();
+  }, function(e) {
+    outerPromise.reject(e);
+    iterator.close();
+  });
+  return outerPromise.promise;
+}
+
+/* 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.getURLSpecFromFile(file);
+}
+
+/**
+ * A generator that generates nsIURIs for CSS files found in jar files
+ * like omni.ja.
+ *
+ * @param jarFile an nsIFile object for the jar file that needs checking.
+ */
+function* generateEntriesFromJarFile(jarFile) {
+  const ZipReader = new Components.Constructor("@mozilla.org/libjar/zip-reader;1", "nsIZipReader", "open");
+  let zr = new ZipReader(jarFile);
+  let entryEnumerator = zr.findEntries("*.css$");
+
+  const kURIStart = getURLForFile(jarFile);
+  while (entryEnumerator.hasMore()) {
+    let entry = entryEnumerator.getNext();
+    let entryURISpec = "jar:" + kURIStart + "!/" + entry;
+    yield Services.io.newURI(entryURISpec, null, null);
+  }
+  zr.close();
+}
+
+/**
+ * The actual test.
+ */
+add_task(function checkAllTheCSS() {
+  let appDir = Services.dirsvc.get("XCurProcD", 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 = yield generateURIsFromDirTree(appDir);
+
+  // Create a clean iframe to load all the files into:
+  let hiddenWin = Services.appShell.hiddenDOMWindow;
+  let iframe = hiddenWin.document.createElementNS("http://www.w3.org/1999/xhtml", "html:iframe");
+  hiddenWin.document.documentElement.appendChild(iframe);
+  let doc = iframe.contentWindow.document;
+
+
+  // Listen for errors caused by the CSS:
+  let errorListener = {
+    observe: function(aMessage) {
+      if (!aMessage || !(aMessage instanceof Ci.nsIScriptError)) {
+        return;
+      }
+      // Only care about CSS errors generated by our iframe:
+      if (aMessage.category.contains("CSS") && aMessage.innerWindowID === 0 && aMessage.outerWindowID === 0) {
+        // Check if this error is whitelisted in kWhitelist
+        if (!ignoredError(aMessage)) {
+          ok(false, "Got error message for " + aMessage.sourceName + ": " + aMessage.errorMessage);
+          errors++;
+        } else {
+          info("Ignored error for " + aMessage.sourceName + " because of filter.");
+        }
+      }
+    }
+  };
+
+  // We build a list of promises that get resolved when their respective
+  // files have loaded and produced no errors.
+  let allPromises = [];
+  let errors = 0;
+  // Register the error listener to keep track of errors.
+  Services.console.registerListener(errorListener);
+  for (let uri of uris) {
+    let linkEl = doc.createElement("link");
+    linkEl.setAttribute("rel", "stylesheet");
+    let promiseForThisSpec = Promise.defer();
+    let onLoad = (e) => {
+      promiseForThisSpec.resolve();
+      linkEl.removeEventListener("load", onLoad);
+      linkEl.removeEventListener("error", onError);
+    };
+    let onError = (e) => {
+      promiseForThisSpec.reject({error: e, href: linkEl.getAttribute("href")});
+      linkEl.removeEventListener("load", onLoad);
+      linkEl.removeEventListener("error", onError);
+    };
+    linkEl.addEventListener("load", onLoad);
+    linkEl.addEventListener("error", onError);
+    linkEl.setAttribute("type", "text/css");
+    linkEl.setAttribute("href", uri.spec);
+    allPromises.push(promiseForThisSpec.promise);
+    doc.head.appendChild(linkEl);
+  }
+
+  // Wait for all the files to have actually loaded:
+  yield Promise.all(allPromises);
+  // Count errors (the test output will list actual issues for us, as well
+  // as the ok(false) in the error listener)
+  is(errors, 0, "All the styles (" + allPromises.length + ") loaded without errors.");
+
+  // Clean up to avoid leaks:
+  Services.console.unregisterListener(errorListener);
+  iframe.remove();
+  doc.head.innerHTML = '';
+  doc = null;
+  iframe = null;
+});
+
+