diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
commit | 6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /comm/mail/test/static | |
parent | Initial commit. (diff) | |
download | thunderbird-upstream.tar.xz thunderbird-upstream.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'comm/mail/test/static')
-rw-r--r-- | comm/mail/test/static/.eslintrc.js | 11 | ||||
-rw-r--r-- | comm/mail/test/static/browser.ini | 15 | ||||
-rw-r--r-- | comm/mail/test/static/browser_parsable_css.js | 573 | ||||
-rw-r--r-- | comm/mail/test/static/browser_parsable_script.js | 169 | ||||
-rw-r--r-- | comm/mail/test/static/dummy_page.html | 9 | ||||
-rw-r--r-- | comm/mail/test/static/head.js | 158 | ||||
-rw-r--r-- | comm/mail/test/static/moz.build | 8 |
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", +] |