summaryrefslogtreecommitdiffstats
path: root/browser/base/content/test/general/browser_parsable_css.js
diff options
context:
space:
mode:
Diffstat (limited to 'browser/base/content/test/general/browser_parsable_css.js')
-rw-r--r--browser/base/content/test/general/browser_parsable_css.js376
1 files changed, 376 insertions, 0 deletions
diff --git a/browser/base/content/test/general/browser_parsable_css.js b/browser/base/content/test/general/browser_parsable_css.js
new file mode 100644
index 000000000..72954d2e5
--- /dev/null
+++ b/browser/base/content/test/general/browser_parsable_css.js
@@ -0,0 +1,376 @@
+/* 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. */
+let whitelist = [
+ // CodeMirror is imported as-is, see bug 1004423.
+ {sourceName: /codemirror\.css$/i,
+ isFromDevTools: true},
+ // The debugger uses cross-browser CSS.
+ {sourceName: /devtools\/client\/debugger\/new\/styles.css/i,
+ isFromDevTools: true},
+ // PDFjs is futureproofing its pseudoselectors, and those rules are dropped.
+ {sourceName: /web\/viewer\.css$/i,
+ errorMessage: /Unknown pseudo-class.*(fullscreen|selection)/i,
+ isFromDevTools: false},
+ // Tracked in bug 1004428.
+ {sourceName: /aboutaccounts\/(main|normalize)\.css$/i,
+ isFromDevTools: false},
+ // 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},
+ // Responsive Design Mode CSS uses a UA-only pseudo-class, see Bug 1241714.
+ {sourceName: /responsive-ua\.css$/i,
+ errorMessage: /Unknown pseudo-class.*moz-dropdown-list/i,
+ isFromDevTools: true},
+
+ {sourceName: /\b(contenteditable|EditorOverride|svg|forms|html|mathml|ua)\.css$/i,
+ errorMessage: /Unknown pseudo-class.*-moz-/i,
+ isFromDevTools: false},
+ {sourceName: /\b(html|mathml|ua)\.css$/i,
+ errorMessage: /Unknown property.*-moz-/i,
+ isFromDevTools: false},
+ // Reserved to UA sheets unless layout.css.overflow-clip-box.enabled flipped to true.
+ {sourceName: /res\/forms\.css$/i,
+ errorMessage: /Unknown property.*overflow-clip-box/i,
+ isFromDevTools: false},
+ {sourceName: /res\/(ua|html)\.css$/i,
+ errorMessage: /Unknown pseudo-class .*\bfullscreen\b/i,
+ isFromDevTools: false},
+ {sourceName: /skin\/timepicker\.css$/i,
+ errorMessage: /Error in parsing.*mask/i,
+ isFromDevTools: false},
+];
+
+// Platform can be "linux", "macosx" or "win". If omitted, the exception applies to all platforms.
+let allowedImageReferences = [
+ // Bug 1302691
+ {file: "chrome://devtools/skin/images/dock-bottom-minimize@2x.png",
+ from: "chrome://devtools/skin/toolbox.css",
+ isFromDevTools: true},
+ {file: "chrome://devtools/skin/images/dock-bottom-maximize@2x.png",
+ from: "chrome://devtools/skin/toolbox.css",
+ isFromDevTools: true},
+];
+
+var moduleLocation = gTestPath.replace(/\/[^\/]*$/i, "/parsingTestHelpers.jsm");
+var {generateURIsFromDirTree} = Cu.import(moduleLocation, {});
+
+// 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();
+
+/**
+ * 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 whitelistItem of whitelist) {
+ let matches = true;
+ for (let prop of ["sourceName", "errorMessage"]) {
+ if (whitelistItem.hasOwnProperty(prop) &&
+ !whitelistItem[prop].test(aErrorObject[prop] || "")) {
+ matches = false;
+ break;
+ }
+ }
+ if (matches) {
+ whitelistItem.used = true;
+ return true;
+ }
+ }
+ return false;
+}
+
+function once(target, name) {
+ return new Promise((resolve, reject) => {
+ let cb = () => {
+ target.removeEventListener(name, cb);
+ resolve();
+ };
+ target.addEventListener(name, cb);
+ });
+}
+
+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);
+ });
+}
+
+var gChromeReg = Cc["@mozilla.org/chrome/chrome-registry;1"]
+ .getService(Ci.nsIChromeRegistry);
+var gChromeMap = new Map();
+
+function getBaseUriForChromeUri(chromeUri) {
+ let chromeFile = chromeUri + "gobbledygooknonexistentfile.reallynothere";
+ let uri = Services.io.newURI(chromeFile, null, null);
+ 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+/);
+ let component;
+ if (type == "content" || type == "skin") {
+ [component] = argv;
+ } else {
+ // skip unrelated lines
+ continue;
+ }
+ let chromeUri = `chrome://${component}/${type}/`;
+ gChromeMap.set(getBaseUriForChromeUri(chromeUri), chromeUri);
+ }
+ });
+}
+
+function convertToChromeUri(fileUri) {
+ let baseUri = fileUri.spec;
+ let path = "";
+ while (true) {
+ let slashPos = baseUri.lastIndexOf("/", baseUri.length - 2);
+ if (slashPos < 0) {
+ info(`File not accessible from chrome protocol: ${fileUri.path}`);
+ return fileUri;
+ }
+ path = baseUri.slice(slashPos + 1) + path;
+ baseUri = baseUri.slice(0, slashPos + 1);
+ if (gChromeMap.has(baseUri)) {
+ let chromeBaseUri = gChromeMap.get(baseUri);
+ let chromeUri = `${chromeBaseUri}${path}`;
+ return Services.io.newURI(chromeUri, null, null);
+ }
+ }
+}
+
+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;
+ }
+ info(`Ignored error for ${sourceName} because of filter.`);
+ }
+ return false;
+}
+
+let imageURIsToReferencesMap = new Map();
+
+function processCSSRules(sheet) {
+ for (let rule of sheet.cssRules) {
+ if (rule instanceof CSSMediaRule) {
+ processCSSRules(rule);
+ continue;
+ }
+ if (!(rule instanceof CSSStyleRule))
+ continue;
+
+ // Extract urls from the css text.
+ // Note: CSSStyleRule.cssText always has double quotes around URLs even
+ // when the original CSS file didn't.
+ let urls = rule.cssText.match(/url\("[^"]*"\)/g);
+ if (!urls)
+ 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, null, null);
+ 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);
+ }
+ }
+ }
+}
+
+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 != Components.results.NS_ERROR_FILE_NOT_FOUND) {
+ dump("Checking " + aURI + ": " + e + "\n");
+ Cu.reportError(e);
+ }
+ }
+ return available > 0;
+}
+
+add_task(function* checkAllTheCSS() {
+ 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 = yield 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 windowless = Services.appShell.createWindowlessBrowser();
+ let iframe = windowless.document.createElementNS("http://www.w3.org/1999/xhtml", "html:iframe");
+ windowless.document.documentElement.appendChild(iframe);
+ let iframeLoaded = once(iframe, 'load');
+ iframe.contentWindow.location = testFile;
+ yield iframeLoaded;
+ let doc = iframe.contentWindow.document;
+
+ // 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 manifestPromises = [];
+ uris = uris.filter(uri => {
+ if (uri.path.endsWith(".manifest")) {
+ manifestPromises.push(parseManifest(uri));
+ return false;
+ }
+ return true;
+ });
+ // Wait for all manifest to be parsed
+ yield Promise.all(manifestPromises);
+
+ // We build a list of promises that get resolved when their respective
+ // files have loaded and produced no errors.
+ let allPromises = [];
+
+ // filter out either the devtools paths or the non-devtools paths:
+ let isDevtools = SimpleTest.harnessParameters.subsuite == "devtools";
+ let devtoolsPathBits = ["webide", "devtools"];
+ uris = uris.filter(uri => isDevtools == devtoolsPathBits.some(path => uri.spec.includes(path)));
+
+ for (let uri of uris) {
+ let linkEl = doc.createElement("link");
+ linkEl.setAttribute("rel", "stylesheet");
+ let promiseForThisSpec = Promise.defer();
+ let onLoad = (e) => {
+ processCSSRules(linkEl.sheet);
+ promiseForThisSpec.resolve();
+ linkEl.removeEventListener("load", onLoad);
+ linkEl.removeEventListener("error", onError);
+ };
+ let onError = (e) => {
+ ok(false, "Loading " + linkEl.getAttribute("href") + " threw an error!");
+ promiseForThisSpec.resolve();
+ linkEl.removeEventListener("load", onLoad);
+ linkEl.removeEventListener("error", onError);
+ };
+ linkEl.addEventListener("load", onLoad);
+ linkEl.addEventListener("error", onError);
+ linkEl.setAttribute("type", "text/css");
+ let chromeUri = convertToChromeUri(uri);
+ linkEl.setAttribute("href", chromeUri.spec + kPathSuffix);
+ allPromises.push(promiseForThisSpec.promise);
+ doc.head.appendChild(linkEl);
+ }
+
+ // Wait for all the files to have actually loaded:
+ yield Promise.all(allPromises);
+
+ // 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 ignored = false;
+ for (let item of allowedImageReferences) {
+ if (image.endsWith(item.file) && ref.endsWith(item.from) &&
+ isDevtools == item.isFromDevTools &&
+ (!item.platforms || item.platforms.includes(AppConstants.platform))) {
+ item.used = true;
+ ignored = true;
+ break;
+ }
+ }
+ if (!ignored)
+ ok(false, "missing " + image + " referenced from " + ref);
+ }
+ }
+ }
+
+ 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.
+ for (let item of whitelist) {
+ if (!item.used && isDevtools == item.isFromDevTools) {
+ ok(false, "Unused whitelist item. " +
+ (item.sourceName ? " sourceName: " + item.sourceName : "") +
+ (item.errorMessage ? " errorMessage: " + item.errorMessage : ""));
+ }
+ }
+
+ // Confirm that all file whitelist rules have been used.
+ for (let item of allowedImageReferences) {
+ if (!item.used && isDevtools == item.isFromDevTools &&
+ (!item.platforms || item.platforms.includes(AppConstants.platform))) {
+ ok(false, "Unused file whitelist item. " +
+ " file: " + item.file +
+ " from: " + item.from);
+ }
+ }
+
+ // Clean up to avoid leaks:
+ iframe.remove();
+ doc.head.innerHTML = '';
+ doc = null;
+ iframe = null;
+ windowless.close();
+ windowless = null;
+ imageURIsToReferencesMap = null;
+});