diff options
Diffstat (limited to 'browser/base/content/test/general/browser_parsable_css.js')
-rw-r--r-- | browser/base/content/test/general/browser_parsable_css.js | 376 |
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; +}); |